Skip to content
kitn AI/UI

Connect any backend

Every integration in this section — a raw provider, a gateway, an agent framework, a harness — is the same job underneath: turn your backend’s output into AI/UI’s messages and stream it in. Learn the contract once; everything else is wiring.

<kai-chat> renders a messages array. You own the array; the component reflects it. A message is plain data — spelled out below so you can see the whole contract, but don’t retype it: the kit exports these as ChatMessage, MessagePart, and ToolPart from @kitn.ai/ui (types only, so they cost nothing at runtime).

import type { ChatMessage, MessagePart, ToolPart } from '@kitn.ai/ui';
const message: ChatMessage = {
id: 'm_1',
role: 'assistant', // 'user' | 'assistant'
parts: [], // ordered, rendered in the order they appear
};
const search: ToolPart = {
type: 'web_search', // the tool name
state: 'output-available', // 'input-streaming' | 'input-available' | 'output-available' | 'output-error'
input: { query: 'kitn ui' }, // arguments
output: { results: [] }, // result, once it returns
toolCallId: 'call_1',
};
// The full union has six variants. Streaming from a model or agent only ever
// produces the first three below; card / source / file are for generative-UI
// cards, citations, and attachments, covered in their own guides. This array is
// checked against the kit's own MessagePart, so it cannot drift from the contract.
const parts: MessagePart[] = [
{ type: 'text', text: 'the text, streamed in token by token' },
{ type: 'reasoning', text: 'the model thinking out loud', label: 'Reasoning' },
{ type: 'tool', tool: search },
{ type: 'card', envelope: { type: 'confirm', id: 'c_1', data: {} } },
{ type: 'source', source: { url: 'https://example.com', title: 'Example' } },
{ type: 'file', attachment: { id: 'a_1', type: 'file', filename: 'notes.md' } },
];

Three things stream into a message, and they map from any model:

  • Text → append to a { type: 'text' } part’s text.
  • Reasoning → set a { type: 'reasoning' } part’s text; it renders in a collapsible reasoning panel.
  • Tool calls → push a { type: 'tool' } part and advance its tool.state as the arguments arrive (input-streaminginput-available) and the result returns (output-available, or output-error).

Set messages in JavaScript — arrays can’t be HTML attributes — and assign a new array on each update. That reassignment is what drives the re-render.

Your model key never touches the browser. Put a thin route on your own server — call it /api/chat — that authenticates the user, calls the provider, and streams the result back:

browser ──fetch /api/chat──▶ your server ──▶ provider · gateway · harness
▲ │
└────────── stream ───────────────┘
(read tokens into messages)

The browser reads that stream and mutates messages — the Streaming recipe is the exact reader loop. Your server route is the only piece that changes per backend, and that’s what the rest of this section covers, one provider at a time.

<kai-chat> is a web component — it hydrates and runs in the browser. That holds in a plain HTML page, a React app, or a Next.js route (where it’s a client island). The component is the front end; your /api/chat route is the back end. Server rendering emits the tag; the chat comes alive on hydration.

  • Harnesses — wire AI/UI to Mastra, Pi, OpenClaw, or your own agent loop.
  • Streaming — the reader loop that drives messages.
  • kai-chat reference — every prop and event the contract exposes.