Unofficial Node/Bun/Deno SDK for AgentRouter — handles the quirks so you can skip straight to ar.chat("hi").
Warning
AgentRouter (agentrouter.org) is a third-party reseller offering free credits for Claude, DeepSeek, and GLM via a proxy. This package is a community wrapper — not affiliated with or endorsed by AgentRouter or Anthropic. Your prompts pass through AgentRouter's infrastructure; do not send proprietary code, credentials, or sensitive data. For production, get a key directly from the model provider.
npm install @rxmxdhxni/agentrouter
# or
bun add @rxmxdhxni/agentrouterImportant
Requires Node 20+, Bun latest, or Deno latest. Zero runtime dependencies.
import { AgentRouter } from "@rxmxdhxni/agentrouter";
const ar = new AgentRouter({ apiKey: "sk-..." });
const reply = await ar.chat("What is 2 + 2?");
console.log(reply); // "2 + 2 equals 4."| Option | Type | Default | Description |
|---|---|---|---|
apiKey |
string |
— | Required. Your AgentRouter API key |
model |
string |
"claude-opus-4-8" |
Model for all requests from this instance |
maxTokens |
number |
1024 |
Max tokens per completion |
baseURL |
string |
"https://agentrouter.org/v1" |
Override the API endpoint |
userAgent |
string |
"QwenCode/0.2.0 (linux; x64)" |
Override the User-Agent header |
timeout |
number |
120000 |
Request timeout in ms |
fetch |
typeof fetch |
globalThis.fetch |
Custom fetch. Receives raw Authorization header — do not wrap with untrusted code |
debug |
(msg: string) => void |
— | Debug callback. Authorization is redacted before the callback is called |
Important
baseURL must use https://. The SDK throws TypeError at construction if given http://, file://, or any non-HTTPS scheme — prevents accidentally leaking your key over plaintext.
Sends a single user message, returns the reply as a string.
const reply = await ar.chat("Summarize this in one sentence: ...");Note
Reasoning models (currently glm-5.2, kimi-k3) may return reasoning output alongside or instead of content. chat() returns an empty string only if the model produced no content at all — check .debug output or use complete() to access result.reasoning directly. kimi-k3 in particular can burn its entire maxTokens budget on reasoning before emitting content — pass a generous maxTokens (500+) or you may see empty content with finishReason: "length".
Full control over messages, model, and parameters.
const result = await ar.complete({
messages: [
{ role: "system", content: "You are a concise assistant." },
{ role: "user", content: "Explain TCP handshake." },
],
model: "gpt-5.5",
temperature: 0.7,
maxTokens: 512,
});
console.log(result.content); // answer text
console.log(result.reasoning); // defined for reasoning models
console.log(result.usage); // { promptTokens, completionTokens, totalTokens }
console.log(result.raw); // unwrapped upstream responseCompletionRequest fields:
| Field | Type | Description |
|---|---|---|
messages |
ChatMessage[] |
Required. Array of { role, content, name? } |
model |
string |
Overrides instance default |
maxTokens |
number |
Overrides instance default |
temperature |
number |
0–2 |
topP |
number |
Nucleus sampling |
stop |
string | string[] |
Stop sequences |
signal |
AbortSignal |
Cancellation |
CompletionResult fields:
| Field | Type | Description |
|---|---|---|
content |
string |
Always a string; "" if the model only produced reasoning |
reasoning |
string | undefined |
Reasoning output from reasoning models |
usage |
Usage |
Token counts |
model |
string |
Model echoed from upstream |
finishReason |
string |
"stop", "length", etc. |
raw |
unknown |
Unwrapped upstream JSON response |
Returns an async iterator of StreamChunk objects.
for await (const chunk of ar.stream("Write a haiku about DNS.")) {
if (chunk.type === "content") process.stdout.write(chunk.delta);
if (chunk.done) break;
}input accepts a plain string or a full CompletionRequest.
StreamChunk fields:
| Field | Type | Description |
|---|---|---|
type |
"content" | "reasoning" |
Which field this delta belongs to |
delta |
string |
Incremental text |
done |
boolean |
true on the final chunk |
Static read-only array of known-working models at the time of publish.
console.log(AgentRouter.models);
// ["claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8", "glm-5.2", "gpt-5.5", "kimi-k3"]Known-good models (verified against live API; subject to upstream availability):
claude-opus-4-6claude-opus-4-7claude-opus-4-8— defaultglm-5.2— reasoning model (also returns content)gpt-5.5kimi-k3— reasoning model (also returns content; needs generousmaxTokens)
Note
Channel availability fluctuates upstream. A model that worked yesterday may return NoChannelError today. Check agentrouter.org for the live list of available models, or catch NoChannelError and fall back to another model from AgentRouter.models.
All errors extend AgentRouterError, which carries .status (HTTP code) and .body (raw response).
| Class | Status | Cause | Action |
|---|---|---|---|
UnauthorizedClientError |
401 | Edge rejected the request fingerprint | SDK bug — do not override userAgent or fetch without restoring defaults |
AuthError |
401/403 | Invalid API key | Check your key |
NoChannelError |
503 | No upstream channel for the requested model | Try a different model; .model property names the offender |
ContentBlockedError |
400 | Upstream content policy blocked the prompt | Rephrase the prompt — switching models does NOT help (filter is edge-level) |
RateLimitError |
429 | Too many requests | Back off; .retryAfter (seconds) may be set |
TimeoutError |
0 | Request exceeded timeout ms |
Increase timeout or retry |
AgentRouterError |
any | Unclassified HTTP error | Inspect .status and .body |
Note
User-initiated aborts (via AbortSignal) throw the native DOMException / AbortError — matches standard fetch behavior. TimeoutError only fires for the SDK's internal timeout option.
import {
AgentRouter,
ContentBlockedError,
NoChannelError,
RateLimitError,
UnauthorizedClientError,
} from "@rxmxdhxni/agentrouter";
try {
const result = await ar.complete({
messages: [{ role: "user", content: "..." }],
model: "gpt-5.5",
});
} catch (err) {
if (err instanceof ContentBlockedError) {
console.error("Prompt blocked by upstream filter — rephrase and retry");
} else if (err instanceof NoChannelError) {
console.error(`No channel for ${err.model} — switch model`);
} else if (err instanceof RateLimitError) {
const wait = err.retryAfter ?? 10;
console.error(`Rate limited. Retry in ${wait}s`);
} else if (err instanceof UnauthorizedClientError) {
console.error("SDK misconfigured — do not override userAgent");
} else {
throw err;
}
}Reasoning models emit both "content" and "reasoning" chunks. Separate them:
let answer = "";
let thinking = "";
for await (const chunk of ar.stream({
messages: [{ role: "user", content: "Why is the sky blue?" }],
model: "glm-5.2",
})) {
if (chunk.type === "content") answer += chunk.delta;
if (chunk.type === "reasoning") thinking += chunk.delta;
}Why do I get unauthorized client detected?
AgentRouter blocks requests that don't look like the official OpenAI Node SDK. The SDK ships the exact required headers (x-stainless-lang: js, etc.). This error appears when you override userAgent or pass a custom fetch that strips headers. Revert to defaults.
Why do I get NoChannelError for a model that worked before?
Channel availability fluctuates upstream. AgentRouter rents pooled access to Claude/DeepSeek/GLM, and a model can lose its channel at any time — the 503 body contains 无可用渠道. Catch NoChannelError and fall back to another model from AgentRouter.models.
Why do I get ContentBlockedError?
AgentRouter runs an edge-level content filter that blocks prompts matching certain patterns (e.g. Who is <person>?). The block is BEFORE model dispatch, so switching models will not help — rephrase the prompt instead.
Why is content empty but reasoning has text?
Some reasoning models put part or all of their output in reasoning_content instead of content. Behavior varies by model — glm-5.2 and kimi-k3 currently return both fields populated. Use complete() and read result.reasoning directly rather than assuming content is empty. Note that kimi-k3 can exhaust a small maxTokens budget on reasoning alone, leaving content empty with finishReason: "length" — increase maxTokens if you see this.
Is this package official?
No. It is not affiliated with AgentRouter, Anthropic, DeepSeek, or Zhipu AI.
Can I use the openai npm package directly?
Yes — point baseURL at https://agentrouter.org/v1. The openai package already sends the right Stainless headers. This SDK exists to handle the double-encoded JSON responses from Claude models and to provide typed errors for AgentRouter-specific failures.
MIT
