npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cana-ai/sdk

v1.0.1

Published

OpenAI-compatible client + agent for Cana Apps — Bearer auth (App API key or short-lived browser JWT with auto-refresh), server-hosted MCP federation, streaming, typed structured output. Node 18+, Bun, Deno, Workers, browsers.

Downloads

1,265

Readme

@cana-ai/sdk

OpenAI-compatible client + prebuilt agent for Cana Apps — Bearer auth (App API key or short-lived browser JWT with auto-refresh), server-hosted MCP federation, streaming, typed structured output, and a hybrid local-tool loop. Web Crypto only — runs on Node 18+, Bun, Deno, Cloudflare Workers, and modern browsers out of the box.

npm install @cana-ai/sdk
# or pnpm add / yarn add / bun add
<!-- or drop the IIFE bundle into a page -->
<script src="https://cana.build/sdk/openai.js"></script>

Table of contents


Quick start (prebuilt agent)

Plug in a prompt + tools. The SDK handles auth, MCP discovery, tool dispatch, the chat loop, streaming, and error capture.

import { CanaAgent, defineTool } from "@cana-ai/sdk/openai";
import { z } from "zod";

const get_weather = defineTool({
  description: "Current weather for a city",
  parameters:  z.object({
    city:  z.string(),
    units: z.enum(["c", "f"]).optional(),
  }),
  handler: async ({ city, units }) => {
    //                ^^^^^^^^^^^^
    //                typed string + "c"|"f"|undefined via z.infer
    return await fetchWeather(city, units);
  },
});

const agent = new CanaAgent({
  appId:        "app_…",
  apiKey:       process.env.CANA_APP_KEY!,   // cak_live_…
  // or for the browser: getToken: () => fetchFreshJwtFromYourBackend()
  model:        "OpenAI/gpt-4o-mini",
  systemPrompt: "You are helpful.",
  tools:        { get_weather },     // local — handler runs in your process
  mcp:          ["github"],          // server-hosted — auto-dispatched
});

const r = await agent.run({ prompt: "What's the weather in Tokyo?" });
console.log(r.text);          // "It's 22°C and sunny in Tokyo."
console.log(r.toolCalls);     // [{ name: "get_weather", args: { city: "Tokyo" }, result: { … }, source: "local" }]
console.log(r.usage);         // { prompt_tokens, completion_tokens, total_tokens }

That's it — no chat loop to write, no messages array to manage, no tool_calls to dispatch yourself.

For full control (driving rounds yourself, custom tool dispatch logic, raw streaming chunks), drop down to the CanaClient documented below.

Authentication

⚠ Breaking change in v1.0.0. CanaClient and CanaAgent no longer use HMAC signing, and the signingKey constructor option has been removed. Callers on signingKey must switch to apiKey (or getToken). The CanaIssuer flow is unchanged.

CanaAgent and CanaClient authenticate with a Bearer credential. Pass exactly one of three options:

  • apiKey: "cak_live_…" — a static App API key for server-to-server use. Sent as Authorization: Bearer cak_live_…. Create and revoke it from the App's OpenAI API dashboard tab; it's hash-stored and shown only once at creation, so copy it then. Never ship it in browser code.

  • getToken: () => Promise<string> | string — for direct-browser use. Returns a short-lived embedder JWT. The client caches it, refreshes ~5s before the JWT's exp, and on a 401 re-calls getToken once and retries. This is the safe browser path — no long-lived secret ever reaches page source.

  • bearerToken: "<jwt>" — a static JWT you manage yourself (a degenerate getToken: not refreshed; a 401 propagates).

// server-to-server
new CanaAgent({ appId, apiKey: process.env.CANA_APP_KEY! });

// browser — short-lived JWT, auto-refreshed on exp/401
new CanaAgent({ appId, getToken: () => fetch("/my-backend/cana-jwt").then(r => r.text()) });

// self-managed static JWT
new CanaAgent({ appId, bearerToken: myJwt });

The JWTs returned by getToken / passed as bearerToken are minted by your server with CanaIssuer. Use apiKey for server-to-server, getToken for the browser, and bearerToken only when you already have a JWT and want to manage its lifecycle yourself.

Model strings

