Skip to content
Documentation
Hosted platform

@opensmartroute/sdk

npm install @opensmartroute/sdk: route, feedback and estimate, the OpenAI-compatible chat, streaming, embeddings and Responses helpers, workspace calls; Node, Deno, Bun, edge and browsers.

Hosted platform 5 min read

TypeScript client for OpenSmartRoute: the hosted platform (https://opensmartroute.ai) and a self-hosted osr serve. Zero runtime dependencies - it uses the global fetch, so it runs on Node 20+, Deno, Bun, Cloudflare Workers, Vercel Edge and in browsers.

npm install @opensmartroute/sdk        # pnpm add / yarn add / bun add work the same

The package version follows the OpenSmartRoute release (1.3.0 today): every release publishes it to the npm registry, and the types under src/generated are generated from that release's platform/api/openapi.json. ESM and CommonJS entry points with bundled .d.ts; import { OpenSmartRoute } from "@opensmartroute/sdk" or const { OpenSmartRoute } = require("@opensmartroute/sdk").

Quick start: route, then report

import { OpenSmartRoute } from "@opensmartroute/sdk";

const osr = new OpenSmartRoute({
  baseUrl: "https://opensmartroute.ai",
  apiKey: process.env.OSR_API_KEY!,
});

const decision = await osr.route({
  text: "Refund order #123, the customer is upset",
  objective: { cost: 0.4, quality: 0.6 },
  constraints: { data_boundary: "private" },
  top_k: 3,
});
console.log(decision.target.id, decision.confidence, decision.explanation);

// ...call the chosen target, then close the loop so the learners improve on your traffic
await osr.feedback({
  request_id: decision.request_id,
  target_id: decision.target.id,
  success: true,
  quality: 0.9,
  latency_ms: 850,
});

route({ ..., execute: true }) also runs the target on the platform and returns the answer in decision.result (text, provider, cost_usd, fallback_from). plan: true composes a persona -> skill -> model plan, plus the tools the model may call (decision.plan). estimate(req) quotes a request before sending it; targets() lists the catalogue; me(), quota() and info() describe the account, where it stands against its monthly request quota, and the deployment.

OpenAI-compatible chat, streaming and embeddings

The same client speaks the platform's /v1 surface. model: "auto" (the default) lets the router choose; a target id pins one; models: [...] gives the router a candidate list with fallbacks. Every answer carries the routing decision in opensmartroute (typed as OsrMetadata).

const reply = await osr.chat.completions.create({
  model: "auto",
  messages: [{ role: "user", content: "Write a haiku about routing." }],
  osr: { objective: { cost: 0.7 }, fallbacks: true },
});
console.log(reply.model, reply.choices[0].message.content);
console.log(reply.opensmartroute.target, reply.opensmartroute.confidence, reply.opensmartroute.cost_usd);

// streaming: an AsyncIterable of chat.completion.chunk objects, parsed from the SSE stream
const stream = await osr.chat.completions.create({
  messages: [{ role: "user", content: "Explain LinUCB in two sentences." }],
  stream: true,
});
console.log("answering:", stream.target); // known from the X-OSR-Target header before the first token
for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta.content ?? "");
  if (chunk.opensmartroute) console.log("\n", chunk.usage, chunk.opensmartroute); // the last chunk
}
// or: const completion = await stream.finalCompletion();

const vectors = await osr.embeddings.create({ input: ["first text", "second text"] });
console.log(vectors.data[0].embedding.length, vectors.opensmartroute.target);

const models = await osr.models.list(); // "auto", "osr/auto:cheap", ... and every executable target

Responses API, Messages API, speech, transcription, images

The OpenAI Responses API (what the OpenAI Agents SDK speaks) and the Anthropic Messages API (what the Anthropic SDKs and Claude Code speak) run over the same routed path; media endpoints route among the targets that can do the job:

const r = await osr.responses.create({ input: "Plan a 3-day trip to Oslo", instructions: "Be concise." });
console.log(responseText(r), r.opensmartroute?.target);
const s = await osr.responses.create({ input: "Explain LinUCB", stream: true });
for await (const ev of s) if (ev.type === "response.output_text.delta") process.stdout.write(ev.delta!);
const final = await s.finalResponse();

const m = await osr.messages.create({ max_tokens: 256, messages: [{ role: "user", content: "Explain LinUCB" }] });
console.log(messageText(m), m.stop_reason);
const ms = await osr.messages.create({ max_tokens: 256, messages: [{ role: "user", content: "hi" }], stream: true });
const done = await ms.finalMessage(); // text and tool_use blocks assembled from the events
const { input_tokens } = await osr.messages.countTokens({ messages: [{ role: "user", content: "hi" }] });

