TypeScript
The codai-sdk package for Node and edge runtimes — configuration, resource groups with full OpenAPI parity, chat, streaming, agents, sessions, errors and migrating from the OpenAI SDK.
codai-sdk is the official TypeScript client. Zero dependencies (platform fetch), fully typed from the gateway OpenAPI, ESM-only, Node 18+ and modern edge runtimes. Since 0.3.0 every operation of the gateway API has exactly one SDK method — a parity test in the package fails when the two drift.
pnpm add codai-sdkConfigure
import { Codai } from 'codai-sdk';
const codai = new Codai({
apiKey: process.env.CODAI_API_KEY!, // required
baseUrl: 'https://ai.codai.ro', // default
sessionId: 'my-project', // optional — enables session memory + sticky routing
device: '<uuid>', // optional — x-codai-device for shared sessions / hosts
client: 'my-app/1.2.0', // optional — x-codai-client surface tag
timeoutMs: 120_000, // default
maxRetries: 2, // retries on 429 / 5xx with exponential backoff
});Prop
Type
Resource groups
Every gateway operation is reachable as a method on the client, grouped by tag. The 0.2.x top-level methods (chat(), chatStream(), embeddings(), models(), feedback(), mintToken(), agents.run(), audio.*) still work; chat, embeddings, models and feedback are callable resource groups, so codai.chat({ messages }) and codai.chat.completions.create({ messages }) are the same call.
| Property | Methods | Gateway |
|---|---|---|
chat.completions | create, stream | POST /v1/chat/completions |
messages | create, stream | POST /v1/messages — Anthropic Messages wire |
responses | create, stream | POST /v1/responses — OpenAI Responses wire |
embeddings | create | POST /v1/embeddings |
audio | transcribe, transcribeDetailed, speech, speechDetailed | /v1/audio/* |
tokens | create | POST /v1/tokens |
models | list | GET /v1/models |
health | get, ready, status | /health, /health/ready, /status |
agents | run; runs.create, runs.get, runs.steps, runs.stats, runs.cancel, runs.stream | /v1/agents/* |
tools | search, fetch | /v1/tools/* |
tasks | list, pending, stats, get, confirm | /v1/tasks/* |
sessions | create, list, get, update, delete, dispatch, stream; events.*, controls.*, lease.*, shares.* | /v1/sessions/* |
devices | list, update, delete, dispatchInbox | /v1/devices/* |
hosts | list, exec, postResult, stream | /v1/hosts/* |
orgs | create, list; members.list, members.add, members.remove | /v1/orgs/* |
account | get, update | /v1/account |
receipt | get | GET /v1/receipt |
feedback | submit | POST /v1/feedback |
phoneModels | list | GET /v1/phone/models |
Every method takes an optional last argument { ext, signal }. ext is a typed CodaiRequestExtensions bag covering all documented X-Codai-* request headers (effort, thinking, thinkingBudget, cache, noTask, taskId, incognito, mode: 'agent', bestOf, compact, shareToken, …) — see headers.
await codai.chat.completions.create(
{ messages, model: 'codai' },
{ ext: { effort: 'high', thinking: true, thinkingBudget: 8192 } },
);
// codai-native SSE streams are async iterators of { event, data } frames
const { id } = await codai.agents.runs.create({ task: 'Summarise the repo README.' });
for await (const ev of codai.agents.runs.stream(id)) {
if (ev.event === 'done') console.log(ev.data.status, ev.data.result);
}
// shared sessions need a device id on the client
const session = await codai.sessions.create({ session_key: 'desktop-main' });
await codai.sessions.lease.acquire(session.id);
for await (const frame of codai.sessions.stream(session.id, { after: 0 })) {
// frame.event ∈ 'event' | 'control' | 'lease' | 'presence'
}Request and response types are generated from the OpenAPI document (src/generated/gateway.ts, pnpm gen) and re-exported both raw (paths, components, operations) and as friendly aliases (ChatCompletionRequest, Task, Session, AccountView, …).
Chat
const res = await codai.chat({
messages: [{ role: 'user', content: 'Explain async iterators in one line.' }],
});
res.content; // assistant text (first choice)
res.toolCalls; // tool calls on the first choice (empty array if none)
res.routedTo; // upstream model that served — from x-codai-routed-to
res.requestId; // from x-codai-trace-id / x-request-id
res.eventId; // from x-codai-event-id — exact target for feedback()
res.usage; // { promptTokens, completionTokens } | null
res.execVerify; // 'passed' | 'refined' | … on codai-labs, else null
res.raw; // the full OpenAI-shaped response
res.headers; // every x-codai-* response headerChatOptions:
Prop
Type
Tool calls
Pass OpenAI function tools and read res.raw.choices[0].message.tool_calls. The SDK never executes tools for you.
const res = await codai.chat({
messages: [{ role: 'user', content: 'Weather in Cluj?' }],
tools: [
{
type: 'function',
function: {
name: 'get_weather',
parameters: { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] },
},
},
],
});
const call = (res.raw.choices as any[])[0].message.tool_calls?.[0];
if (call) {
const result = await getWeather(JSON.parse(call.function.arguments).city);
const followUp = await codai.chat({
messages: [
{ role: 'user', content: 'Weather in Cluj?' },
{ role: 'assistant', content: '', ...(res.raw.choices as any[])[0].message },
{ role: 'tool', tool_call_id: call.id, content: JSON.stringify(result) },
],
});
}Streaming
chatStream() returns an object that is both an async iterable of text deltas and a holder of a .final promise with the metadata you only know at the end.
const stream = codai.chatStream({
messages: [{ role: 'user', content: 'Write a haiku about TypeScript.' }],
});
for await (const delta of stream) {
process.stdout.write(delta);
}
const { content, requestId, usage, routedTo, toolCalls } = await stream.final;
if (requestId) await codai.feedback(requestId, 1);ChatStreamResult:
| Field | Meaning |
|---|---|
content | The concatenated text. |
toolCalls | Tool calls assembled from the piecewise deltas — id, function.name, function.arguments already joined. |
requestId, routedTo, execVerify | Same as on chat(). |
usage | From the gateway's final usage chunk, with cachedTokens when reported. |
finishReason | finish_reason of the last content chunk. |
headers | The response headers (x-codai-cost-estimate: pending on native streams). |
.final rejects if the stream errors; the SDK swallows an unhandled-rejection warning if you never await it. Only data: frames are parsed — keep-alive comments are ignored. stream.chunks() yields the raw chat.completion.chunk objects when you need more than text deltas.
Server-side agent
agents.run() runs a plan-and-execute loop on the gateway. Your process stays thin; planning, tool use and iteration happen server-side and are billed to your key.
const run = await codai.agents.run({
task: 'Summarize the key points of the provided text.',
context: '…your input…', // optional, ≤ 256 000 chars
system: 'Answer in Romanian.', // optional, ≤ 32 000 chars
model: 'codai', // optional
});
run.result; // string
run.model; // upstream that served
run.eventId; // usage row id — pass to feedback()
run.usage; // raw usage object | nulltask is required (≤ 64 000 chars). Agent runs use the same task cap as chat.
Persisted runs live under agents.runs: create() returns 202 with the run id, get() polls it, steps() lists the persisted step trace, stats(days) aggregates your runs, cancel() flips a queued or running run to cancelled and stream() is an async iterator of step / done / timeout events.
Feedback
Thumbs on a completed request train the routing priors. rating is 1 or -1; comment is optional (≤ 500 chars).
const res = await codai.chat({ messages: [{ role: 'user', content: 'hi' }] });
if (res.requestId) await codai.feedback(res.requestId, 1, 'exactly right');The callable form sends requestId as event_id. To grade by session_id or your own client_request_id, use codai.feedback.submit({ session_id, rating }) — see headers.
Embeddings
const { embeddings, raw } = await codai.embeddings({
input: ['hello', 'world'],
model: 'codai-embed', // default
dimensions: 512, // optional
});
embeddings[0]; // number[]Audio
import { readFile, writeFile } from 'node:fs/promises';
// Speech-to-text — multipart upload, max 25 MB. Returns the transcript string.
const text = await codai.audio.transcribe({
file: await readFile('clip.webm'), // Blob | Uint8Array | ArrayBuffer
filename: 'clip.webm', // extension drives format detection
model: 'codai-transcribe', // default
});
// Text-to-speech — returns the audio bytes (mp3 by default).
const audio = await codai.audio.speech({ input: 'Hello from codai.', voice: 'alloy' });
await writeFile('hello.mp3', Buffer.from(audio));transcribe() is the one method that does not retry — a multipart body cannot be replayed safely. It also forwards language, prompt, responseFormat and temperature; transcribeDetailed() / speechDetailed() return the raw body, contentType and headers alongside.
Ephemeral tokens
For browsers and webviews that must never hold your key:
const { token, expiresAt, scope } = await codai.mintToken('realtime', 600);
// hand `token` to the client; it works only on the realtime surface, for ≤ ttl secondsScopes: 'realtime' | 'audio' | 'embeddings'. TTL 60–3 600 s. See ephemeral tokens.
Models
const ids = await codai.models(); // Array<{ id: string }> — 0.2.x shape
const full = await codai.models.list(); // CoreModel[] with the `codai` capability objectReturns the data array of GET /v1/models — everything your key may address.
Errors
Every non-2xx response after retries throws a CodaiError with the HTTP status, the stable gateway code, the requestId (x-codai-trace-id), retryAfter (seconds, on 429s) and the parsed body (the error envelope). Network failures after retries throw with status: 0.
import { Codai, CodaiError } from 'codai-sdk';
try {
await codai.chat({ messages: [{ role: 'user', content: 'hi' }] });
} catch (err) {
if (err instanceof CodaiError) {
console.error(err.status, err.code, err.requestId, err.message);
if (err.code === 'quota_exceeded') {
/* plan task allowance used up — back off for err.retryAfter seconds */
}
}
}The stable code values are listed under limits & pricing.
Migrating from the OpenAI SDK
The payload is OpenAI-shaped, so the change is the client, not the messages.
// before
import OpenAI from 'openai';
const openai = new OpenAI({ apiKey, baseURL: 'https://ai.codai.ro/v1' });
const r = await openai.chat.completions.create({ model: 'codai', messages });
r.choices[0].message.content;
// after
import { Codai } from 'codai-sdk';
const codai = new Codai({ apiKey });
const res = await codai.chat({ messages });
res.content;What you gain: routedTo, requestId and usage as first-class fields, .final on streams with assembled tool calls, one-option access to sessions / agent mode / compaction / best-of, and retries with backoff. What you keep: the openai client still works against https://ai.codai.ro/v1 if you prefer it — both can share the same key and session id.
Default export
Codai is also the default export, so import Codai from 'codai-sdk' works.