Skip to content

TypeScript SDK (`sociaro-ai`)

One OpenAI-compatible API for every model behind the gateway — LLM chat (with streaming), image generation/editing, async video, client-side fallback chains, typed errors, and a typed /gw management client for resellers.

Terminal window
npm install sociaro-ai

Requires Node.js 20+ or any edge runtime with WHATWG fetch (Cloudflare Workers, Vercel Edge, Deno) — the SDK is fetch-based, never imports node:http/node:fs, ships ESM + CJS with types, and has zero runtime dependencies.

import { Sociaro } from "sociaro-ai";
const client = new Sociaro({ apiKey: "gw_live_..." }); // or set SOCIARO_API_KEY
const response = await client.chat.completions.create({
model: "alibaba/qwen3.7-max", // always a catalog slug "author/model"
messages: [{ role: "user", content: "What is the speed of light?" }],
});
console.log(response.choices[0].message.content);

model is always a catalog slug "author/model" (see the model catalog) — the gateway’s doors resolve the slug and route to wherever inference runs. The author only selects the request format client-side: anthropic/... models are translated to the Anthropic Messages format and sent to POST /v1/messages; every other author (including ones the SDK has never heard of) uses the OpenAI format via POST /v1/chat/completions. Unknown slugs get the server’s 404/400 — the SDK does not pre-validate them.

JavaScript is async-native, so a single Sociaro class covers what Python splits into Sociaro and AsyncSociaro — every method returns a Promise (or an AsyncIterable when streaming).

Every option is passed to the new Sociaro({...}) constructor:

OptionTypeDefaultNotes
apiKeystringSOCIARO_API_KEY envFalls back to the SOCIARO_API_KEY environment variable when a process.env exists. A missing key throws AuthError at construction time (not on the first request).
baseURLstringhttps://api.sociaro.comTrailing slashes are stripped. Point it at http://localhost:8080 for local development.
timeoutnumber (ms)60000Per-request timeout, enforced with an AbortSignal.timeout.
maxRetriesnumber2Transport-level retries for transient errors (connect/timeout/502/503/504). See Retries vs fallback.
attributionRecord<string, string>Default X-Attr-* headers sent on every request (see Attribution).
modelsRecord<string, string[]>Alias registry mapping a logical name to an ordered fallback chain (see Fallback chains).
fallbackOnRateLimitbooleantrueWhen true, a 429 advances the fallback chain; when false, a 429 is terminal.
fetchFetchLikeglobalThis.fetchCustom fetch implementation for proxies, testing, or runtimes that do not expose a global fetch. Signature: (input: string | URL, init?: RequestInit) => Promise<Response>.
const client = new Sociaro({
apiKey: "gw_live_...", // or set SOCIARO_API_KEY
baseURL: "http://localhost:8080", // override for local dev
timeout: 30_000, // ms; default 60000
maxRetries: 3, // transport retries; default 2
attribution: { team: "product" }, // default X-Attr-* tags
models: { smart: ["openai/gpt-4o", "anthropic/claude-sonnet-4-6"] },
});

On edge runtimes without process.env (some Cloudflare Workers / Deno deploy configurations), always pass apiKey explicitly — the env fallback is guarded so the SDK does not crash where process is absent, but it cannot read a key that is not there, so construction throws AuthError. client.baseURL exposes the resolved base URL (never the api key).

Two distinct layers handle failures, and they compose:

  1. Transport retries (maxRetries) — for one chain entry, the transport retries connection errors, timeouts and 502/503/504 responses with exponential backoff: 500 ms, 1000 ms, 2000 ms between attempts (the schedule caps at the last value). Total attempts = maxRetries + 1. A connect/timeout error that survives all retries surfaces as an APIError with statusCode === 0.
  2. Fallback chain — only after an entry’s transport retries are exhausted does the SDK decide whether to advance to the next entry in the chain. Retryable outcomes (ProviderUnavailableError, the statusCode === 0 connect/timeout APIError, and RateLimitError when fallbackOnRateLimit is on) advance; terminal outcomes (BadRequestError, AuthError, any other APIError) throw immediately.

