A minimal, multi-provider chat built on the Khwan TypeScript client —
@khwan/client.
Khwan is a pure AI-memory layer — it never runs a model. This sample shows how to wire Khwan's memory around your own model: Khwan prepares the context, you call your model to generate the reply, then you hand the reply back so Khwan can persist and learn from it.
Everything is configured through a .env file — there is no settings popup
and no login. Clone it, fill in .env.local, pick your provider, run.
prepare → your model → record
const turn = await client.prepare(input)— Khwan builds the context (memory + constitution + coherence). No LLM call happens here.- If
turn.allowed === false, the coherence gate blocked the turn — showturn.reasonand stop. - Call your own model with
turn.messages. await client.record(turn, answer)— Khwan persists the exchange and learns.- Render the answer, plus
turn.coherenceand how manyturn.sourcesKhwan used, so the "memory" is visible.
In this sample the whole loop runs server-side, inside the Next.js route
app/api/chat/route.ts. That's what keeps your API
keys in .env and out of the browser: the browser only ever sends the user's
message and receives the answer.
The route streams each step back as newline-delimited JSON, so the UI
animates the loop live — you watch prepare (Khwan, no LLM) → your model
→ record (Khwan learns) light up in sequence on every turn.
Set MODEL_PROVIDER to one of three families. The provider adapter translates
Khwan's prepared messages into each API's shape.
MODEL_PROVIDER |
Vendor | Example MODEL_NAME |
Default endpoint |
|---|---|---|---|
openai |
OpenAI | gpt-4o-mini |
https://api.openai.com/v1 |
anthropic |
Anthropic Claude | claude-3-5-haiku-latest |
https://api.anthropic.com |
google |
Google Gemini | gemini-1.5-flash |
https://generativelanguage.googleapis.com |
Anything OpenAI-compatible works through MODEL_PROVIDER=openai plus a
MODEL_BASE_URL — Groq, OpenRouter, DeepSeek, Together, Mistral, xAI (Grok),
Ollama, LM Studio, vLLM, and more. The full cheat-sheet of base URLs and model
names is in .env.example.
Adding a fourth provider is one file: implement the Provider interface in
lib/providers/ and register it in
lib/providers/index.ts.
- Node.js 18+
- A Khwan API key (from the Khwan dashboard — starts with
kwk_) - An API key for whichever model provider you choose
npm install
cp .env.example .env.local # then edit .env.local
npm run devOpen http://localhost:3000. If anything required is missing, the app tells you exactly which variables to set.
All configuration is environment variables — copy .env.example and fill it in.
Values are read only on the server; none are exposed to the browser.
Khwan · memory layer
| Variable | Required | Notes |
|---|---|---|
KHWAN_API_KEY |
✅ | Your Khwan key (kwk_...). |
KHWAN_BASE_URL |
— | Defaults to https://api.khwan.ai. |
KHWAN_USER |
— | Default end-user id → an isolated sub-brain (paid). Blank = one shared brain. Editable per-session from the chat header (see below). |
KHWAN_CORE |
— | Isolated core slug. Blank = the account's default core. |
Your model · generation
| Variable | Required | Notes |
|---|---|---|
MODEL_PROVIDER |
✅ | openai | anthropic | google. |
MODEL_API_KEY |
✅ | The provider's key. |
MODEL_NAME |
✅ | Model name, e.g. gpt-4o-mini. |
MODEL_BASE_URL |
— | Override the endpoint (e.g. to hit an OpenAI-compatible host). |
MODEL_MAX_TOKENS |
— | Cap on generated tokens. Defaults to 1024. |
Restart npm run dev after editing .env.local.
The chat header has a User field. Each distinct value is an isolated
sub-brain — Khwan remembers each user separately. Type alice, tell it your
name, then switch to bob: it won't know you. Switch back to alice and the
memory is still there. Leave the field blank to chat against one shared
brain (the default).
Under the hood the browser sends userId with each message; the server passes
it to Khwan as the end-user (X-Khwan-User). Changing the User clears the chat
so the brain switch is obvious.
The free plan includes a few per-user sub-brains so you can try this out; paid plans lift the cap. When you exceed the plan's limit, adding a new User returns a 402 (the app shows a clear message) — reuse an existing User or upgrade. Blank (shared brain) works on every plan.
The header has a Khwan ON/OFF toggle. ON runs the memory loop. OFF is the honest baseline: a raw, stateless model call — no memory, no history — so it forgets between turns. Tell it your name with Khwan ON, ask for it again with Khwan OFF (it won't know), then turn Khwan back ON (it remembers). That's the whole point of the layer, in three messages.
import { Khwan } from "@khwan/client";
import { generate } from "@/lib/providers"; // provider adapter (openai | anthropic | google)
const client = new Khwan({
apiKey: process.env.KHWAN_API_KEY!,
baseUrl: process.env.KHWAN_BASE_URL,
userId: process.env.KHWAN_USER || undefined,
core: process.env.KHWAN_CORE || undefined,
});
// 1. Khwan builds the context — no model runs here.
const turn = await client.prepare("hello");
// 2. Coherence gate.
if (!turn.allowed) throw new Error(turn.reason ?? "blocked");
// 3. Call YOUR model with the prepared messages, via the configured provider.
const answer = await generate(modelConfig, turn.messages);
// 4. Hand the answer back so Khwan persists + learns.
await client.record(turn, answer);See app/api/chat/route.ts,
lib/config.ts, and lib/providers/ for the
full wiring.
- API keys live in
.envand are read only in server code (lib/config.tsimportsserver-only). They are never bundled into the client. - The browser talks only to this app's
/api/chatroute — never directly to Khwan or your model provider. - Your model key is never sent to Khwan; Khwan never sees your model.
This sample depends on the published client from npm:
"@khwan/client": "^0.1.0"npm install pulls it for you — nothing else to wire up.
MIT — see LICENSE.
Next.js (App Router, Node runtime) + React + Tailwind CSS + TypeScript.