@hanzo/ai
v0.2.0
Published
Headless TypeScript client for the Hanzo AI backend — chat completions, Anthropic messages, models, and portable chat/message threads. No UI, no React.
Readme
@hanzo/ai
Headless TypeScript client for the Hanzo AI backend.
Pure client + types. No UI, no React, no DOM dependencies beyond the standard
fetch / ReadableStream web APIs. This is the shared API layer consumed by
hanzo.chat, hanzo.app, the Hanzo desktop app, and the hanzo-dev CLI. UI lives
in separate packages (@hanzo/chat, @hanzo/agent).
It wraps the real Hanzo AI backend (github.com/hanzoai/ai, served via
api.hanzo.ai):
- OpenAI-compatible chat completions (
/v1/chat/completions), streaming and not. - Anthropic-compatible messages (
/v1/messages), streaming and not. - Models catalog (
/v1/models). - Portable chat threads — the cross-surface conversation store (
chats+ theirmessages), so a conversation started on one surface resumes on another. - Account — the signed-in IAM identity.
Install
npm install @hanzo/aiUsage
Auth is a Hanzo IAM access token (e.g. from @hanzo/iam) sent as
Authorization: Bearer <token>.
import { createAiClient } from "@hanzo/ai";
const ai = createAiClient({
// baseUrl defaults to https://api.hanzo.ai
token: process.env.HANZO_TOKEN!,
// or, for rotating tokens:
// getToken: async () => session.accessToken,
});Chat completions (OpenAI-compatible)
const res = await ai.chat.completions.create({
model: "zen-1",
messages: [{ role: "user", content: "Explain MoE routing in one sentence." }],
});
console.log(res.choices[0].message.content);Streaming:
const stream = await ai.chat.completions.create({
model: "zen-1",
messages: [{ role: "user", content: "Write a haiku about Go." }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta.content ?? "");
}Tool calling works by passing tools / tool_choice (forwarded to the provider).
Messages (Anthropic-compatible)
const msg = await ai.messages.create({
model: "claude-sonnet",
max_tokens: 512,
system: "You are concise.",
messages: [{ role: "user", content: "Summarize the CAP theorem." }],
});
// Streaming yields Anthropic SSE events:
const events = await ai.messages.create({ /* ...,*/ stream: true } as any);
for await (const ev of events) {
if (ev.type === "content_block_delta") { /* ev.delta.text */ }
}Models
const models = await ai.models.list(); // Model[]Portable chat threads
A Chat is a durable thread keyed by "owner/name"; its turns are Messages.
This is the store that lets a conversation move between surfaces.
await ai.chats.create({ owner: "hanzo", name: "thread-1", user: "alice", type: "AI" });
await ai.chats.messages.append({
owner: "hanzo",
name: "msg-1",
chat: "thread-1",
author: "alice",
text: "Hello",
});
const thread = await ai.chats.get("hanzo/thread-1");
const turns = await ai.chats.messages.list({ chat: "thread-1" });Account
const me = await ai.account.get();Errors
All failures throw a subclass of HanzoAIError:
APIError— non-2xx HTTP, or a CRUD envelope withstatus: "error". Carries.statusand.body.AuthError— no token was available for an authenticated request.
import { APIError } from "@hanzo/ai";
try {
await ai.models.list();
} catch (err) {
if (err instanceof APIError) console.error(err.status, err.message);
}Advanced
createAiClient accepts:
| option | type | default |
| ---------- | -------------------------------------- | ------------------------ |
| baseUrl | string | https://api.hanzo.ai |
| token | string | — |
| getToken | () => string \| Promise<string> | — |
| fetch | typeof fetch | globalThis.fetch |
| headers | Record<string, string> | {} |
The low-level transport (client.http) and the SSE helpers (parseSSE,
streamChatCompletion, streamMessage) are exported for custom calls.
License
Apache-2.0