Models are written as <providerConfigName>/<modelId>. providerConfigName is the exact name of an LlmProviderConfig the App owner has access to (case-insensitive; whitespace-stripped at create time). The proxy:

  1. finds an active config of that name (ORG > PLATFORM scope; newer wins on ties),
  2. confirms it hosts the requested modelId,
  3. forwards your request with the config's baseUrl + decrypted apiKey.
// Examples — substitute YOUR provider config names
"Kimi/moonshot-v1-32k"
"OpenAI/gpt-4o-mini"
"Anthropic/claude-haiku-4-5-20251001"
"Vertex/gemini-2.5-pro"

// No slash → fallback: any accessible config hosting that model id wins.
"gpt-4o-mini"

Two distinct errors disambiguate misses: unknown_provider (no config of that name) vs model_not_found (config exists but doesn't host the model).


Prebuilt agent: CanaAgent

agent.run — one-shot

const r = await agent.run({
  prompt: "...",
  // or full history:
  // messages: [...],
  // overrides per-call:
  // mcp: ["slack"], maxRounds: 5, excludeTools: ["risky_tool"],
});

r.text;       // string | null  — final assistant text (null if ended on a tool round)
r.toolCalls;  // ToolCallRecord[] — every tool call dispatched, in order:
              //   { name, args, result, isError, source: "local" | "mcp" }
r.messages;   // ChatMessage[] — full conversation (system + user + assistant + tool)
r.rounds;     // number — model rounds executed
r.usage;      // { prompt_tokens, completion_tokens, total_tokens } — aggregated

maxRounds (default 10) caps the loop so a misbehaving model can't burn tokens forever. When hit, the run returns with whatever's accumulated; no exception.

defineTool — Zod + JSON Schema

Two overloads. Pick whichever fits your project.

Zod path (recommended) — handler args are typed via z.infer:

import { z } from "zod";

const send_email = defineTool({
  description: "Send a transactional email.",
  parameters: z.object({
    to:      z.string().email(),
    subject: z.string().min(1).max(100),
    body:    z.string(),
    cc:      z.array(z.string().email()).optional(),
  }),
  handler: async ({ to, subject, body, cc }) => {
    //                ^^^^^^^^^^^^^^^^^^^^^^
    //                all typed; Zod .parse() runs first
    return await emailService.send({ to, subject, body, cc });
  },
});

The schema's .parse() runs on the model's args before your handler sees them — invalid objects throw early. The schema is also converted to JSON Schema (via the optional peer dep zod-to-json-schema, lazy-imported, WeakMap-cached) and exposed to the model.

JSON Schema path — for callers without Zod:

const send_email = defineTool({
  description: "Send a transactional email.",
  parameters: {
    type: "object",
    required: ["to", "subject", "body"],
    properties: {
      to:      { type: "string", format: "email" },
      subject: { type: "string", maxLength: 100 },
      body:    { type: "string" },
      cc:      { type: "array", items: { type: "string", format: "email" } },
    },
  },
  handler: async (args) => {
    const { to, subject, body, cc } = args as { to: string; subject: string; body: string; cc?: string[] };
    return await emailService.send({ to, subject, body, cc });
  },
});

Hybrid local + MCP tools

Local tools run in your process; MCP tools run on the server. The agent auto-routes either way based on the tool name (MCP tools are prefixed mcp__<connector>__<tool>).

import { CanaAgent, defineTool } from "@cana-ai/sdk/openai";
import { z } from "zod";

const fill_form = defineTool({
  description: "Fill any subset of onboarding-form fields.",
  parameters: z.object({
    companyName: z.string().optional(),
    teamSize:    z.number().int().min(1).optional(),
    priority:    z.enum(["low", "medium", "high", "urgent"]).optional(),
  }),
  handler: async (patch) => {
    formStore.update(patch);                    // mutate your UI state
    return { ok: true, form: formStore.snapshot() };
  },
});

const agent = new CanaAgent({
  appId, apiKey,
  model:        "Anthropic/claude-haiku-4-5-20251001",
  systemPrompt: "You're an onboarding assistant.",
  tools:        { fill_form },                  // local
  mcp:          ["slack"],                      // server-hosted
});

const r = await agent.run({
  prompt: "Onboard Acme (12 ppl, high priority), then post to #onboarding.",
});

// r.toolCalls includes BOTH:
//   { name: "fill_form",                       source: "local", result: { ok: true, form: {…} } }
//   { name: "mcp__slack__send_message",        source: "mcp",   result: { … } }

agent.stream — streamed events

Same arguments as run, returns an AsyncIterable<AgentEvent>.

for await (const ev of agent.stream({ prompt: "Tokyo weather?" })) {
  switch (ev.type) {
    case "round_start":  console.log(`[round ${ev.round}]`); break;
    case "text_delta":   process.stdout.write(ev.delta); break;            // token-by-token
    case "tool_call":    console.log(`→ ${ev.source}:${ev.name}(${JSON.stringify(ev.args)})`); break;
    case "tool_result":  console.log(`✓ ${ev.name} ${ev.isError ? "FAILED" : "ok"}`); break;
    case "done":         console.log("\nfinal:", ev.text, "usage:", ev.usage); break;
    case "error":        console.error("error:", ev.code, ev.message); break;
  }
}

Event shapes:

type AgentEvent =
  | { type: "round_start"; round: number }
  | { type: "text_delta";  delta: string; round: number }
  | { type: "tool_call";   name: string; args: unknown; source: "local" | "mcp"; round: number }
  | { type: "tool_result"; name: string; result: unknown; isError: boolean; source: "local" | "mcp"; round: number }
  | { type: "done";        text: string | null; toolCalls: ToolCallRecord[]; messages: ChatMessage[]; usage: …  }
  | { type: "error";       code: string; message: string; status?: number };

text_delta events stream the model's text token-by-token (using the SDK's chunk aggregator). Tool-call deltas split across multiple OpenAI chunks are reassembled before they're dispatched, so you get one tool_call event per actual call, not one per partial chunk.

