Skip to content

Repository files navigation

@rxmxdhxni/agentrouter

npm version license node

Unofficial Node/Bun/Deno SDK for AgentRouter — handles the quirks so you can skip straight to ar.chat("hi").

60-second SDK overview

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.

Install

npm install @rxmxdhxni/agentrouter
# or
bun add @rxmxdhxni/agentrouter

Important

Requires Node 20+, Bun latest, or Deno latest. Zero runtime dependencies.

Quick start

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."

API

new AgentRouter(options)

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.

chat(prompt, opts?)

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".

complete(request)

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 response

CompletionRequest 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

stream(input, opts?)

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

AgentRouter.models

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"]

Models

Known-good models (verified against live API; subject to upstream availability):

  • claude-opus-4-6
  • claude-opus-4-7
  • claude-opus-4-8 — default
  • glm-5.2 — reasoning model (also returns content)
  • gpt-5.5
  • kimi-k3 — reasoning model (also returns content; needs generous maxTokens)

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.

Errors

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;
  }
}

Streaming

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;
}

FAQ

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.

License

MIT

About

Unofficial SDK for AgentRouter (agentrouter.org) — Claude, DeepSeek, and GLM via OpenAI-compatible proxy. Zero dependencies, fully typed, works on Node, Bun, and Deno.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages