OpenAI and OpenAI-compatible keys
In the desktop or Android app
- Create a key at platform.openai.com/api-keys on an account with billing set up. Give it a name like "Open Dungeon Master" so you can revoke it later.
- Home screen, Story AI, AI Dungeon Master via your OpenAI API key.
- Paste the key. The Dungeon Master model defaults to
gpt-5.1and the utility model togpt-5-mini. Save.
That is the whole setup. The key is the device's: every campaign on the world, including ones made before the key existed, narrates with it and paints with it. The screen remembers that a key is saved ("Saved on this device. Blank keeps it") and brings the model names back filled in, so switching models later is one field. Inside a campaign, the Setup tab shows the device's storyteller and keeps pictures on or off, their shape and speed, and passage length per campaign.
On a self-hosted server
Admin, Server settings, Text model: provider Custom backend, backend URL https://api.openai.com/v1, the model you pay for, and the key. Or in .env.server:
DEFAULT_TEXT_PROVIDER=custom
OPENAI_COMPAT_BASE_URL=https://api.openai.com/v1
OPENAI_COMPAT_MODEL=gpt-5.1
OPENAI_COMPAT_API_KEY=sk-...
UTILITY_TEXT_MODEL=gpt-5-miniAn administrator can point a single campaign elsewhere in its Setup tab with a key of its own; campaigns anyone else owns use the server's. Press Test backend once: it streams a line, makes a real function call and continues after it, the way a turn does, and shows which OpenAI route the model takes. A saved key only goes to the host it was saved for: moving the storyteller to another address clears it, and Test backend never tries it against a newly typed one.
One key for pictures too
A story whose backend host is api.openai.com lends its key to the OpenAI image backend, so nobody pastes the same key twice: scene art, location maps and portraits paint on it. The lending is gated on the host, so a key for llama.cpp, LM Studio or OpenRouter is never sent to OpenAI. On a server, campaign covers and library portraits paint from the server's own image settings, not a campaign's key; set OPENAI_IMAGE_API_KEY (or the Images section) for those. The default image model is gpt-image-1; fast and slow map to medium and high quality.
Context
OpenAI models are packed against a 128K token window by name, so far more of the story reaches the Dungeon Master than the 16K stand-in an unknown endpoint gets. Retired small models keep their real sizes. OPENAI_COMPAT_CONTEXT overrides in either direction.
Strict-vendor handling
The server recognises the endpoint by its host. For openai.com it sends only what OpenAI accepts: no top_k, min_p or repeat penalty, max_completion_tokens instead of max_tokens, and thinking requested the way OpenAI expects. A reasoning model that rejects temperature or another field gets the field dropped and the request retried, up to three fields. OpenAI's GPT-6 models call their tools where they can: GPT-6 Sol and Luna on Chat Completions without reasoning, GPT-6.1 Sol and GPT-6 Astra over the Responses API, and a model the server has not met learns its route from OpenAI's refusal within the same turn; tools are never dropped in silence. You do not configure any of this.
Cost
You pay OpenAI per token. A turn sends the packed prompt (tens of thousands of tokens on a long campaign) and receives a few hundred back; the utility model handles the cheap chores. Pictures are the expensive part of a session and can be turned off, set to fast, or limited by shape in the Setup tab. Watch usage on OpenAI's dashboard for the first session or two.
Any other OpenAI-compatible endpoint
The same three fields work for anything that speaks the chat completions API with tool calling: a hosted router, a vLLM or LM Studio server on your network, a provider's own compatible endpoint. Use the provider's base URL (ending in /v1 for most), the model id exactly as they name it, and their key. On the apps, these go in the world's admin panel rather than the Story AI screen. Endpoints that are neither OpenAI nor OpenRouter are treated as local and permissive: every sampling parameter is sent, and the context is assumed to be 16K unless the endpoint reports it (llama.cpp does) or you set OPENAI_COMPAT_CONTEXT.