Roleplay Text Based Scenarios With an LLM API
Roleplay text based scenarios work best when the model plays a narrator with rules, and your code owns the state. This article walks through a small engine in Python: prompt, streaming, memory and a cost estimate for a long session.

Key points
- Split the work: the model writes the story, your code stores facts, inventory and rules.
- Stream replies and keep each stream short enough to free its concurrency slot quickly.
- A 100,000-token window holds a very long session, but summaries keep cost and drift down.
- A 200-turn session at 6,000 input and 400 output tokens per turn costs about $0.22.
Start with the narrator, not the plot
People who build roleplay text based scenarios often begin by writing a plot. That is backwards. A scenario is a loop between a player and a narrator, and the narrator needs a job description before it needs a story. The model has to know who speaks, what it may decide for the player, how long a reply should be, and what it should never do. Everything else can be improvised.
A good system prompt for a scenario engine has four parts. First comes the role: game master, interviewer, ship's computer, a character in a play. Second, the world rules in a few plain sentences: tone, setting, what exists and what does not. Third, formatting rules, because your code may parse them: for example, "end each reply with one line starting with OPTIONS:". Fourth, boundaries. The text API here is tuned to answer without content refusals for lawful adult use, which makes it suited to dark fiction, villains with real menace and morally grey plots. It still has one hard line: no sexual content involving minors. Put that rule in your own terms of use as well, and enforce an age gate in your app if your scenarios are aimed at adults.
Keep the prompt under about 600 words. Long rule lists get half-followed. If you need many rules, rank them and drop the bottom third.
A working loop with streaming
Streaming matters more in roleplay than in most uses. A player reading a story wants words to appear while the model writes, and a 300-token reply feels slower than it is if it lands all at once. The API streams over server-sent events with stream: true, and the OpenAI SDKs handle the parsing. The endpoint is OpenAI-compatible, the model ID is uncensored, and the base URL for this site is https://api.sdxlapi.com/v1.
from openai import OpenAI
client = OpenAI(base_url="https://api.sdxlapi.com/v1", api_key="YOUR_KEY")
SYSTEM = """You are the narrator of a noir mystery set in a rainy harbour city.
Write in second person, present tense, 120 to 200 words per reply.
Never decide what the player says or does. Invent details freely, but stay
consistent with the STATE block you are given.
End every reply with one line: OPTIONS: a | b | c"""
state = {"place": "Pier 9 office", "time": "23:40", "inventory": ["brass key"], "facts": []}
history = []
def turn(player_text: str) -> str:
history.append({"role": "user", "content": player_text})
msgs = [{"role": "system", "content": SYSTEM + "\nSTATE: " + str(state)}] + history[-20:]
stream = client.chat.completions.create(
model="uncensored", messages=msgs, stream=True,
temperature=0.9, top_p=0.95, max_tokens=500,
presence_penalty=0.3,
)
reply = ""
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
piece = chunk.choices[0].delta.content
reply += piece
print(piece, end="", flush=True)
history.append({"role": "assistant", "content": reply})
return reply
turn("I check the desk drawer.")The settings are tuned for fiction. A temperature around 0.8 to 1.0 gives varied prose, and top_p near 0.95 trims the strangest word choices. A small presence penalty discourages the narrator from circling back to the same images. Set max_tokens to cap reply length; it is optional, but without it a chatty narrator can write far more than you wanted. For a scene you want to replay exactly during testing, pass seed and keep the other parameters fixed.
Let code own the facts
The most common failure in long roleplay is drift. The key is in the player's pocket in turn 5 and on the table in turn 40. The innkeeper was called Mara, then Marta. A model reading 30,000 tokens of history will often get this right, but "often" is not a design. Keep the facts in a small structure that your program updates, and put that structure into every request, as the STATE line above does.
How do you update the state? Ask the model to report changes after each turn, in a second, cheap call that uses JSON mode. The first call writes the story for the player. The second reads the new passage and returns only what changed.
import json
def extract_changes(passage: str) -> dict:
r = client.chat.completions.create(
model="uncensored",
response_format={"type": "json_object"},
temperature=0,
max_tokens=250,
messages=[
{"role": "system", "content":
'Read the passage. Return JSON: {"place": str|null, "inventory_add": [str], '
'"inventory_remove": [str], "new_facts": [str]}. Only report what the passage states.'},
{"role": "user", "content": passage},
],
)
return json.loads(r.choices[0].message.content)
def apply(ch: dict):
if ch.get("place"):
state["place"] = ch["place"]
for item in ch.get("inventory_add", []):
if item not in state["inventory"]:
state["inventory"].append(item)
state["inventory"] = [i for i in state["inventory"] if i not in ch.get("inventory_remove", [])]
state["facts"] = (state["facts"] + ch.get("new_facts", []))[-30:]Cap the list of facts, as the slice does, or the state block grows until it is as heavy as the history it was meant to replace. If a JSON reply fails to parse, skip the update for that turn and move on; one missed fact is cheaper than a crash.
Dice, health and timers are better handled in plain code. When the player tries something risky, roll in Python and tell the narrator the outcome: "The player's attempt fails." The model then writes the consequences. This keeps the model from quietly letting the player win every time, which language models tend to do.
Memory for sessions that run for hours
The context window is 100,000 tokens, counting prompt and completion together. A typical reply of 250 tokens plus a 30-token player message is about 280 tokens per turn, so raw history alone could hold hundreds of turns. You can send it all. Many builders still do not, for two reasons: cost grows with every turn, because the whole history is billed as input each time, and old details can distract the model.
A reasonable layout keeps three layers:
- Pinned: the system prompt and the state block. Always sent.
- Summary: a running paragraph or two of what has happened, rewritten every 10 to 15 turns by a short call.
- Recent turns: the last 10 to 20 messages verbatim, so the prose keeps its rhythm.
When the window of recent turns fills, fold the oldest ten into the summary and drop them. The summary call can use a low temperature and a limit of 300 tokens. Players rarely notice the compression, as long as names, promises and unresolved threats make it into the summary. Say that explicitly in the summarising prompt: "keep names, debts, injuries, open questions".
If you want a character to keep a stable voice, give it a short style card in the pinned layer: three example lines of its speech and two things it never says. Short examples shape tone better than adjectives.
What a session costs
Pricing is $0.15 per 1M input tokens and $0.50 per 1M output tokens, with prepaid credit and no subscription. Work through a long session. Say 200 turns, with the pinned layer, summary and recent turns adding up to an average of 6,000 input tokens per request, and 400 output tokens per reply (the story reply plus a small share for state and summary calls).
Input: 200 x 6,000 = 1,200,000 tokens, which is 1.2 x $0.15 = $0.18. Output: 200 x 400 = 80,000 tokens, which is 0.08 x $0.50 = $0.04. The session costs $0.22.
Compare the lazy version that resends the whole raw history. By turn 200 the history is around 56,000 tokens, and across the session the average input is about 28,000 tokens per request. That is 200 x 28,000 = 5,600,000 input tokens, or 5.6 x $0.15 = $0.84, plus the same $0.04 for output, which makes $0.88. Still affordable, but four times the bill for no gain in quality. For an app with many players, that gap decides your margin.
Failed or refused requests are not charged, and credit never expires, so unused balance simply waits. Top-ups run from $20 to $500 in whole dollars, with a 5% bonus from $50, 10% from $100 and 15% from $200. See the pricing page for the full list of options.
Running many players at once
A full key allows 8 concurrent requests and 300 requests per minute. A streamed request holds its slot until the stream ends, so with 8 slots and replies that take about ten seconds, you can serve roughly 48 reply streams per minute, assuming nothing else runs. That is a rough rule, not a measured figure, since speed varies with load and reply length. The point is to plan for it: if your game has more simultaneous players than slots, queue the requests in your server and show a short "narrator is thinking" state instead of firing everything at once.
When you exceed a limit the API returns HTTP 429. The code is rate_limited for requests per minute and concurrency for too many at once. Temporary overload also gives 429, with a Retry-After: 5 header. A simple retry that waits those five seconds is enough for a casual game. Read the response headers too: X-RateLimit-Limit-Requests and X-RateLimit-Limit-Concurrency show your caps, and X-Balance-USD shows what is left, which you can surface in an admin screen.
Do not run the state-extraction call in parallel with the next player turn. Wait for it, or the narrator will write from stale facts. A good compromise is to start the extraction as soon as the stream ends and let the player read while it runs.
Connecting scenes to your image pipeline
This site is built for people who use text models around Stable Diffusion workflows, and scenarios fit that well. The API itself is text only: it generates no images, audio or video. But the narrator can write a scene description that your pipeline turns into an image prompt. Add one more JSON call after a major scene change, asking for a short visual description of the setting with lighting, mood and camera angle, then pass that string to whichever image tool you run. The docs page shows the request shape for prompt-writing calls, and the article on the SD prompt generator goes deeper on that side.
To try the engine, create an account and claim the trial. New users can get $0.50 of trial credit with one click on the API key page, valid for 7 days with no card needed. By the numbers above, that is enough for a couple of full sessions. Trial keys are limited to 2 concurrent requests and 60 per minute, which fits solo testing.
Keep fiction in the fiction. Adult-themed scenarios are lawful use when everyone involved is an adult; the one rule that never bends is no sexual content involving minors. Prompts sent to the API are not used for training.
More guides
Questions and answers
Can the model play any character or genre?
It is a general text model tuned not to refuse lawful adult requests, so horror, crime, drama and comedy are all fine. The one hard limit is sexual content involving minors.
How long can a session be before the context fills up?
The window is 100,000 tokens for prompt plus completion. With a summary layer you can run far past that in practice, since old turns are compressed.
Should I stream the replies?
Yes, for anything a person reads live. Streaming gives you usage at the end, and a stream occupies one of your concurrency slots until it finishes.
Is there a way to make a scene repeat exactly?
Pass the same seed with identical messages and settings. It is a testing aid and does not guarantee byte-identical output.
Your key is one form away
Create an account, copy the key, change the base URL. That is the whole setup.