AgentSession — multi-turn memory

const s = agent.session();
const r1 = await s.send("My favorite city is Reykjavik.");
const r2 = await s.send("What's my favorite city?");
console.log(r2.text);                       // "Reykjavik."

s.messages;                                 // ChatMessage[] — read-only
s.clear();                                  // reset to empty

// streaming variant:
for await (const ev of s.stream("Tell me a story.")) { /* ... */ }

Each send carries the full prior history forward; the session stores whatever the agent returns (system + user + assistant + tool messages together).

Handler errors are captured, not thrown

If your tool handler throws, the agent records the error as the tool result and continues the loop. The model sees a JSON error blob and can either retry, switch tools, or apologise to the user.

const broken = defineTool({
  description: "Always fails — for testing.",
  parameters: z.object({ x: z.string() }),
  handler: async () => { throw new Error("kaboom"); },
});

const r = await agent.run({ prompt: "Call broken with x='hi'." });
r.toolCalls[0];
//   {
//     name:    "broken",
//     args:    { x: "hi" },
//     result:  { error: "kaboom" },
//     isError: true,
//     source:  "local",
//   }

Low-level client: CanaClient

When you want to drive the loop yourself, or only need the wire-level helpers (auth, streaming, MCP RPC).

import { CanaClient } from "@cana-ai/sdk/openai";

const client = new CanaClient({
  appId:      "app_…",
  apiKey:     process.env.CANA_APP_KEY!,     // cak_live_… (or getToken / bearerToken)
  apiBase:    "https://cana.build",          // optional, this is the default
});

Chat completions

const c = await client.chat.completions.create({
  model:       "OpenAI/gpt-4o-mini",
  messages:    [{ role: "user", content: "Summarize TCP slow-start in 3 bullets." }],
  temperature: 0.2,
  max_tokens:  300,
});

console.log(c.choices[0].message.content);
console.log("usage:", c.usage);

Request fields supported: model, messages, tools, tool_choice, response_format, stream, max_tokens, temperature, top_p, stop.

Streaming

const stream = await client.chat.completions.create({
  model:    "OpenAI/gpt-4o-mini",
  messages: [{ role: "user", content: "Write a haiku about TCP." }],
  stream:   true,
});

for await (const chunk of stream) {
  const delta = chunk.choices[0]?.delta;
  if (delta?.content) process.stdout.write(delta.content);
  if (chunk.choices[0]?.finish_reason) console.log("\n[done]", chunk.usage);
}

TypeScript narrows the return shape on stream:

// stream: true  → AsyncIterable<ChatCompletionChunk>
// stream: false → ChatCompletion

Function tools — manual dispatch loop

OpenAI tool format works as-is. The low-level client doesn't dispatch client tools for you — you do it. (Or use CanaAgent above and skip this.)