So a single-entry request still gets maxRetries + 1 transport attempts; a multi-entry chain gets that many attempts per entry before the chain moves on. /gw management calls and streaming opens use the same transport (streaming performs a single attempt — no transport retry once the stream is opening).

const stream = client.chat.completions.create({
model: "anthropic/claude-sonnet-4-6",
messages: [{ role: "user", content: "Count to 5." }],
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta.content ?? "");
}
const img = await client.images.generate({
model: "bytedance/seedream-5-0",
prompt: "A red fox in snow",
});
console.log(img.data[0].url);

images.generate goes through the POST /v1/images/generations door — any door-reachable image-kind slug works by its catalog slug (a few slugs whose upstream has no OpenAI-compatible images path are door-unreachable; see media doors). images.edit is the one escape hatch: it stays on the native /openai/v1/images/edits prefix (the doors reject multipart uploads by design) and supports openai/... models only. Note: no openai image model is in the live catalog yet, so images.edit is currently unusable in production — the example below is illustrative of the API shape only. It accepts byte inputs only — Uint8Array, ArrayBuffer or Blob (File extends Blob). Filesystem paths are a Python-only idiom (the TS SDK is edge-safe); Node users read the file themselves:

import fs from "node:fs/promises";
// Illustrative only — no openai image model is enabled in the catalog today.
const edited = await client.images.edit({
model: "openai/<image-model>",
image: await fs.readFile("original.png"),
prompt: "Add a sunset",
});

Video is called through the standard POST /v1/videos door by catalog slug author/model (e.g. bytedance/seedance-1-5-pro, alibaba/wan2.7-t2v) — the same slug convention as chat and images. The door resolves the slug and routes to wherever inference runs. Blocking — the SDK drives the create→poll→result loop:

const result = await client.video.generate({
model: "bytedance/seedance-1-5-pro", // catalog slug "author/model"
prompt: "A timelapse of cherry blossoms blooming",
seconds: 5,
size: "1280x720", // OpenAI-style "WxH"; the door buckets by short side
intervalMs: 3000, // poll interval (default 2000)
timeoutMs: 300_000, // overall deadline (default 300000)
onProgress: (status) => console.log("status:", status),
});
console.log(result.url); // first/primary URL; result.urls is the full list

generate and submit share these params: model, prompt, seconds, size, image, attribution, plus any extra keys forwarded verbatim through the door. image enables image-to-video: pass a URL or a data: URI string (the SDK passes it through — raw byte upload is out of scope):

const result = await client.video.generate({
model: "bytedance/seedance-1-5-pro",
prompt: "Animate this still into a gentle pan",
image: "https://example.com/frame.png", // or "data:image/png;base64,..."
seconds: 5,
});

Non-blocking — submit returns a Job handle (a single CREATE; no fallback — only the first chain entry is used, since a job id is bound to one model):

const job = await client.video.submit({
model: "bytedance/seedance-1-5-pro",
prompt: "Ocean waves crashing at sunset",
seconds: 5,
});
console.log(job.taskId, await job.status()); // status() = one poll
const result = await job.wait({ timeoutMs: 300_000 }); // poll to terminal, caches
console.log(job.result().url); // cached VideoResult; throws if wait() hasn't run yet

The Job handle exposes taskId, status() (one poll, returns the door’s raw status string), wait(options) (poll to a terminal state and cache the VideoResult), and result() (return the cached result, throwing "job result not available yet — call wait() first" if wait() has not completed). wait() is idempotent — a second call returns the cache.

Polling knobs are milliseconds: intervalMs (default 2000) and timeoutMs (default 300000) — Python’s interval/timeout seconds. The poll wait grows exponentially (×1.5, with jitter) up to a 15 s cap. Watch the units: these polling knobs and the client timeout are milliseconds, but the AsyncJobTimeout.timeout field on a timeout error is seconds (see Errors).

Define logical model aliases backed by an ordered list of catalog slugs; the SDK advances down the chain on retryable errors (5xx, timeouts, connection errors, and 429 by default):

