npm.io
0.7.0 • Published yesterday

@canarycoders/ai

Licence
MIT
Version
0.7.0
Deps
0
Size
468 kB
Vulns
0
Weekly
0

@canarycoders/ai

TypeScript SDK for the CanaryCoders AI gateway. Runs on Node 18+ and Bun, with no runtime dependencies.

Docs: https://docs.ai.canarycoders.es

One client covers chat, image/video/audio generation, embeddings, vision, OCR/PII-redaction, conversational agents, realtime sessions, and usage data. The queue polling, SSE streaming, retries, and error typing are handled for you, so most calls are a single await.

Install

bun add @canarycoders/ai
# or: npm install @canarycoders/ai

Quick start

import CanaryCodersAI from "@canarycoders/ai";

const client = new CanaryCodersAI({ apiKey: process.env.CANARY_AI_API_KEY });

const res = await client.chat.complete({
  provider: "openai",
  model: "gpt-4o-mini",
  messages: [{ role: "user", content: "Hello" }],
});

console.log(res.content, res.usage.totalTokens);

apiKey defaults to CANARY_AI_API_KEY (legacy CANARYLLM_API_KEY still works) and baseURL to the hosted gateway, so new CanaryCodersAI() with the env var set is enough.

How the queue works

The gateway runs most work through a queue: a request returns a queueId, and you poll for the result. The SDK does that for you. chat.complete, images.generate, audio.speech, and the rest submit the job and poll until it finishes, then resolve the typed result.

When you want the handle instead of waiting inline, every queued method has a *Job (or submit) sibling:

const job = await client.chat.submit({ provider: "openai", messages });
console.log(job.id);          // queueId, e.g. to track out of band
const result = await job.result();

Cancelling the await also cancels the job on the server:

const ctrl = new AbortController();
const pending = client.video.generate({ provider: "gemini", prompt }, { signal: ctrl.signal });
ctrl.abort();                 // stops polling and fires queue/cancel

Poll timing is tunable per call (initialIntervalMs, maxIntervalMs, maxWaitMs) or once on the client via the poll option. The default wait budget per operation matches the gateway's own timeouts.

Streaming

for await (const event of client.chat.stream({
  provider: "anthropic",
  model: "claude-sonnet-4-5",
  messages: [{ role: "user", content: "Write a haiku" }],
})) {
  if (event.type === "text") process.stdout.write(event.delta);
}

Events are normalized regardless of the underlying provider: start, text, thinking, tool_call, usage, done. Anything the SDK doesn't recognize comes through as { type: "raw" } rather than being dropped. If the gateway sends an error mid-stream, it throws out of the loop as a typed error.

Media and vision

const img = await client.images.generate({ provider: "openai", prompt: "a yellow canary" });
const speech = await client.audio.speech({ provider: "elevenlabs", text: "Hello there" });
const { text } = await client.audio.transcribe({ provider: "elevenlabs", audio, mimeType: "audio/mp3" });
const song = await client.audio.music({ prompt: "calm lo-fi", durationMs: 20000 });
const { detections } = await client.vision.detect({ image });

OCR and PII redaction (local)

Text recognition with bounding boxes and PII redaction, processed fully on CanaryLLM's own EU infrastructure (Apple Vision). Images and extracted text are never stored.

const { text, lines } = await client.ocr.recognize({ image, granularity: "word" });

const { redactedImage, counts } = await client.ocr.redact({
  image,
  presets: ["email", "phone", "iban"],
  style: "blur", // "box" is the safest for hard PII redaction
});

Embeddings

Embed text into vectors with a local model (LM Studio), for customer-side RAG. The gateway processes the text transiently and stores nothing — you keep the vectors and documents in your own store (e.g. pgvector, sqlite-vec).

const { embeddings, dimensions } = await client.embeddings.create({
  provider: "lmstudio",
  model: "nomic-embed-text-v1.5",
  input: ["first chunk of text", "second chunk of text"],
});

OpenAI embedding clients work too: point them at <baseURL>/v1 and use lmstudio/<model>.

Errors

Every failure is a subclass of APIError: AuthenticationError, PermissionError, RateLimitError, BadRequestError, NotFoundError, InternalServerError, and APIConnectionError / APIConnectionTimeoutError. Branch on .code or .status, not .message, since the gateway sanitizes messages in production.

import { RateLimitError } from "@canarycoders/ai";

try {
  await client.chat.complete({ provider: "openai", messages });
} catch (err) {
  if (err instanceof RateLimitError) {
    console.warn("slow down", err.retryAfterMs, err.remaining);
  } else {
    throw err;
  }
}

Transient failures (429, 5xx, network drops) retry with exponential backoff and jitter. The default is two retries; set maxRetries to change it. Queue submits never retry on 5xx, so a job can't be enqueued twice.

Realtime and conversational agents

The SDK belongs on your backend, where the API key stays. It mints a short-lived credential; the browser opens the actual WebRTC connection.

import { toBrokeredCredential } from "@canarycoders/ai";

// backend
const session = await client.realtime.sessions.create({ kind: "voice", voice: "alloy" });
return toBrokeredCredential(session);   // safe to send to the browser
// browser
import { connectRealtime } from "@canarycoders/ai/realtime-client";

const conn = await connectRealtime({ credential, onEvent: console.log });
conn.send({ type: "response.create" });

For ElevenLabs conversational agents, client.conversations.sessions.create() returns a signed URL you hand to the ElevenLabs client.

Drop-in OpenAI / Anthropic

Already using the official SDKs? Point them at the gateway and keep your code:

import OpenAI from "openai";

const oa = new OpenAI(client.compat.openai());
await oa.chat.completions.create({
  model: "openai/gpt-4o-mini",   // provider/modelId
  messages,
  stream: true,
});

Client options

Option Default Notes
apiKey CANARY_AI_API_KEY API key (clk_live_…)
baseURL hosted gateway override for self-hosted
authStyle "bearer" or "x-api-key"
timeoutMs 60000 per-request total timeout
maxRetries 2 retries for transient failures
fetch global inject a custom fetch
defaultTag attached to requests for usage attribution
poll default poll timing for queued ops

Agent skill

skills/canary-ai/ ships a Codex/Claude Code Agent Skill that drives the gateway (research, X search, chat, image generation, model discovery) from a single Bun script, no MCP server needed. See docs.ai.canarycoders.es/agent-skill for install/update/uninstall steps. It's a plain repo folder, not part of the published npm package.

Keeping types in sync

The gateway API is the source of truth. Regenerate types from its OpenAPI spec and diff them against the hand-written ones:

bun run gen:openapi

License

MIT

Keywords