const tools = [{
  type: "function",
  function: {
    name: "lookup_weather",
    description: "Current temperature in °C for a city.",
    parameters: { type: "object", required: ["city"], properties: { city: { type: "string" } } },
  },
}];

let messages = [{ role: "user", content: "What's it like in Tokyo?" }];
for (let i = 0; i < 5; i++) {
  const c = await client.chat.completions.create({ model: "OpenAI/gpt-4o-mini", messages, tools });
  const msg = c.choices[0].message;
  messages.push(msg);
  if (!msg.tool_calls?.length) break;
  for (const call of msg.tool_calls) {
    const args = JSON.parse(call.function.arguments);
    const result = await lookupWeather(args.city);
    messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(result) });
  }
}

MCP federation — runWithMcp auto-dispatch

const r = await client.runWithMcp({
  model:    "Anthropic/claude-haiku-4-5-20251001",
  messages: [{ role: "user", content: "Find last week's PRs in cana-web." }],
  mcp:      ["github", "search"],
  maxRounds: 5,
});
console.log(r.message.content);
console.log(r.rounds);

runWithMcp discovers + dispatches mcp__* calls server-side, and EXITS the loop when the model calls a non-MCP tool (so the caller can dispatch local tools, append the result, and call again). For an integrated local+MCP loop, prefer CanaAgent.

MCP federation — manual

const { tools, dropped } = await client.mcp.tools(["github"]);
//   tools:   OpenAI-format ChatTool[] you pass to chat.completions.create
//   dropped: connector names the App owner doesn't have enabled

const c = await client.chat.completions.create({ model, messages, tools });

for (const call of c.choices[0].message.tool_calls ?? []) {
  if (!call.function.name.startsWith("mcp__")) continue;
  const out = await client.mcp.execute({
    name:      call.function.name,
    arguments: JSON.parse(call.function.arguments),
  });
  // out.result / out.isError / out.durationMs
}

Structured output (JSON Schema)

const c = await client.chat.completions.create({
  model:    "OpenAI/gpt-4o-mini",
  messages: [{ role: "user", content: "Give me one US state with capital + population." }],
  response_format: {
    type: "json_schema",
    json_schema: {
      name:   "USState",
      strict: true,
      schema: {
        type: "object",
        required: ["name", "capital", "population"],
        properties: {
          name:       { type: "string" },
          capital:    { type: "string" },
          population: { type: "integer", minimum: 0 },
        },
      },
    },
  },
});

const parsed = JSON.parse(c.choices[0].message.content!);

Structured output (Zod helper)

import { z } from "zod";

const Person = z.object({
  name:       z.string(),
  age:        z.number().int(),
  occupation: z.string(),
});

const { result, raw } = await client.chat.completions.generate({
  model:    "OpenAI/gpt-4o-mini",
  messages: [{ role: "user", content: "Anna is a 34-year-old marine biologist." }],
  schema:   Person,
});

result.name;        // "Anna"
result.age;         // 34
raw.usage;          // full ChatCompletion if you need it

Optional peer dep: zod-to-json-schema (auto-installed in Node; bundle it yourself in browser builds).

System prompts & multi-turn

For CanaAgent use systemPrompt + the built-in session(). For CanaClient, just include {role: "system", …} messages and keep appending:

const messages = [
  { role: "system", content: "You are a terse senior backend engineer." },
  { role: "user",   content: "When is a UNIQUE index worse than a CHECK constraint?" },
];

const r1 = await client.chat.completions.create({ model, messages });
messages.push(r1.choices[0].message);
messages.push({ role: "user", content: "Give a concrete example." });
const r2 = await client.chat.completions.create({ model, messages });

Usage / cost tracking

Every non-streaming response includes usage. Streaming responses include it on the final chunk (OpenAI / Moonshot / Kimi / DeepSeek do). CanaAgent.run aggregates across all rounds for you.

const c = await client.chat.completions.create({ model, messages });
const { prompt_tokens, completion_tokens, total_tokens } = c.usage!;

Custom fetch (testing, proxies, telemetry)

Override the network layer for unit tests, edge-network proxies, request mirroring, or tracing. Works on both CanaAgent and CanaClient.

