Skip to Content
Bring Your Own Model

Bring Your Own Model

Khwan is a cognition layer, not a model. In the BYOM flow you call the LLM; Khwan only prepares context and records the result. This page is the deep dive on that loop.

The contract

turn = kw.prepare(user_input) # Khwan → you: a ready-to-send brief answer = my_own_llm(turn.messages) # you → your model → you: the reply kw.record(turn, answer) # you → Khwan: persist + learn

Two things cross the boundary:

  • Out of Khwan: a Turn — most importantly turn.messages (the brief) and turn.turn_token (an opaque handle that ties record back to this prepare).
  • Back into Khwan: the plain-text answer your model produced.

Khwan never sees your model provider key and never makes the generation call — there is no hosted model path; you always call your own model.

prepare() — what you get

kw.prepare(input) returns a Turn. It does not call an LLM; it assembles context on the server and hands it back.

FieldTypeMeaning
turn.messageslist[{role, content}]The brief to feed your model.
turn.coherencefloat | NoneCoherence signal for this turn.
turn.sourceslistMemory / retrieval sources used.
turn.allowedboolFalse if Khwan gated the turn.
turn.reasonstr | NoneWhy it was gated (when allowed is False).
turn.turn_tokenstr | NoneOpaque handle; pass to record().
turn.raw()dictThe full server payload.

Always check turn.allowed before calling your model. When it’s False, skip generation and surface turn.reason — the turn conflicts with the core’s constitution or memory.

What’s inside turn.messages

A standard chat array with Khwan’s value baked into the system message:

[ { "role": "system", "content": "<constitution> + <retrieved memory> + <learned lessons> + <coherence>" }, {"role": "user", "content": "remember I prefer short answers in Thai"} ]

Don’t parse it — just pass it through. The constitution (identity + rules), retrieved memory for this core, distilled lessons, and coherence framing are already composed into it.

record() — closing the loop

kw.record(turn, answer)

Pass back the same Turn and your model’s answer as a string. Khwan uses turn.turn_token to match it to the original prepare, then persists the exchange and updates what it has learned. Skip record and Khwan learns nothing from that turn.

Writing my_own_llm

my_own_llm is just your normal LLM call: it takes turn.messages and returns a string. Drop-in versions for common providers follow.

Anthropic (Claude)

Anthropic takes the system prompt as a separate argument, so split it out:

import anthropic client = anthropic.Anthropic(api_key="sk-ant-...") # your key, Khwan never sees it def my_own_llm(messages): system = next((m["content"] for m in messages if m["role"] == "system"), "") chat = [m for m in messages if m["role"] != "system"] r = client.messages.create( model="claude-sonnet-4-6", max_tokens=1024, system=system, messages=chat, ) return r.content[0].text

OpenAI

OpenAI accepts the system message inline, so pass messages straight through:

from openai import OpenAI client = OpenAI(api_key="sk-...") # your key def my_own_llm(messages): r = client.chat.completions.create( model="gpt-4o", messages=messages, # system + user, as-is ) return r.choices[0].message.content

Local Ollama

Fully local generation — the input and the answer never leave your machine except for the Khwan memory calls:

import ollama # pip install ollama, with a local Ollama server running def my_own_llm(messages): r = ollama.chat( model="llama3.1", messages=messages, # Ollama uses the same {role, content} shape ) return r["message"]["content"]

Anything else

Any callable that maps list[{role, content}] -> str works. Streaming, tool use, and multi-model routing are all fine — Khwan only cares about the final answer string you hand to record().

Isolate a core

The BYOM loop above runs against one brain. To run several fully separate brains — each with its own memory, identity, and learning — pass core per client:

test = Khwan(api_key="kwk_live_xxx", user_id="alice", core="test") client1 = Khwan(api_key="kwk_live_xxx", user_id="alice", core="client1") # test and client1 share no memory or lessons.

core is sent as the X-Khwan-Core header. Omit it for the account’s default core. user_id still identifies the end user on each request — it does not create a separate brain. Quota is pooled at the account level. See Isolated cores and the cores API.

Last updated on