const client = new Sociaro({
apiKey: "gw_live_...",
models: { smart: ["openai/gpt-4o", "anthropic/claude-sonnet-4-6", "alibaba/qwen3.7-max"] },
});
const response = await client.chat.completions.create({
model: "smart",
messages: [{ role: "user", content: "Hello" }],
});

Terminal errors (400/404/422, 401/403) throw immediately. If every entry fails, AllProvidersFailed aggregates the per-entry errors. Streaming fallback happens only before the first chunk is delivered. 429 fallback is controlled by fallbackOnRateLimit (default true).

const client = new Sociaro({ apiKey: "gw_live_...", attribution: { team: "product" } });
await client.chat.completions.create({
model: "openai/gpt-4o",
messages: [{ role: "user", content: "Hello" }],
attribution: { feature: "chat" }, // → X-Attr-Feature header
});

Tags show up as dimensions in GET /gw/stats.

Every error is a subclass of SociaroError; the api key never appears in any message or field.

SociaroError
├── APIError HTTP error from the gateway
│ ├── AuthError 401 / 403
│ ├── RateLimitError 429
│ ├── BadRequestError 400 / 404 / 422
│ └── ProviderUnavailableError 502 / 503 / 504 (after transport retries)
├── AsyncJobFailed async media job reached terminal failure
├── AsyncJobTimeout async media job did not finish within timeout
└── AllProvidersFailed every provider in the fallback chain failed
ClassExtra fields
APIError (and the four HTTP subclasses)statusCode (0 for a connect/timeout error that escaped the transport retries), body (response body, truncated to 2000 chars), provider (the routed slug, when known, else null)
AsyncJobFailedjobId: string, reason: string
AsyncJobTimeoutjobId: string, timeout: numberin SECONDS (mirrors the Python SDK), even though the polling knob you set (timeoutMs) is in milliseconds
AllProvidersFailederrors: SociaroError[] — one entry per chain element; each entry’s message names the slug, and the original error is chained via the standard cause

Units trap: the polling option is timeoutMs (milliseconds), but the AsyncJobTimeout.timeout field on the resulting error is in seconds. A job that runs past timeoutMs: 300000 throws AsyncJobTimeout with timeout === 300.

import {
AllProvidersFailed,
AsyncJobFailed,
AsyncJobTimeout,
AuthError,
RateLimitError,
} from "sociaro-ai";
try {
const result = await client.video.generate({ model: "bytedance/seedance-1-5-pro", prompt });
} catch (err) {
if (err instanceof AllProvidersFailed) {
for (const e of err.errors) console.error(" -", e.message); // per-entry
} else if (err instanceof AsyncJobFailed) {
console.error(`job ${err.jobId} failed: ${err.reason}`);
} else if (err instanceof AsyncJobTimeout) {
console.error(`job ${err.jobId} timed out after ${err.timeout}s`); // SECONDS
} else if (err instanceof RateLimitError) {
console.error("rate limited:", err.statusCode);
} else if (err instanceof AuthError) {
console.error("auth failed:", err.statusCode);
} else {
throw err;
}
}

client.gw is a typed client for the /gw self-service API: everything a reseller needs to automate key issuance, sub-accounts, budgets, white-label branding, analytics and compliance export from their backend. /gw calls are single-target management calls — no fallback chains; the transport retry on transient errors applies.

Group / methodEndpoint
gw.me()GET /gw/me — the calling key’s scopes and entitlements
gw.ceiling()GET /gw/ceiling — the maximum a child key may be granted
gw.keys.list() / create() / revoke(id)GET/POST /gw/keys, DELETE /gw/keys/{id}
gw.subaccounts.list() / create() / update(id)GET/POST /gw/subaccounts, PATCH /gw/subaccounts/{id}
gw.budgets.list() / set()GET /gw/budgets, PUT /gw/budgets
gw.branding.get() / set()GET/PUT /gw/branding
gw.async.list() / iterate()GET /gw/async (cursor pagination; iterate auto-follows)
gw.stats() / gw.usage()GET /gw/stats, GET /gw/usage
gw.evidence()GET /gw/evidence — AsyncIterable of parsed NDJSON lines

The flagship flow — issue a scoped, sub-account-bound key and use it:

