Skip to content
OpenSmartRoute
Documentation
Hosted platform

@opensmartroute/sdk

Routing, feedback, estimates, streaming chat and every workspace call from Node, Deno, Bun, edge runtimes and browsers.

Hosted platform 6 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 (2.1.0 today): every release publishes it to the npm registry, and the types under src/generated are generated from that release's OpenAPI document. 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

Create an API key at opensmartroute.ai/dashboard/keys (free plan, no card) or point baseUrl at your own osr serve. Only estimate() and info() work without a key.

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.

The short forms take the same loop down to a line each - the text first, the other fields second, per-request options third:

const d = await osr.route("Refund order #123, the customer is upset", { objective: { cost: 0.4 } });
await osr.feedback(d, { success: true, quality: 0.9 }); // request_id and target_id read from the decision

const answer = (await osr.run("Summarise this ticket: ...")).result?.text; // route + execute in one call
const text = await osr.ask("What is the capital of France?", { system: "One word." }); // chat with model "auto"

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, osr.insights (usage, activity, savings, audit trail, statistics, rankings, SLA), osr.memory (what the routed chat remembers, the conversations) and osr.knowledge (the workspace's knowledge graph: facts, "forget that ...", the context preview, suggestions, export, training).

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)
maxRetriesretries after a connection failure, a 408, a 429 or a 502-504 (default 2; Retry-After honoured up to a minute, otherwise backoff from 0.5 s to 8 s with jitter); a final 4xx is never retried, a stream only before its first byte
allowInsecureHttpsend the key over plain http:// to a public host; off by default - a key only travels in clear text to loopback and private addresses and to names that resolve inside a network (router, osr.internal, *.svc)
opts.signalyour AbortSignal (also cancels a stream and a pending retry)
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: "..." });            // already retried twice with backoff when the failure was transient
} catch (e) {
  if (e instanceof RateLimitError) await sleep((e.retryAfter ?? 1) * 1000);
  else throw e;
}

No error message, detail or body ever contains the key you sent.

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 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 (pip install "opensmartroute[server,yaml]"):

osr -t targets.yaml -r 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.

Something wrong or out of date on this page? Tell us - the page is named for you, so a one-line note is enough.