@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.
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 sameThe 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 targetResponses 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-tokenconst 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
| Option | Header / effect |
|---|---|
apiKey | Authorization: Bearer <key> (platform API key, or an osr serve token) |
tenant / opts.tenant | X-OSR-Tenant - the tenant's constraints and budget apply |
app | X-OSR-App - attribution echoed in the decision metadata |
opts.requestId | X-Request-Id - your own id, echoed back and usable for feedback |
headers, opts.headers | extra headers for every / this request |
timeoutMs, opts.timeoutMs | abort when no headers arrive in time (streams keep flowing) |
opts.signal | your AbortSignal (also cancels a stream) |
fetch | a 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:
| Class | Status | code |
|---|---|---|
AuthenticationError | 401 | authentication |
PermissionDeniedError (upgradeRequired when X-Upgrade: true) | 403 | permission |
NotFoundError | 404 | not_found |
RateLimitError (budget from the X-Budget-* headers) | 429 | rate_limit |
OpenSmartRouteError | 400 / 413 / 422 | validation |
OpenSmartRouteError | 501 / 502-504 / other 5xx | not_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 tsupThe 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-tsWithout 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.