const speech = await osr.audio.speech.create({ input: "Hello there", response_format: "mp3" }); // bytes + headers
const text = await osr.audio.transcriptions.create({ file: audioBlob, filename: "memo.wav" });
const pic = await osr.images.generate({ prompt: "a lighthouse at dawn", size: "1024x1024" });

Workspace management (platform)

Every route of the platform's OpenAPI document is reachable, typed from the generated schema, through osr.rest - paths, path / query parameters, bodies and responses are checked by tsc:

const keys = await osr.rest.get("/api/v1/keys");
await osr.rest.post("/api/v1/keys", { body: { name: "ci", scopes: ["route", "feedback"], expires_in_days: 90 } });
await osr.rest.del("/api/v1/keys/{key_id}", { path: { key_id: keys[0].id } });

Convenience groups sit on top of it: osr.keys (list / create / rename / rotate / revoke), osr.tenants, osr.governance (policy), osr.providers (bring your own model providers), osr.notifications, osr.support, osr.handoffs, osr.billing, osr.marketplace and osr.insights (usage, activity, savings, audit trail, statistics, rankings, SLA).

Against osr serve

osr serve mounts the same routes at the root (no /api/v1 prefix) and takes a bearer token:

osr -t targets.yaml -r rules.yaml serve --port 8000 --token my-secret-token
const local = new OpenSmartRoute({
  baseUrl: "http://localhost:8000",
  apiKey: "my-secret-token",
  apiPrefix: "", // osr serve: /route, /feedback, /targets; /v1/... is the same
});
const who = await local.whoami(); // { edition: "self-hosted", authenticated: true, ... }
const d = await local.route({ text: "Summarise this ticket" });

Options and headers

OptionHeader / effect
apiKeyAuthorization: Bearer <key> (platform API key, or an osr serve token)
tenant / opts.tenantX-OSR-Tenant - the tenant's constraints and budget apply
appX-OSR-App - attribution echoed in the decision metadata
opts.requestIdX-Request-Id - your own id, echoed back and usable for feedback
headers, opts.headersextra headers for every / this request
timeoutMs, opts.timeoutMsabort when no headers arrive in time (streams keep flowing)
opts.signalyour AbortSignal (also cancels a stream)
fetcha custom fetch (tests, proxies, instrumentation)

requestWithResponse(method, path, body) returns the decoded body and the Response, for headers such as X-OSR-Target, X-OSR-Trace-Id, ETag or the quota headers.

Errors

Every non-2xx answer raises OpenSmartRouteError with status, detail (the server's message), requestId (X-Request-Id / X-OSR-Request-Id), retryAfter (seconds, from Retry-After), headers and a stable code:

ClassStatuscode
AuthenticationError401authentication
PermissionDeniedError (upgradeRequired when X-Upgrade: true)403permission
NotFoundError404not_found
RateLimitError (budget from the X-Budget-* headers)429rate_limit
OpenSmartRouteError400 / 413 / 422validation
OpenSmartRouteError501 / 502-504 / other 5xxnot_implemented / upstream / server
ConnectionError- (no response)network
import { RateLimitError } from "@opensmartroute/sdk";

try {
  await osr.route({ text: "..." });
} catch (e) {
  if (e instanceof RateLimitError) await sleep((e.retryAfter ?? 1) * 1000);
  else throw e;
}

Types

RouteIn, FeedbackIn, EstimateIn, ChatCompletionIn, EmbeddingsIn and RoutingOptions are generated from the platform's OpenAPI document (src/generated/openapi.ts, also exported as OpenAPIPaths / OpenAPIComponents for openapi-fetch users). RouteDecision, OsrMetadata, ChatCompletion, ChatCompletionChunk, EmbeddingsResponse, ModelList, TargetInfo, Estimate and Me describe the answers.

Development

npm ci
npm run generate   # regenerate src/generated/openapi.ts from ../api/openapi.json (commit the result)
npm run check      # eslint + tsc + vitest
npm run build      # dist/ (ESM + CJS + .d.ts) with tsup

The live test tests/live.test.ts runs only when OSR_SDK_BASE_URL and OSR_SDK_API_KEY are set (OSR_SDK_API_PREFIX forces /api/v1 or an empty prefix; without it the test probes GET /whoami to recognise osr serve). Against a local server from the repository root:

$env:PYTHONPATH = "src"
python -X utf8 -m opensmartroute -t examples/targets.yaml -r examples/rules.yaml serve --port 8765 --token sdk-test-token
# in another shell
$env:OSR_SDK_BASE_URL = "http://127.0.0.1:8765"; $env:OSR_SDK_API_KEY = "sdk-test-token"
npm test --prefix platform/sdk-ts

Without a model provider configured the chat and embeddings calls answer with a well-formed error (the test accepts either outcome); routing, feedback, targets and models are asserted fully.

License

Apache-2.0 - see LICENSE.