@nrouter_ai/sdk
v3.1.2
Published
nRouter SDK — one API key for models across six provider clouds. Native OpenAI and Anthropic wire formats, plus embeddings, audio, images and video.
Maintainers
Readme
@nrouter_ai/sdk (JS/TS)
SDK for the nRouter LLM gateway: one API key for models
across six provider clouds. It wraps the official openai package with the
same API surface, pre-configured for nRouter.
The examples below pass a claude-* id on purpose: client.nr.chat() sends
Claude ids to /v1/messages itself (the only wire Anthropic serves) and
translates the response back to an OpenAI-shaped completion. The plain
client.chat.completions.create() surface does not do that — give it a
model whose provider serves chat-completions, such as gpt-5.4-mini. Pass any
model returned by client.nrouterModels.list() for your key.
Install
npm install @nrouter_ai/sdkAuthentication & Setup
The SDK automatically reads your API key from the NROUTER_API_KEY environment variable:
export NROUTER_API_KEY="sk-nrouter-your-api-key-here"Usage
import { nRouter } from "@nrouter_ai/sdk";
const client = new nRouter(); // reads NROUTER_API_KEY from env
const response = await client.nr.chat({
model: "claude-sonnet-4-5-20250929",
prompt: "Hello!",
maxTokens: 32,
});
console.log(client.nr.text(response));const { nRouter } = require("@nrouter_ai/sdk");
const client = new nRouter({ apiKey: process.env.NROUTER_API_KEY });nRouter extends the OpenAI class directly, so the resources nRouter serves
(chat.completions, completions, responses, embeddings, images, audio,
videos, models, streaming) are called exactly as you would call them against
OpenAI — same method names, same request and response shapes.
What "compatible" does and does not mean
The openai package exposes a larger API than nRouter serves, and a model is
callable only on the routes ITS provider serves. Three limits. Every one of them
fails loudly — a 404 from the gateway, never a silent wrong answer — but they
fail at call time, not at compile time: the wrapper inherits the full openai
type surface, so all three type-check.
- Resources nRouter does not mount. The served resources are exactly
chat.completions,completions,responses,embeddings,images,audio,videosandmodels. Everything else theopenaiclient carries 404s —files,fineTuning,batches,beta(assistants/threads),vectorStores,uploads,containers,conversations,webhooks,moderations,evals,graders,admin,skillsandcontentProvenanceChecks.realtimeis the one that does not 404, because it is a WebSocket surface with no gateway to connect to; it fails to open. Treat the served list, not this one, as authoritative — it isspec/nrouter-sdk-spec.json, derived from the gateway's own route table. - Methods a served resource does not mount. Being on the served list is per
ROUTE, not per resource.
images.generate()is served andimages.edit()/images.createVariation()are not;videos.create(),videos.retrieve()andvideos.downloadContent()are served andvideos.list()/videos.delete()/videos.remix()are not;models.list()andmodels.retrieve()are served andmodels.delete()is not. - Routes a model's provider does not serve. An Anthropic model answers
/v1/messagesand neither/v1/chat/completionsnor/v1/responses. Only OpenAI and Azure models answer/v1/responses. Sending a Claude id toclient.chat.completions.create()is a 404 from the gateway, not a translation. AWS Bedrock is narrower than its name suggests: only the Anthropic family on Bedrock is served, and only on/v1/messages. A Nova, Llama, Titan, Qwen, Mistral or DeepSeek id on Bedrock has no text route here at all — the refusal says so rather than pointing you at a second wire.
client.nr.chat() covers the common half of the third one: it recognises a
Claude id by name and sends it to /v1/messages. That is a NAME heuristic, so
it does not help with an alias whose name hides its provider — a Bedrock or
Vertex id for a non-Claude model. For those, and whenever you call
client.chat.completions directly, ask the gateway instead of guessing:
const models = await client.nrouterModels.list();
const entry = models.data.find((m) => m.id === alias);
// e.g. ["/v1/messages", "/v1/messages/count_tokens"]
console.log(entry.nrouter_endpoints);Any alias whose nrouter_endpoints contains /v1/chat/completions works with
the stock openai resource unmodified. An empty array means no route on this
gateway serves that alias — pick another model rather than trying a second wire.
nRouter Helpers
Use client.nr.chat() when you want nRouter features and response metadata in
one call:
const result = await client.nr.chat({
model: "claude-sonnet-4-5-20250929",
prompt: "Summarize this ticket.",
systemPrompt: "Be concise.",
promptTemplateId: "<prompt-template-id>",
promptVariables: { customer: "Acme" },
cache: false, // force provider egress; omit or true uses the gateway default
});
console.log(client.nr.text(result));
console.log(result.meta.requestId, result.meta.cost, result.meta.model);Guardrails are not selected per request. They are assigned per key, team or
organization in the nRouter dashboard and apply automatically to every call.
The guardrailIds option is deprecated and throws a configuration error: the
gateway runs no per-request override, so it never scoped anything.
Other helpers:
client.nr.compare(options, models)runs one prompt against several models and returns results in the same order asmodels.client.nr.stream(options, signal)opens an SSE stream with typed errors.client.nr.responses(body, options)posts to/v1/responsesand applies the same nRouter guardrail, prompt-template and cache fields.client.nr.messages(body, options)posts to/v1/messagesfor Anthropic-style message bodies while keeping nRouter metadata and errors.client.nr.countTokens(body)posts to/v1/messages/count_tokens; the body is sent unchanged so callers can use the gateway token-count contract directly.client.nr.meta(headers)parsesx-nr-*headers from a response you obtained another way.client.nr.media.speech(),.transcribe(),.translate(),.image(),.video(),.videoStatus(),.videoContent()and.embeddings()cover the non-chat endpoints with the same metadata and error handling.
Audio and voice
speech() returns audio BYTES with the metadata attached, and transcribe() /
translate() return a result discriminated on the media type you asked for, so
an srt request comes back as a cue track rather than flattened into { text }:
const spoken = await client.nr.media.speech({
model: "tts-1",
input: "The build is green.",
voice: "alloy",
response_format: "mp3",
});
await fs.writeFile("out.mp3", spoken.bytes);
const heard = await client.nr.media.transcribe({
file: spoken.bytes,
fileName: "out.mp3", // the extension is required
model: "gpt-4o-mini-transcribe",
response_format: "verbose_json", // see below — this is a billing decision
});response_format is not only a formatting choice. A whisper-1 transcription is
priced from the response's duration, and duration arrives only with
verbose_json — ask for anything else on a per-second model and the call is
served but settles unpriced, with no cost figure to report. Token-priced models
(gpt-4o-mini-transcribe, gpt-4o-transcribe) are unaffected. Speech is priced
per character of input.
There is no streaming TTS and no realtime session: a voice turn is a cascade of
three separately billed calls, transcribe() → nr.chat() → speech(). The
runnable version is demo/voice-agent/,
and the full semantics — the upload rules, what is and is not guardrail-scanned,
and why a missing cost must never be summed as zero — are in
docs/audio.md.
Images
image() always returns JSON — url gives you links and b64_json gives you
base64 inside the body, and there is no image-bytes route to return instead:
const res = await client.nr.media.image({
model: "gpt-image-1-mini",
prompt: "A flat vector lighthouse at dusk, three colours, no text.",
n: 1, // 1 through 10 — each image is a separate charge
size: "1024x1024",
quality: "low",
});n, size, quality and response_format are checked before the request leaves
the process, against the gateway's own bounds, so a typo costs no round trip;
validateImageParams() is exported so a form can use the same check. Pass an
unlisted value through extra if a model takes one.
Which quantity you are billed on depends on the model — gpt-image-* prices from
the usage block in the body, everything else prices per image from n × size ×
quality — and no response header carries the count, size or quality. Reconcile
against the spend row by request id rather than recomputing. The runnable version
is demo/image-agent/
and the semantics are in docs/images.md.
Video
Video is asynchronous: the create returns a job, and you collect the result over two more calls. Only the create bills.
const created = await client.nr.media.video({
model: "sora-2",
prompt: "A lighthouse beam sweeping over water at dusk.",
seconds: 4,
size: "1280x720",
});
const jobId = String(created.body.id);
await client.nr.media.waitForVideo(jobId, { pollIntervalMs: 5000, timeoutMs: 600_000 });
const file = await client.nr.media.videoContent(jobId);
await fs.writeFile("out.mp4", file.bytes);Polling and downloading are free of credit but not free of quota — every poll
spends a rate-limit slot, which is why pollIntervalMs has a floor. A free call
reports costStatus: null, which is not the same as the unpriced a billed
call reports when it could not be priced; conflating them turns a long render into
a pricing bug that does not exist. A retry of the create is a second render, not
merely a second bill. The runnable version is
demo/video-agent/ and
the semantics — the sealed job handle, why an accepted-then-failed job stays
billed, and the download bound — are in docs/video.md.
Model Discovery
Use the nRouter helper for model listing:
const models = await client.nrouterModels.list();
console.log(models.data[0].id);The raw nRouter /models response is valid JSON, but the current OpenAI JS SDK
page parser exposes it with an empty data array. nrouterModels.list()
bypasses that parser and returns the gateway response directly. It still travels
the client's own request pipeline, so a configured fetch, timeout,
maxRetries, fetchOptions and default headers apply to it exactly as they do
to every other call.
Development
npm ci
npm testThe test suite runs TypeScript test files directly through Node's built-in test
runner, so use Node 22.18.0 or newer. Older Node 22 builds fail before the
tests execute because they cannot strip TypeScript syntax from .ts test files.
Examples And Live Diagnostics
The repo includes JavaScript examples and interactive tools for manual SDK checks:
npm run build
node demo/agent.js --live
node demo/interactive-agent.mjs --live --voice
node demo/ui/server.jsdemo/interactive-agent.mjs provides an interactive terminal REPL with multi-turn chat, streaming tokens, model switching, per-turn latency/cost tracking, and optional speech synthesis playback (--voice).
demo/ui/server.js starts a conversational browser UI at http://127.0.0.1:4317 with real-time SSE streaming, speech recognition via browser microphone, audio replay for voice synthesis, and one-click test suite execution. The browser does not receive the API key; the local Node server reads NROUTER_API_KEY and calls the built SDK package.
See demo/README.md for commands and
docs/live-sdk-agent-report.md for the latest
manual test findings.
Requirements
Node 22.18 or newer, declared in engines. That is the floor for this
package because the test runner executes TypeScript tests directly through
Node's native type stripping. Before 2.0.0 nothing declared a floor at all, so
an unsupported runtime failed somewhere further in with a worse message.
The dependency tree is deliberately one package. openai 7 has no
dependencies of its own, where openai 4 pulled in 36 transitive packages —
which is where every supply-chain advisory against this package used to come
from.
How guardrails, budgets and routing work
They are configured in the dashboard and enforced at the gateway, not in
this package. The useful guarantee is not that they are always on — it is that
whatever you have enabled cannot be bypassed by a client, this one
included, and behaves identically from every nRouter SDK and from raw curl.
- Guardrails — PII redaction, injection protection, secret and keyword scanning, pre-call and post-call. Which ones run is resolved per request: the organization's guardrail switch first, then the narrowest applicable assignment wins across key > team > org > default, and a winner disabled at that scope does not run.
- Budget controls — spend limits per key, team and organization.
- Observability — cost and usage
on billable calls. Free routes are genuinely free and carry no
x-nr-request-cost:/v1/messages/count_tokens, and video polling and content retrieval.
Smart Router aliases and fallback chains carry two conditions worth knowing before you rely on failover you have not enabled:
- Opt-in by what you put in
model. An alias gets the strategy and its chain; a concrete model is never re-routed and inherits no hidden fallback. - Text wires only — chat completions, responses, messages and legacy completions. Audio, image and video calls take a single-provider route and are not cross-provider Smart Router wires.
- Node.js / TypeScript quickstart and the API reference.
Demos & Examples
Runnable demonstrations live in demo/:
- Quickstart TS / Quickstart JS — basic client and chat completion.
- Voice Agent — speech, transcription, and multimodal agent.
- Chat Agent — conversation memory, tools, and streaming.
- Image Agent — image generation and cost tracking.
- Video Agent — video generation lifecycle.
- Demo Documentation — execution runbooks.
Validation Playbook
This SDK maintains a repeatable 18-step verification process:
- Validation Playbook — comprehensive end-to-end verification runbook.
Open-Source Standards & License
- License: MIT License
- Repository: nRouterGateway/nrouter-sdk
- Issue Tracker: GitHub Issues