const reseller = new Sociaro({ apiKey: process.env.SOCIARO_RESELLER_KEY });
const ceiling = await reseller.gw.ceiling(); // what a child key may get
const sub = await reseller.gw.subaccounts.create({ name: "Acme Corp", slug: "acme-corp" });
const created = await reseller.gw.keys.create({
scopes: ["inference:use"],
entitlements: [{ provider: "openai", model_pattern: "gpt-4o*", effect: "allow" }],
subAccountId: sub.id,
});
// created.key is the PLAINTEXT secret — returned ONLY here. Store it now.
const customer = new Sociaro({ apiKey: created.key });
await customer.chat.completions.create({
model: "openai/gpt-4o",
messages: [{ role: "user", content: "Hello from Acme!" }],
});

Notes that matter in practice:

  • Plaintext key once: keys.create() returns the secret (created.key) only in that one response; the gateway stores a hash and cannot show it again. The response is a GWKeyCreated (api_key_id, key, scopes, optional sub_account_id).

  • entitlements omitted (or null) → the key inherits the client’s full ceiling; an explicit [] is a deliberate deny-all on inference (the two are distinct on the wire).

  • Requested scopes/entitlements must fit gw.ceiling() (403 otherwise).

  • Entitlement axis — each entitlement is { provider, model_pattern, effect } plus an optional axis:

    • axis: "provider" (the default when omitted) matches provider against the inference provider and model_pattern against the native model id.
    • axis: "vendor" matches provider against the catalog slug vendor (the author-part, a glob) and model_pattern against the model name.
    • axis: "kind" matches provider against the catalog modality (llm | image | video | 3d | embedding | rerank | audio) and model_pattern against the model name. Like vendor, it only matches catalog-known slugs; unlike vendor, allow rules on this axis are always operator-issued (there is no self-service resolve path).

    Pick vendor/kind when you reason in catalog slugs/modalities (e.g. allow everything authored by alibaba, or everything of kind: "image"), and provider when you reason in inference providers:

    entitlements: [
    { axis: "vendor", provider: "alibaba", model_pattern: "*", effect: "allow" },
    { axis: "kind", provider: "image", model_pattern: "*", effect: "allow" },
    { axis: "provider", provider: "openai", model_pattern: "gpt-4o*", effect: "allow" },
    ],
  • gw.evidence() streams the tamper-evident export (evidence): a header line, usage/audit records, then a checksum line — each line’s prev field carries the SHA-256 hash chain.

  • /gw error responses carry the {"error":{type,message}} envelope in APIError.body; extract it with parseGWError(err) (returns null for non-envelope bodies, never throws).

import { APIError, parseGWError } from "sociaro-ai";
try {
await reseller.gw.branding.set({ portalSlug: "taken-slug", productName: "Acme AI" });
} catch (err) {
if (err instanceof APIError) {
console.error(parseGWError(err)); // e.g. { type: "conflict", message: "..." }
}
}

gw.async reads the client’s async-media (video/image job) ledger. list() returns one cursor page ({ data, nextCursor }); iterate() is an AsyncGenerator that auto-follows nextCursor until the cursor is empty, with the same filters applied to every page:

// Every failed video task in the last 7 days, across the whole client.
for await (const task of reseller.gw.async.iterate({
scope: "client", // "key" (default) or "client"
status: "failed", // pending | succeeded | failed | cancelled | expired
kind: "video",
createdAfter: "2026-06-01T00:00:00Z", // RFC 3339; createdBefore is the upper bound
limit: 100, // page size, 1..500 (gateway default 100)
})) {
console.error(task.task_id, task.provider, task.model, task.provisional_cost_usd);
}

list() accepts the same scope/status/kind/createdAfter/createdBefore/ limit/cursor filters; cursor is the opaque nextCursor from a previous page. Each GWAsyncTask (snake_case wire shape) carries:

FieldMeaning
task_id, provider, model, kindJob identity and modality
statuspending | succeeded | failed | cancelled | expired
provisional_cost_usdEstimated cost; null on a tariff miss
final_cost_usdSettled cost; null until finalization (and for expired tasks)
expiration_reasonnull for non-expired tasks
attributionThe job’s X-Attr-* tags, or null
created_at, finalized_atRFC 3339; finalized_at is null while pending

