Migrate from OpenRouter
Port an OpenRouter app to AnonRouter by changing the base URL and API key, plus the handful of field differences to know about.
OpenRouter and AnonRouter expose the same developer surface: the OpenAI Chat
Completions API, spoken through the official OpenAI Python and TypeScript SDKs.
Both use creator/model ids, and many ids are identical on the two catalogs.
For most applications the port is exactly two changes: the base URL and the API
key.
https://openrouter.ai/api/v1 -> https://api.anonrouter.ai/v1
sk-or-v1-... -> ar_...Everything below was verified request-by-request with the official OpenAI SDKs (Python and TypeScript) against an AnonRouter gateway.
Before you begin
- Create an AnonRouter account and an inference key in the dashboard. Keys are
prefixed
ar_and shown once. - When you create the key, enable OpenAI-compatible access on it. This is the static-key mode OpenRouter users expect, and it is off by default because the fully private ticket flow is the default. See Compatibility mode for the tradeoff.
Why the extra toggle?
AnonRouter's default flow splits your identity from your prompts with single-use tickets, so the two cannot be linked. A static bearer key cannot make that split, so OpenAI-compatible access is a deliberate per-key opt-in, not the default. You can run both: static key for tools that need it, ticket flow for workloads where unlinkability matters.
The two-line port
from openai import OpenAI
client = OpenAI(
base_url="https://api.anonrouter.ai/v1", # was https://openrouter.ai/api/v1
api_key=os.environ["ANONROUTER_API_KEY"], # was OPENROUTER_API_KEY
max_retries=0, # see "Disable automatic retries"
)
completion = client.chat.completions.create(
model="anthropic/claude-sonnet-4.5", # same id on both catalogs
max_tokens=256,
messages=[{"role": "user", "content": "Hello"}],
)
print(completion.choices[0].message.content)Streaming, stream_options: {"include_usage": true}, tool calling with
multi-turn tool-result loops, response_format structured outputs, image
(image_url) content parts, and models.list() all work through the same two
changes.
Model ids
Both routers use creator/model slugs, and the overlap is large:
anthropic/claude-sonnet-4.5, openai/gpt-4o, google/gemini-3.5-flash,
deepseek/deepseek-v3.2, qwen/..., moonshotai/... resolve on both. The
catalogs are not identical, so map your model list once:
curl -s https://api.anonrouter.ai/v1/models \
-H "Authorization: Bearer $ANONROUTER_API_KEY" | jq -r '.data[].id'Differences to know:
| OpenRouter | AnonRouter |
|---|---|
openrouter/auto | /auto (routes within your privacy boundary) |
some/model:nitro | Same: :nitro sorts providers by throughput |
some/model:floor | Same: :floor sorts providers by price |
:free, :online, :thinking, :extended suffixes | Not supported, model_not_found |
| Unknown model | Same OpenAI error envelope, model_not_found |
Each /v1/models entry also carries AnonRouter metadata the OpenAI SDK ignores:
privacy_class (anonymous, private, e2ee), per-route capabilities
(tools, reasoning efforts, vision), and pricing. Use capabilities the way
you used OpenRouter's supported_parameters.
Request fields: what maps where
Verified field by field. "Ignored" means the request succeeds and the field is silently dropped, so you can migrate without stripping it first.
Works unchanged
| Field | Notes |
|---|---|
messages, temperature, top_p, stop, seed, penalties | Standard passthrough |
max_tokens or max_completion_tokens | Either alias works. Sending both with different values is a 400, not a silent pick |
stream, stream_options.include_usage | SSE with data: [DONE] terminator |
tools, tool_choice, parallel_tool_calls | Full tool-calling transport, streaming and non-streaming |
response_format | json_object and json_schema forwarded to the provider |
reasoning_effort and reasoning: {"effort": ...} | Same two spellings as OpenRouter. Effort levels are per-model, read capabilities.reasoning |
reasoning: {"enabled": false} | Disables thinking on models that support disabling |
provider.order, provider.allow_fallbacks, provider.ignore, provider.only, provider.sort, provider.require_parameters, provider.max_attempts | Same shapes as OpenRouter provider routing. See Routing |
models: ["a", "b", ...] | Cross-model fallback works on the OpenAI-compatible endpoint with a short candidate list. Pricing follows the model actually used |
user | Accepted and forwarded. AnonRouter keeps no per-end-user analytics by design |
Accepted and ignored
| Field | Why |
|---|---|
HTTP-Referer, X-Title headers | No app attribution or public leaderboards, by design. Leave them in or remove them |
transforms: ["middle-out"] | No server-side context compression yet |
route: "fallback" | Legacy OpenRouter routing hint |
usage: {"include": true} | Unnecessary: non-streaming responses already include usage with a cost field in USD |
plugins: [...] | No server-side web search or file parsing. The gateway transports tools, it never executes them |
n, logprobs, top_logprobs, logit_bias | Accepted for SDK compatibility, not honored |
Legacy functions, function_call | Stripped. Migrate to tools and tool_choice |
Must change
| OpenRouter | AnonRouter replacement |
|---|---|
provider.data_collection: "deny" | provider.minimum_privacy: "private" (or "e2ee"). This is a floor the router can never silently lower |
provider.quantizations, other OpenRouter-only provider keys | Remove them. The provider policy object is strict and rejects unknown keys with 400 invalid_request |
reasoning.max_tokens | Rejected. Use effort levels (reasoning.effort). Reasoning tokens share the max_completion_tokens ceiling |
reasoning.exclude: true | Rejected. Use venice_parameters: {"strip_thinking_response": true} to hide the trace |
| OAuth PKCE key issuance ("Connect with OpenRouter") | Not offered. Issue keys from the dashboard |
Disable automatic retries
Set max_retries=0 (Python) or maxRetries: 0 (TypeScript). AnonRouter does
not yet support client-supplied idempotency, so an SDK auto-retry after an
ambiguous failure could start a second paid generation. The OpenAI SDK default
is 2 retries, so this is worth changing during the port. Handle retries
yourself where you need them.
Errors and limits
Errors use the OpenAI envelope, so typed SDK exceptions keep working:
AuthenticationError on a bad key, NotFoundError on an unknown model,
RateLimitError on 429. Your existing error handling ports as-is.
Two OpenRouter surfaces have no AnonRouter equivalent and fail with 404:
the Responses API (client.responses.*) and models.retrieve(id) (use
models.list()). Image and speech generation exist as separate
Studio routes rather than OpenAI-style endpoints.
What you gain
- A privacy floor instead of a preference. Every route is labeled
anonymous,private, ore2ee, andprovider.minimum_privacyis enforced with no silent downgrade. See Privacy. - Routing transparency headers on every completion:
x-anonrouter-provider,x-anonrouter-routing,x-anonrouter-provider-attempts,x-anonrouter-provider-fallback,x-anonrouter-privacy-class. - Inline cost accounting without asking: non-streaming responses carry
usage.costin USD. - Embeddings through the same SDK and key (
POST /v1/embeddings), which OpenRouter does not offer. - An upgrade path beyond drop-in. When a workload needs unlinkability, switch that call site to the ticket flow or an E2EE model. The request body stays the same, only authentication changes.
No BYOK, on purpose
OpenRouter's bring-your-own-key integrations re-identify you to the upstream provider, which defeats the point of an anonymizing router. AnonRouter does not offer BYOK.