# TypeScript (/docs/sdks/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](../api/gateway/index.mdx) has exactly one SDK method — a parity test in the package fails when the two drift.

```bash
pnpm add codai-sdk
```

## Configure [#configure]

```ts
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
});
```

<TypeTable
  type={{
    apiKey: { type: 'string', description: 'Your codai_… key. Throws synchronously if empty.', required: true },
    baseUrl: { type: 'string', description: 'Gateway origin, no trailing slash.', default: "'https://ai.codai.ro'" },
    sessionId: { type: 'string', description: 'Sent as X-Codai-Session-Id on every call. Per-call override available.' },
    device: { type: 'string', description: 'Device UUID sent as X-Codai-Device — required by sessions.* and hosts.*.' },
    deviceName: { type: 'string', description: 'X-Codai-Device-Name, recorded on first registration.' },
    devicePlatform: { type: "'android' | 'ios' | 'web' | 'desktop' | 'cli' | 'agent'", description: 'X-Codai-Device-Platform.' },
    client: { type: 'string', description: 'X-Codai-Client surface tag used for task attribution.' },
    defaults: { type: 'CodaiRequestExtensions', description: 'Any other X-Codai-* header applied to every request; per-call ext wins.' },
    timeoutMs: { type: 'number', description: 'AbortSignal.timeout applied when you do not pass your own signal. Streams are never time-capped.', default: '120000' },
    maxRetries: { type: 'number', description: 'Retries on 429, 500, 502, 503, 504 and network errors.', default: '2' },
    fetch: { type: 'typeof fetch', description: 'Injectable fetch for tests or custom agents.' },
  }}
/>

## Resource groups [#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`, `ground` | `/v1/tools/*` — [web tools](../agents/tools.mdx#web-tools-one-contract-everywhere) |
| `research` | `create`, `retrieve`, `wait`, `run` | `/v1/research` — [deep research](../api/gateway/research.mdx) |
| `vault` | `account`, `items`, `putItem`, `decide`, `catalog`, `use`, `httpCall`, …; `devices.challenge`, `devices.enroll`, `devices.list`, `devices.rename`, `devices.revoke`; org vaults `members.list`, `members.candidates`, `members.add`, `members.update`, `members.remove`, `rotateKey`, `setQuorum`, `vote`; `simulatePolicy`, `receiptKeys` | `/v1/vault/*` — [vault](../api/gateway/vault.mdx) |
| `cards` | `weather`, `places`, `geocode`, `staticMap`, `fx`, `unfurl` | `/v1/cards/*` — [cards](../agents/cards.mdx) |
| `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](../gateway/headers.mdx).

```ts
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 [#chat]

```ts
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 header
```

`ChatOptions`:

<TypeTable
  type={{
    messages: { type: 'ChatMessage[]', description: 'role: system | user | assistant | tool; content: string or content-part array.', required: true },
    model: { type: 'string', description: 'Any alias or upstream name.', default: "'codai'" },
    temperature: { type: 'number' },
    maxTokens: { type: 'number', description: 'Sent as max_tokens.' },
    tools: { type: 'object[]', description: 'OpenAI function tools. You own the tool loop.' },
    sessionId: { type: 'string', description: 'Per-call override of the client sessionId.' },
    agentMode: { type: 'boolean', description: 'X-Codai-Mode: agent — plan-and-execute (Starter plan or higher).' },
    compact: { type: "'auto'", description: 'X-Codai-Compact: auto — server-side context compaction.' },
    bestOf: { type: '0 | 3', description: 'X-Codai-Best-Of — 3 forces best-of-N, 0 disables.' },
    agentId: { type: 'string', description: 'X-Codai-Agent-Id — attribution for agent fleets.' },
    signal: { type: 'AbortSignal', description: 'Replaces the default timeout signal.' },
    ext: { type: 'CodaiRequestExtensions', description: 'Any other X-Codai-* extension header for this call.' },
  }}
/>

### Tool calls [#tool-calls]

Pass OpenAI function tools and read `res.raw.choices[0].message.tool_calls`. The SDK never executes tools for you.

```ts
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 [#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.

```ts
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 [#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.

```ts
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 | null
```

`task` 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 [#feedback]

Thumbs on a completed request train the routing priors. `rating` is `1` or `-1`; `comment` is optional (≤ 500 chars).

```ts
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](../gateway/headers.mdx#client-identity).

## Embeddings [#embeddings]

```ts
const { embeddings, raw } = await codai.embeddings({
  input: ['hello', 'world'],
  model: 'codai-embed', // default
  dimensions: 512,      // optional
});
embeddings[0]; // number[]
```

## Audio [#audio]

```ts
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 [#ephemeral-tokens]

For browsers and webviews that must never hold your key:

```ts
const { token, expiresAt, scope } = await codai.mintToken('realtime', 600);
// hand `token` to the client; it works only on the realtime surface, for ≤ ttl seconds
```

Scopes: `'realtime' | 'audio' | 'embeddings'`. TTL 60–3 600 s. See [ephemeral tokens](../gateway/embeddings-audio.mdx#ephemeral-tokens).

## Models [#models]

```ts
const ids = await codai.models();        // Array<{ id: string }> — 0.2.x shape
const full = await codai.models.list();  // CoreModel[] with the `codai` capability object
```

Returns the `data` array of `GET /v1/models` — everything your key may address.

## Host apps (`CodaiApps`) [#host-apps-codaiapps]

Apps that resell codai to their own users (e.g. studiai.ro) manage keys through the mgmt API (`https://api.codai.ro`) with an **app connection token** (`codai_app_…`, created at `codai.ro/console/apps/<app>`), not a user key. Since 0.29.0.

```ts
import { CodaiApps } from 'codai-sdk';

const apps = new CodaiApps({ appToken: process.env.CODAI_APP_TOKEN });
const minted = await apps.keys.mint({ email: 'ana@example.com', endUserExternalId: 'uid:gh1' });
await apps.users.setOverride('uid:gh1', { tasksPerDay: 200 });
await apps.subscriptions.sync({ email: 'ana@example.com', event: stripeEvent });
```

Methods: `keys.mint/list/update/revoke/usage/revokeAllFor`, `users.getOverride/setOverride/clearOverride`, `subscriptions.sync`, `validateKey` (separate `validateToken` secret). The `{ ok, data }` envelope is unwrapped; errors are `CodaiError` with the mgmt `code` (`missing_scope:keys.mint`, `no_key_for_external_id`). 20 s timeout, one retry on 429/5xx, `keys.mint` is never retried.

## Errors [#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`.

```ts
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](../gateway/limits-pricing.mdx#error-contract).

## Migrating from the OpenAI SDK [#migrating-from-the-openai-sdk]

The payload is OpenAI-shaped, so the change is the client, not the messages.

```ts
// 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 [#default-export]

`Codai` is also the default export, so `import Codai from 'codai-sdk'` works.
