Integration
Kiori never writes your chat. Your app keeps the conversation, the model and the prompts; Kiori gives every character a brain. There are three ways in, from the least code to the most control. All of them take your own ids: you never have to store ours.
Before you start: ask for access on kiori.io; you'll get a sign-in link to the console. There, in Settings, add your model provider key (OpenRouter, or any OpenAI-compatible API): Kiori builds and updates your characters' brains with it, and you pay your provider directly. Then make an API key for your app. The hosted API is at https://api.kiori.io (the examples below use a self-hosted http://127.0.0.1:7700; use whichever you have).
1. The drop-in: change two lines
If your app already calls an OpenAI-compatible chat API (OpenAI, OpenRouter, Together, Groq, DeepSeek, a local server…), point it at Kiori and say which story it is. Kiori syncs the messages, adds each character's mind to your system prompt, passes the request on to your model with your provider key, and records the reply. Streaming works.
import OpenAI from 'openai';
const ai = new OpenAI({
baseURL: 'http://127.0.0.1:7700/v1', // Kiori instead of your provider
apiKey: process.env.KIORI_API_KEY, // your Kiori project key
defaultHeaders: {
'X-Kiori-Upstream-Url': 'https://openrouter.ai/api/v1', // where your model lives
'X-Kiori-Upstream-Key': process.env.OPENROUTER_API_KEY, // your key there
},
});
const reply = await ai.chat.completions.create(
{ model: 'your/model', messages, stream: true }, // exactly what you send today
{ headers: { 'X-Kiori-Story': chat.id, 'X-Kiori-Characters': character.id } }, // your own ids
);
from openai import OpenAI
ai = OpenAI(base_url="http://127.0.0.1:7700/v1", api_key=KIORI_API_KEY,
default_headers={"X-Kiori-Upstream-Url": "https://openrouter.ai/api/v1", "X-Kiori-Upstream-Key": OPENROUTER_API_KEY})
reply = ai.chat.completions.create(model="your/model", messages=messages,
extra_headers={"X-Kiori-Story": chat_id, "X-Kiori-Characters": character_id})
X-Kiori-Story: your conversation id. The first call makes the story;X-Kiori-Characters(your character ids or names, comma separated) says who is in it,X-Kiori-Playerthe player's name.- Your messages go as they are. Give them ids (
{ id, role, content }) if you have them: edits and rerolls are then recognized exactly (without ids, by their words). X-Kiori-Check:auto(default: when the player brings up the past, the reply is held, checked by the auditor and fixed before it's sent, even on a streaming request),true(always),false(never). The answer'sX-Kiori-Checkheader says what happened.- The provider key can live in the project instead of every request:
PATCH /v1/project { "settings": { "upstream": { "url": "...", "key": "..." } } }(it is never shown back). - Instead of headers,
"kiori": { "story", "characters", "player", "check" }in the body works too (Kiori takes it out before your model sees the request). - Your system prompt stays first; Kiori's block is added after it (a system message of its own if you have none).
GET /v1/modelslists your provider's models. - Only https providers on the internet are passed on to; a self-hosted Kiori talking to a model on the same machine sets
KIORI_PROXY_ALLOW_LOCAL=1.
2. One call per turn
For full control of your prompt: before you generate, send the story and your recent messages; get each character's context back.
import { Kiori } from './sdk/kiori.mjs';
const kiori = new Kiori({ apiKey: process.env.KIORI_API_KEY });
const turn = await kiori.turn({
story: { external_id: chat.id, characters: [character.id], player: { name: user.name } },
messages: chat.messages.slice(-30), // as they are now, oldest first: { id, role, content }
});
let reply = await yourModel([{ role: 'system', content: `${yourCharacterPrompt}\n\n${turn.characters[0].text}` }, ...chat.messages]);
if (turn.check_recommended) reply = (await kiori.stories.check(turn.story.id, { draft: reply })).fixed;
turn = kiori.turn(story={"external_id": chat_id, "characters": [character_id], "player": {"name": user_name}},
messages=messages[-30:])
system_prompt = character_prompt + "\n\n" + turn["characters"][0]["text"]
curl http://127.0.0.1:7700/v1/turn -H "Authorization: Bearer $KIORI_API_KEY" -H "Content-Type: application/json" -d '{
"story": { "external_id": "chat_7", "characters": ["char_42"], "player": { "name": "Leo" } },
"messages": [ { "id": "m1", "role": "user", "content": "Hey Mira." } ]
}'
- Edits, rerolls, deleted messages: nothing to do. Send the list as it is now; from the first message that changed, the story is rewound and written again, and only what those messages taught is learned again. Anything after the last message you send counts as deleted, so always send the latest ones.
- The reply is learned on the next turn (it's in the list then). To have it learned even if the user leaves, also send the list with the reply to
stories.sync(story, messages). - A story with one character: they're present in every message unless you say otherwise. A cast: the context defaults to whoever is in the current scene (up to 4, placed first by one small model call); name them with
charactersto choose. check_recommendedis true when the player's last message brings up the past ("do you remember", "how's your sister", "hani geçen yaz"): that's where models invent a shared past or know what they shouldn't. Runcheckon those turns and stream the rest.
Your ids and your character cards
external_idon characters and stories is yours; anywhere one of our ids goes (paths,characterslists,story), yours works too. Creating a character or a story again with the sameexternal_idgives the same one back, so you can call create without checking first.characters.create({ external_id, character_card })takes a Tavern / SillyTavern character card (v1, v2 or v3, as JSON, or the card PNG in base64): its description, personality, scenario, example dialogue and lorebook become what the brain is built from, and the scenario, first message and examples stay on the character's card. Adddescriptionorlorebeside it to give more.
3. The building blocks
turn is made of these; use them directly when your flow is different.
A cast, each voiced separately (agents, NPCs, a group chat): give each one only their own block. A model can't leak what it was never given.
const { characters } = await kiori.stories.context(story.id, { characters: ['Ines', 'Tomas'], messages: recent });
for (const c of characters) {
const line = await yourModel([{ role: 'system', content: `${cards[c.character.name]}\n\n${c.text}` }, ...recent]);
await kiori.stories.record(story.id, [{ role: 'character', speaker: c.character.name, text: line, present: scenePeople }]);
}
One narrator for everyone (games, interactive fiction): one model voices the whole scene and sees every character's block, so label each block, tell the model to keep them apart, and check before showing the reply.
const { characters } = await kiori.stories.context(story.id, { characters: 'scene', place: true, messages: recent });
const minds = characters.map(c => `### ${c.character.name} (only ${c.character.name} knows this)\n${c.text}`).join('\n\n');
const draft = await yourModel([{ role: 'system', content: `${narratorRules}\n\n${minds}` }, ...recent]);
const { fixed } = await kiori.stories.check(story.id, { draft, place: true });
The console's Playground tab is a working version of this pattern.
recordadds events;rewind(DELETE /v1/stories/:id/events?from=N) removes event N and everything after it;syncdoes both from your message list.contextcosts nothing but an embedding: call it every turn.checkis one small model call.
Presence: the one thing worth sending
If your app knows who is in the scene (the room, the party, the group chat), send present with every event (in turn/sync messages too). Who-knows-what is then exact and learning is cheaper. If you don't, Kiori works it out scene by scene; place: true has the newest events placed right away, and characters: "scene" gets the minds of whoever is in the current scene without naming them.
SDKs and other languages
- JavaScript / TypeScript:
sdk/kiori.mjs, types insdk/kiori.d.ts.new Kiori({ apiKey, baseUrl, timeout }). - Python:
sdk/kiori.py, no dependencies.Kiori(api_key, base_url, timeout); the same calls in snake_case (stories.sync,characters.create(external_id=..., character_card=...)). - Anything else: plain HTTP (the curl above), or generate a client from
/openapi.json; import it into Postman to try every call.
Errors are KioriError with status (the HTTP status) and code. When Kiori can't be reached, status is 0 and code is NETWORK (or TIMEOUT): catch it and let your chat carry on without memory for that turn. characters.create, stories.learn and stories.rebuild wait by polling and throw (BUILD_FAILED, LEARN_FAILED, CREDITS) when the work failed.
Tips
- Keep your own recent history in the prompt as you do today: Kiori is the long-term memory and the personality, not the context window.
- Learning happens in the background every 20 events (the newest 4 wait, since they're the ones still being rerolled); call
learnat the end of a session if you want it all right away. - Put the world's canon in the project's lore (
PATCH /v1/project); every character you build afterwards is built inside it. - Call Kiori from your backend, never from the browser: the project key is a secret.