nrouter 2.1.1
nrouter: ^2.1.1 copied to clipboard
nRouter SDK — one API key for models across six provider clouds. Works in Flutter and plain Dart.
nRouter SDK for Dart & Flutter #
One API key for models across six provider clouds. One dependency (http), so
the same code runs on Flutter mobile, desktop, web, and the plain Dart VM.
dependencies:
nrouter: ^2.1.1
Use it #
import 'package:nrouter/nrouter.dart';
final client = NRouter(apiKey: myKey);
final result = await client.chatCompletions({
'model': 'claude-sonnet-4-5',
'messages': [{'role': 'user', 'content': 'Hello!'}],
});
print(result.body['choices']);
client.close();
Streaming #
The four text-generation wires expose cold, cancellable SSE streams. The SDK
sets stream: true, accepts OpenAI [DONE] and native Anthropic
message_stop terminators, and turns in-band gateway errors into the same
typed exceptions as buffered calls.
await for (final chunk in client.messagesStream({
'model': 'claude-haiku-4-5-20251001',
'max_tokens': 64,
'messages': [{'role': 'user', 'content': 'Hello!'}],
})) {
print(chunk.delta);
}
Available helpers are chatCompletionsStream, completionsStream,
messagesStream, and responsesStream; stream(path, body) is the generic
escape hatch. Cancelling the subscription stops consuming the underlying HTTP
response.
Streaming metadata is captured from the opening response headers. The final
request cost is normally unknown there because the headers are sent before
generation completes; cost therefore remains null, never a misleading
zero.
Why there is no environment fallback #
The server-side nRouter SDKs read NROUTER_API_KEY. This one deliberately does
not: Platform.environment requires dart:io, which does not exist in a
Flutter web build, and on mobile it is empty anyway. A fallback that quietly
resolves to nothing is worse than no fallback, so apiKey is required.
Do not ship a key in the app #
Anything compiled into a Flutter bundle is readable by anyone who downloads it — and on web it is served in plain text. A shipped key is a published key, and an nRouter key spends real credits. Mint a short-lived key on your backend and pass it here.
What a call cost #
final meta = result.meta;
print('request ${meta.requestId} | model ${meta.model}');
// Branch on the status, never on `cost` being null-ish. An unpriced model
// reports cost == null, and rendering that as $0 reports a free request —
// which no enabled model is.
print(meta.isPriced ? 'cost \$${meta.cost}' : 'cost unpriced');
NRouterResponseMeta carries all thirteen x-nr-* headers: requestId,
cost, costStatus, model, inputTokens, outputTokens, totalTokens,
cacheReadTokens, cacheWriteTokens, limitSource, authReason,
responseCache, responseCacheAge.
Errors #
Every refusal is a subclass of the sealed NRouterError, chosen from the
gateway's stable code — not the HTTP status, which cannot separate the two
400s or the two 429s. Because it is sealed, a switch over it is exhaustive and
the analyzer tells you when a new case appears.
try {
await client.chatCompletions(body);
} on NRouterGuardrailBlockedError {
// a rule denied it; changing the request is the fix
} on NRouterCreditError {
// out of credits; topping up is the fix
} on NRouterRateLimitError catch (e) {
// e.body?.limitSource names WHICH ceiling. null when the gateway could not
// attribute the refusal — this SDK does not guess, because sending a
// customer to raise the wrong limit is worse than saying nothing.
if (e.isRetryable) await retryLater();
}
| Type | Code(s) | HTTP |
|---|---|---|
NRouterRequestError |
invalid_request |
400 |
NRouterGuardrailBlockedError |
guardrail_blocked |
400 |
NRouterAuthenticationError |
invalid_api_key |
401 |
NRouterCreditError |
insufficient_credits |
402 |
NRouterNotFoundError |
model_not_found |
404 |
NRouterRateLimitError |
rate_limit_exceeded, tpm_limit_exceeded |
429 |
NRouterServiceError |
credit_check_failed, service_unavailable |
503 |
NRouterOtherError |
anything newer than this SDK | — |
NRouterTransportError |
never reached the gateway | — |
isRetryable is true only for rate-limit, service and transport failures.
Configuration #
NRouter(
apiKey: myKey,
baseUrl: 'https://api-stage.nrouter.ai/v1',
httpClient: myClient, // your own http.Client — proxy, retries, mocks
);
Pass your own http.Client and this SDK will not close it; the one it creates
itself is released by close().
Endpoints #
All 15 gateway operations have named helpers: chatCompletions, completions,
embeddings, imagesGenerations, messages, countTokens, responses,
models, model, createVideo, retrieveVideo, downloadVideoContent,
audioSpeech, audioTranscriptions, and audioTranslations. post, get,
bytes, and multipart remain available as escape hatches.
Not JSON: audioTranscriptions and audioTranslations send multipart/form-data
(the gateway requires a binary file part, so the JSON helpers cannot reach them);
audioSpeech() and downloadVideoContent() return raw bytes plus metadata;
bytes(path, body) remains available for other non-JSON responses. The JSON
helpers refuse a non-JSON response rather than handing back an empty body for a
request you were billed for.
Build and test #
dart pub get
dart analyze
dart test
Publishing: PUBLISHING.md.
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.
- API reference — the wire contract every SDK here implements.