✦ Kiori

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})

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." } ]
}'

Your ids and your character cards

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.

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

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