const agent = new CanaAgent({
  appId, apiKey, model: "OpenAI/gpt-4o-mini",
  fetch: async (url, init) => {
    console.log("→", init?.method, url);
    const t0 = Date.now();
    const res = await globalThis.fetch(url, init);
    console.log("←", res.status, `${Date.now() - t0}ms`);
    return res;
  },
});

Hosted page / embed: CanaIssuer

If your product hosts a Cana embed widget or sends users to the hosted page (cana.build/a/<slug>), you typically want visitors to land already authenticated as themselves, not as anonymous traffic.

CanaIssuer is the server-side helper for that flow:

import { CanaIssuer } from "@cana-ai/sdk/openai";

const issuer = new CanaIssuer({
  appId:      "app_…",
  signingKey: process.env.CANA_APP_ISSUER_KEY!,   // csk_live_… — issuer-purpose key
  kid:        process.env.CANA_APP_ISSUER_KID,    // optional; defaults to appId
  apiBase:    "https://cana.build",
});

// Inside your issuer-URL endpoint (Next.js route handler, Express, …):
const { code } = await issuer.issueExchangeCode({
  user: { sub: user.id, email: user.email, name: user.name },
  // ttlSec: 30,        // JWT lifetime (clamped to 1–300; default 30)
  // codeTtlSec: 60,    // exchange-code TTL on Cana's side (default 30)
});

return Response.redirect(issuer.buildReturnUrl(returnTo, code));
//      ^ equivalent to `${returnTo}?exchange=<code>`

Key separation. Use a dedicated "issuer"-purpose AppSigningKey (csk_live_…), separate from the Bearer App API key (cak_live_…) the SDK uses for CanaClient / CanaAgent auth. Cana's verifier filters by purpose, so rotating one credential doesn't break the other. Mint the issuer key from your App dashboard or via the cana-web admin path.

Just the JWT

const jwt = await issuer.signJwt({
  user: { sub: "user_123", email: "[email protected]", name: "Alice" },
  ttlSec: 60,
});

The JWT is HS256 with header {alg, typ, kid} and claims {sub, email?, name?, metadata?, iss, aud, iat, exp, jti} — matches what Cana's verifyAppToken accepts.

End-to-end visitor flow

1. User opens             https://cana.build/a/<slug>
2. Cana 302s →            https://your.app/issuer?return_to=<encoded>
3. Your endpoint:
     - checks your session cookie
     - (optional) issuer.issueExchangeCode({ user })
     - 302 to issuer.buildReturnUrl(returnTo, code)
4. Cana page redeems the code via /exchange/redeem,
   drops a session cookie, page is authenticated.

Direct API key vs issuer flow at a glance

| | Direct API key (CanaClient / CanaAgent) | Issuer flow (CanaIssuer) | | --- | --- | --- | | Triggered by | Your server code | A user visiting a Cana URL | | Identity | Anonymous (App-level) | End user (sub claim) | | Key | App API key (cak_live_…) | "issuer"-purpose AppSigningKey (csk_live_…) | | Auth | Bearer App API key (cak_live_…) | HS256 JWT + one HMAC per exchange | | Browser sees key | No | No | | Sets Cana session cookie | No | Yes | | Who renders the UI | You do | Cana does |

Browser — <script> tag

A pre-built IIFE bundle is served from your Cana instance and falls through to jsDelivr for CDN edge caching. Exposes CanaOpenAI.CanaAgent, CanaOpenAI.defineTool, CanaOpenAI.CanaClient, etc.

<script src="https://cana.build/sdk/openai.js"></script>
<script>
  const agent = new CanaOpenAI.CanaAgent({
    appId:    "app_…",
    // Fetch a short-lived JWT from YOUR backend (minted there with CanaIssuer).
    // The SDK caches it, auto-refreshes before exp, and re-fetches once on a 401.
    getToken: () => fetch("/my-backend/cana-jwt").then(r => r.text()),
    model:    "OpenAI/gpt-4o-mini",
  });

  agent.run({ prompt: "Hello" })
    .then(r => console.log(r.text));
</script>

✅ With getToken, no long-lived secret ever reaches page source — your backend mints a short-lived JWT per session and the SDK refreshes it automatically. This is the safe browser pattern. Do not paste a static apiKey (cak_live_…) into client-side code: anyone who views source can read it.

Plain OpenAI SDK against the proxy

The proxy authenticates with a standard Authorization: Bearer header, so any OpenAI client (openai-node, openai-python, the AI SDK from Vercel, etc.) works out of the box — no fetch wrapper, no signing. URL shape:

https://cana.build/api/v1/apps/<appId>/openai/chat/completions

Just pass your App API key as the client's apiKey; the OpenAI SDK sends it as Authorization: Bearer … for you — example using openai-node 4.x:

import OpenAI from "openai";

const APP_ID = "app_…";

const openai = new OpenAI({
  baseURL: `https://cana.build/api/v1/apps/${APP_ID}/openai`,
  apiKey:  process.env.CANA_APP_KEY!,        // cak_live_… → sent as Authorization: Bearer
});

Low-level: signRequest

A generic HMAC-SHA256 helper, still exported. As of v1.0.0 it is not used for proxy auth — the proxy authenticates with Authorization: Bearer (see Authentication), so you don't need this for normal SDK or plain-OpenAI usage. It remains available if you have your own HMAC-signing needs (e.g. webhooks). It is the same primitive CanaIssuer uses internally for the /exchange/issue exchange.

import { signRequest } from "@cana-ai/sdk/openai";

const { signatureHeader, timestamp, signatureHex } = await signRequest({
  signingKey: "csk_live_…",
  rawBody:    JSON.stringify({ … }),
  // timestampMs: 1700000000000,  // override for deterministic tests
});

Low-level: parseChunkStream

Parses an OpenAI-style SSE body into AsyncIterable<ChatCompletionChunk>. Handles partial-buffer reassembly, comment heartbeats, and the [DONE] sentinel automatically.

import { parseChunkStream } from "@cana-ai/sdk/openai";

const res = await fetch(url, { method: "POST", headers, body });
for await (const chunk of parseChunkStream(res.body!)) {
  process.stdout.write(chunk.choices[0]?.delta.content ?? "");
}

Errors

All non-2xx responses throw CanaApiError with .status and a typed .code. Common codes:

| code | HTTP | meaning | | --- | --- | --- | | signature_invalid | 401 | wrong key or malformed header | | signature_expired | 401 | timestamp outside 5-min skew | | signature_replayed | 401 | nonce already seen | | idempotency_conflict | 409 | same key + different body | | app_paused | 403 | App.status != LIVE | | rate_limited | 429 | check Retry-After | | billing_exceeded | 402 | App monthly cap hit | | unknown_provider | 400 | no LlmProviderConfig with that name | | model_not_found | 400 | config exists but doesn't host the model | | request_invalid | 400 | Zod-rejected request body | | response_format_unsupported_for_provider | 400 | Anthropic + strict JSON schema | | mcp_connector_unknown | 400 | connector not enabled on this App | | mcp_dispatch_failed | 502 | MCP server returned an error | | upstream_error | 502 | LLM provider failed | | internal | 500 | unhandled |

import { CanaApiError } from "@cana-ai/sdk/openai";

try {
  await agent.run({ prompt: "..." });
} catch (e) {
  if (!(e instanceof CanaApiError)) throw e;
  if (e.code === "rate_limited") { /* back off, check Retry-After */ }
  if (e.code === "billing_exceeded") { /* upgrade plan */ }
}

In agent.stream, errors surface as a terminal {type:"error", code, message, status} event instead.

TypeScript exports

import {
  // High-level agent
  CanaAgent, AgentSession, defineTool,
  type CanaAgentOptions, type AgentTool, type AgentToolContext,
  type AgentRunArgs, type AgentRunResult, type AgentEvent, type ToolCallRecord,

  // Hosted-page / embed issuer
  CanaIssuer,
  type CanaIssuerOptions, type CanaIssuerUser,
  type SignJwtArgs, type IssueExchangeCodeArgs, type ExchangeCodeResult,

  // Low-level client
  CanaClient, type CanaClientOptions,
  signRequest, parseChunkStream,

  // Errors + wire types
  CanaApiError,
  type ChatMessage, type ChatTool, type ChatToolCall, type ChatToolChoice,
  type ChatCompletion, type ChatCompletionChunk, type ChatCompletionRequest,
  type ResponseFormat, type ResponseFormatJsonSchema,
  type CanaErrorBody,
} from "@cana-ai/sdk/openai";

Web Crypto only

Zero node:crypto import at compile time — bundles cleanly for Node 18+, Bun, Deno, Cloudflare Workers, Vercel Edge, and modern browsers.

License

MIT