sociaro-ai ships type declarations; the public surface is fully typed. The symbols a consumer imports:

  • Client: Sociaro (the value), and the option/result types SociaroOptions, FetchLike, Route (a catalog-slug string), VERSION (the package version string).
  • Error classes (all instanceof-checkable): SociaroError, APIError, AuthError, RateLimitError, BadRequestError, ProviderUnavailableError, AsyncJobFailed, AsyncJobTimeout, AllProvidersFailed, plus the parseGWError(err: APIError) helper.
  • Video: Job, JobWaitOptions, VideoGenerateParams, VideoSubmitParams, VideoImageInput, and the VideoResult result type.
  • Images: ImageGenerateParams, ImageEditParams, ImageInput, and the ImagesResponse result type.
  • Chat: ChatCompletionCreateParams (and the …NonStreaming / …Streaming variants), and the result types ChatCompletion, ChatCompletionChoice, ChatCompletionMessage, ChatCompletionChunk, ChatCompletionChunkChoice, ChatCompletionChunkDelta, ChatMessageParam, CompletionUsage.
  • /gw client: the resource classes GW, GWKeys, GWSubAccounts, GWBudgets, GWBranding, GWAsync; the param types GWKeyCreateParams, GWSubAccountCreateParams, GWSubAccountUpdateParams, GWBudgetSetParams, GWBrandingSetParams, GWStatsParams, GWUsageParams, GWAsyncListParams, GWEvidenceParams; and the result types GWMeResponse, GWCeilingResponse, GWKeyInfo, GWKeyCreated, GWSubAccount, GWBudget, GWBrandingConfig, GWStatsRow, GWUsageRow, GWAsyncTask, GWAsyncListPage, GWEntitlement, and the evidence-line union GWEvidenceLine (GWEvidenceHeaderLine | GWEvidenceUsageLine | GWEvidenceAuditLine | GWEvidenceChecksumLine).
import {
Sociaro,
VERSION,
AllProvidersFailed,
parseGWError,
type Route,
type ChatCompletion,
type GWAsyncTask,
} from "sociaro-ai";
console.log("SDK version:", VERSION);

Wire-shaped result fields stay snake_case (finish_reason, task_id, provisional_cost_usd, …) because they are the verbatim gateway shapes; only the option names you pass in are camelCase.

Both SDKs are kept behaviourally identical by shared contract fixtures (sdk/contract-fixtures/): golden JSON files locking routing, the fallback policy, error mapping, adapters, attribution, the async runner and the /gw client (query-string and body construction, error-envelope and NDJSON parsing). Both test suites run against the same fixtures in CI, so the SDKs cannot drift apart silently.

The differences that remain are deliberate idiom, not behaviour:

TypeScriptPython
Option namescamelCase (subAccountId, limitUsd)snake_case (sub_account_id, limit_usd)
Duration inputsmilliseconds (timeout, intervalMs, timeoutMs)seconds (timeout, interval)
Async-ledger groupgw.asyncgw.async_ (async is a Python keyword)
Client classessingle Sociaro (async-native)Sociaro + AsyncSociaro
Image inputsbytes only: Uint8Array / ArrayBuffer / Blob (edge-safe)bytes, filesystem path, or file object

Wire-shaped response fields stay snake_case in both SDKs (finish_reason, sub_account_id, …) — they are the verbatim gateway shapes. The one duration field that breaks the rule: AsyncJobTimeout.timeout is in seconds in both SDKs, even though the TypeScript polling input is timeoutMs in milliseconds (see Errors).

This page is the authoritative SDK reference — it documents every client option, resource and result type the package exports. For copy-pasteable end-to-end scripts, the repository ships runnable examples: sdk/typescript/examples (node-chat.mjs, reseller-issue-key.mjs, and nextjs-route.ts — a Vercel Edge route showing the SDK running on an edge runtime). The package README is a shorter quick-start; this page is a superset of it.

The same behaviour is available in the Python SDK, kept in lockstep by the shared contract fixtures above.