# AnonRouter documentation
> AnonRouter is a privacy-focused, OpenAI-compatible model router. Prompts, responses, and keys are never written to disk, and billing settles from usage metadata only.
Source: https://docs.anonrouter.ai
---
# Overview
Build private, OpenAI-compatible AI applications across many models.
AnonRouter is a privacy-focused model router. It exposes familiar,
OpenAI-compatible APIs while separating account identity, request content, and
provider credentials so that prompts, responses, and keys are never written to
disk. Billing settles from usage metadata only.
## Supported API surface
AnonRouter documents the APIs it implements instead of claiming compatibility
with every OpenAI endpoint. The core inference surface is:
* `GET /v1/models` and `GET /v1/catalog/models`
* `POST /v1/inference/tickets`
* `POST /v1/chat/completions`
* `POST /v1/embeddings`
* `POST /v1/images/generations` (when image generation is enabled)
* `POST /v1/audio/speech` (when speech is enabled)
* `GET /v1/capabilities`
Account, routing, and billing endpoints are listed in the
[API reference](/api-reference).
> **A deliberate subset**
>
> Endpoints, parameters, and behaviors in these docs are the ones AnonRouter
> actually implements. Features that are not part of the live surface are left
> out.
## Choose a model or router
Start with an exact [model ID](/models), or use a [router](/routers) to select
for cost, speed, coding, uncensored chat, or TEE inference. For $0 chat, enable
[free models](/free-models) and choose `anonrouter/free` or a listed `:free` ID.
Free access uses Anonymous routes and shared capacity, with no paid fallback.
Before large requests, read [context limits](/context-limits) and
[long-context pricing](/metering#long-context-pricing). The output cap, temporary
balance hold, and final charge are different parts of a request.
## Two ways to connect
The **private ticket flow** keeps the stable API key on the control plane and
sends only a single-use ticket to the content relay. The relay sees request
content and an opaque ticket, never your key or account. This is the preferred
production integration.
**Compatibility mode** accepts a normal `Authorization: Bearer` key for clients
that cannot perform the ticket step, such as off-the-shelf OpenAI SDK
integrations. It is easier to adopt, but the compatibility broker temporarily
holds API-key identity and plaintext content together in memory. Newly created
keys enable it by default so ordinary tools work immediately; turn it off at
creation when a key will use only the private ticket flow.
## The privacy promise, and how to check it
* In the private ticket flow, no single component holds both who you are and what
you sent. Identity stays on the control plane; content goes to the relay with
only an opaque ticket.
* The content side (relay, compatibility broker, and every provider worker)
runs inside an **Intel TDX confidential VM** that terminates TLS in-enclave, so
prompts never reach a host an operator can read them off.
* Prompts, responses, system prompts, tool arguments, and provider payloads are
never written to disk.
* API keys are shown once and stored only as a hash.
* Billing is computed from usage metadata: model, token counts, status,
latency, and cost.
* Automatic routing never silently widens the privacy set you allow.
None of that is a promise you have to take on faith. The plane attests to itself
credential-free, so you can verify the build handling your traffic before you
send it anything:
```bash
NONCE=$(openssl rand -hex 32)
curl -s "https://api.anonrouter.ai/v1/gateway/attestation?nonce=$NONCE"
```
Read the full [privacy model](/privacy) for what is and is not retained, and
[verify AnonRouter itself](/tee-verification#verify-anonrouter-itself-hop-1) for
what that document proves. On an ordinary route the upstream provider still sees
your prompt under its own policy. The [privacy label](/models) says which, and
an [E2EE route](/e2ee) is what avoids it.
---
# Quickstart
Make your first ticketed chat completion with AnonRouter.
## Before you begin
You need an AnonRouter inference API key. Create one in the dashboard, keep it on
a trusted server, and load it from an environment variable. Inference keys are
prefixed `ar_` and are shown only once at creation.
```bash
export ANONROUTER_API_KEY="ar_..."
export ANONROUTER_API_URL="https://api.anonrouter.ai/v1"
export ANONROUTER_CONTROL_URL="https://control.anonrouter.ai/v1"
```
> **Do not expose API keys in browsers**
>
> The examples below are for trusted server-side environments. A browser client
> should call your own backend, which holds the key.
## 1. Request a single-use ticket
The ticket binds the request to its model and output limit. Issue a new ticket
for every inference request. Tickets are valid for 30 seconds, so issue one and
send the completion promptly.
```bash
TICKET=$(curl -s "$ANONROUTER_CONTROL_URL/inference/tickets" \
-H "Authorization: Bearer $ANONROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "meta-llama/llama-3.3-70b",
"max_completion_tokens": 256
}' | jq -r .ticket)
```
The response contains an opaque `ticket` string plus the constraints it was
bound to (`model`, `max_output_tokens`, `operation`, `privacy_class`,
`reasoning`, and `expires_in`).
## 2. Send the completion
Present the ticket to the relay in the `x-anonrouter-ticket` header. The relay
does not need your API key. The `model` and output limit must match the ticket.
```bash
curl "$ANONROUTER_API_URL/chat/completions" \
-H "Content-Type: application/json" \
-H "x-anonrouter-ticket: $TICKET" \
-d '{
"model": "meta-llama/llama-3.3-70b",
"max_tokens": 256,
"messages": [
{
"role": "user",
"content": "Explain private model routing in one paragraph."
}
]
}'
```
The response is a standard OpenAI chat completion object. Headers report the
selection: `x-anonrouter-selected-model` and `x-anonrouter-routing`, plus
`x-anonrouter-provider`, `x-anonrouter-provider-attempts`,
`x-anonrouter-provider-fallback`, and `x-anonrouter-privacy-class` for the
provider that served it. See [response headers](/api-reference#response-headers).
## With the OpenAI SDK
The OpenAI SDKs work against the same base URL. Issue the ticket with a plain
HTTP call, then pass it as an extra header on the completion. Use a non-secret placeholder for the OpenAI client's required key setting;
your actual key belongs only on the content-free ticket request. Disable
automatic retries because tickets are single-use and an ambiguous failure can
follow a paid generation.
```python
import os
import httpx
from openai import OpenAI
api_url = os.environ.get("ANONROUTER_API_URL", "https://api.anonrouter.ai/v1").rstrip("/")
control_url = os.environ.get(
"ANONROUTER_CONTROL_URL",
"https://control.anonrouter.ai/v1",
).rstrip("/")
api_key = os.environ["ANONROUTER_API_KEY"]
ticket_response = httpx.post(
f"{control_url}/inference/tickets",
headers={"Authorization": f"Bearer {api_key}"},
json={
"model": "meta-llama/llama-3.3-70b",
"max_completion_tokens": 256,
},
)
ticket_response.raise_for_status()
ticket = ticket_response.json()["ticket"]
# The real key was used only for the content-free ticket request. The OpenAI
# client requires a non-empty api_key, so use a non-secret placeholder here.
client = OpenAI(api_key="ticket-only", base_url=api_url, max_retries=0)
completion = client.chat.completions.create(
model="meta-llama/llama-3.3-70b",
max_tokens=256,
messages=[{"role": "user", "content": "Hello from AnonRouter"}],
extra_headers={"x-anonrouter-ticket": ticket},
)
print(completion.choices[0].message.content)
```
```typescript
import OpenAI from "openai";
const apiURL = process.env.ANONROUTER_API_URL ?? "https://api.anonrouter.ai/v1";
const controlURL =
process.env.ANONROUTER_CONTROL_URL ?? "https://control.anonrouter.ai/v1";
const apiKey = process.env.ANONROUTER_API_KEY!;
const ticketRes = await fetch(`${controlURL}/inference/tickets`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "meta-llama/llama-3.3-70b",
max_completion_tokens: 256,
}),
});
if (!ticketRes.ok) throw new Error(`Ticket request failed: ${ticketRes.status}`);
const { ticket } = await ticketRes.json();
// The real key was used only for the content-free ticket request. The OpenAI
// client requires a non-empty apiKey, so use a non-secret placeholder here.
const client = new OpenAI({ apiKey: "ticket-only", baseURL: apiURL, maxRetries: 0 });
const completion = await client.chat.completions.create(
{
model: "meta-llama/llama-3.3-70b",
max_tokens: 256,
messages: [{ role: "user", content: "Hello from AnonRouter" }],
},
{ headers: { "x-anonrouter-ticket": ticket } },
);
console.log(completion.choices[0].message.content);
```
## Prefer a static key?
If your client cannot perform the ticket step, use a key with
[compatibility mode](/compatibility) enabled and point the OpenAI SDK straight
at `https://api.anonrouter.ai/v1` with no ticket. New keys enable compatibility
by default. Review the privacy tradeoff first.
## Next steps
* Read [authentication](/authentication) to understand keys and tickets.
* Browse the [model catalog](/models) and privacy labels.
* Use [`/auto` routing](/routing) to let AnonRouter pick a model.
* Choose or pin the serving provider with [provider routing](/provider-routing).
* Control [reasoning effort](/reasoning) per request.
---
# Authentication
Inference keys, management keys, tickets, private accounts, and compatibility mode.
AnonRouter separates credentials by purpose. A single bearer credential does not
automatically receive both inference and account-management authority.
## Credential types
| Credential | Purpose | Prefix |
| -------------------------- | ------------------------------------------------------------------------------------------------------------ | ------ |
| Inference API key | Issue tickets and call inference endpoints | `ar_` |
| Management key | Read account balance and usage metadata | `arm_` |
| Connected-app access token | List models and issue tickets within an approved app grant; [not enabled in production yet](/connected-apps) | `aro_` |
| Browser or private session | Manage keys, workspaces, billing, and account state | Cookie |
| Inference ticket | Authorize one model-bound inference request | Opaque |
Secrets are shown once when they are created. Store them in a secret manager or
an environment variable. Keys are stored only as a hash, so a lost key cannot be
recovered and must be replaced.
## Private ticket flow
The recommended production flow has two steps:
1. Present the inference key to the control plane at `POST /v1/inference/tickets`
and request a ticket.
2. Present the ticket to the relay in the `x-anonrouter-ticket` header.
The ticket is short-lived, single-use, and bound to the authorized request
constraints (model, operation, output limit, and reasoning selection). The relay
redeems the opaque ticket and never receives your key or account identity. A
request whose `model`, output limit, or reasoning selection drifts from the
ticket is rejected before any provider work.
> **Issue one ticket per request**
>
> Tickets are valid for about 30 seconds and can be redeemed once. Do not cache
> or reuse them.
## Inference keys vs management keys
**Inference keys** (`ar_`) can list models, issue tickets, and run completions,
embeddings, image generation, and speech. They cannot read account balance or
usage.
**Management keys** (`arm_`) can read `GET /v1/balance` and `GET /v1/usage`. They
cannot run inference or manage other keys.
Creating, listing, and revoking keys is a dashboard operation that requires an
authenticated session. Neither key type can rotate keys programmatically. An
account may hold up to 20 active inference keys and 5 active management keys.
Each inference key can carry an optional per-key credit limit and a compatibility
flag. The dashboard enables OpenAI-compatible access by default for new keys;
turn it off at creation when the key will use only the private ticket flow.
## Compatibility mode
Compatibility mode is intended for clients that can only send a static
`Authorization: Bearer ...` header, such as off-the-shelf OpenAI SDK
integrations.
> **Privacy tradeoff**
>
> In compatibility mode, the broker receives API-key identity and plaintext
> request content together in memory. Prompts and responses are not intentionally
> logged or retained, but this flow is not unlinkable like the private ticket
> flow.
Compatibility mode is controlled per inference key and is only available where
the operator has enabled it. New keys default to compatibility access on; you can
turn it off for a ticket-only key. See
[compatibility mode](/compatibility) for the full integration.
## Accounts
You can create an account two ways:
* **Email and password.** Standard sign-up with email verification.
* **Private account.** A username is assigned automatically, there is no email,
and the session stays opaque. Your identity never becomes part of the
inference request.
### Recovery phrases
A private account can protect itself with a recovery phrase: a standard BIP-39
mnemonic, shown once at sign-up. The dashboard uses a 12-word phrase.
Recovery at `POST /v1/private-accounts/recover` verifies the phrase, resets the
password, and revokes every existing session and API key. A recovery phrase only
resets the account password. It does not derive a wallet or encrypt account
data. Rotating the phrase invalidates all earlier phrases.
> **Store the phrase safely**
>
> If you lose the recovery phrase for a private account with no email, you may
> permanently lose access.
## Management access
Management keys read account-level balance and usage metadata. They cannot run
inference or create and revoke credentials. Credential lifecycle operations
require an authenticated browser or private session.
Applications can also receive narrowly scoped access through
[Connected Apps](/connected-apps). A Connect access token represents a user's
explicit `openid inference` grant to one app and its dedicated workspace; it is
not a general account credential.
---
# Models and privacy labels
Browse the catalog, read model capabilities, and understand the privacy label on each route.
Every model in AnonRouter is a route with a declared privacy guarantee, a set of
capabilities, and a price. Model IDs follow a `creator/model` slug, for example
`meta-llama/llama-3.3-70b`. The router also resolves a model's raw provider
route id, so either form works in a request.
## Latest model aliases
Use a **Latest** alias when you want a model family to follow AnonRouter's
latest reviewed stable release without changing the model ID in your app. For
example, **DeepSeek Flash Latest** uses the exact ID
`~deepseek/deepseek-flash-latest`, including the leading `~`.
A Latest alias points to one reviewed model in its creator and family. It is
updated through a reviewed promotion, not by guessing which provider name or
release date is newest. It is an explicit model choice, rather than `/auto` or
a router that chooses among different families for each request.
| Alias | Model ID |
| --------------------- | --------------------------------- |
| DeepSeek Flash Latest | `~deepseek/deepseek-flash-latest` |
| DeepSeek Pro Latest | `~deepseek/deepseek-pro-latest` |
| Kimi Latest | `~moonshotai/kimi-latest` |
| GLM Latest | `~z-ai/glm-latest` |
| GLM Flash Latest | `~z-ai/glm-flash-latest` |
| MiniMax Latest | `~minimax/minimax-latest` |
| Qwen Latest | `~qwen/qwen-latest` |
| Qwen Coder Latest | `~qwen/qwen-coder-latest` |
Use the alias as the `model` value in a chat request:
```json
{
"model": "~deepseek/deepseek-flash-latest",
"max_tokens": 1024,
"messages": [{ "role": "user", "content": "Explain this code." }]
}
```
In the [private ticket flow](/quickstart), request the ticket with the same
alias ID. The concrete target is frozen when the ticket is issued: a promotion
between ticket issuance and completion does not switch that request's model.
The response names the concrete model that served the request.
### Check what Latest currently means
The model page shows the current target and version history. Catalog entries
include `alias.targetModelId`, `alias.revision`, and `alias.history`;
`GET /v1/models` also exposes the target as `canonical_slug`. Read the current
catalog rather than hard-coding today's mapping in your app.
Prices, capabilities, context limits, and eligible provider routes follow the
resolved target. A future promotion can change them. Your provider pin,
workspace settings, privacy boundary, and price limits still apply; if the
target is unavailable or fails those checks, the request is refused rather than
silently choosing an unrelated model. A `-latest` name does not make a model
free or add it to the automatic routing pool.
For reproducible integrations, select the concrete versioned model ID instead
of the Latest alias. See [provider routing](/provider-routing) to pin a provider
as well, and [choose a router](/routers) when you want selection across models.
## List models
`GET /v1/models` returns the models enabled on the deployment. It requires an
inference or management key.
```bash
curl "https://control.anonrouter.ai/v1/models" \
-H "Authorization: Bearer $ANONROUTER_API_KEY"
```
Each entry describes one model:
```json title="Illustrative model object"
{
"id": "meta-llama/llama-3.3-70b",
"object": "model",
"owned_by": "meta-llama",
"display_name": "Llama 3.3 70B",
"provider": "venice",
"model_type": "text",
"privacy_class": "private",
"provider_privacy_class": "private",
"context_window": 65536,
"max_output_tokens": 4096,
"pricing": {
"input_usd_per_million_tokens": 0.7,
"output_usd_per_million_tokens": 2.8,
"unit_usd": null
},
"capabilities": {
"embeddings": false,
"streaming": true,
"tools": true,
"web": false,
"reasoning": {
"supported": false,
"effort_configurable": false,
"supported_efforts": [],
"can_disable": false,
"default_effort": null,
"always_on": false
}
},
"routing": {
"enabled": true,
"quality_tier": 4,
"tasks": ["general", "coding"],
"expected_latency_ms": 900
}
}
```
> **The endpoint is the source of truth**
>
> Field values above are illustrative. Prices, context windows, and the enabled
> model set change over time. Read them from the live endpoint rather than
> hard-coding them.
`model_type` is one of `text`, `image`, `tts`, or `embedding`. For flat-priced
media models, `pricing.unit_usd` is the per-image or per-character price and the
per-token fields are null.
## Public catalog
`GET /v1/catalog/models` is an unauthenticated, cacheable view of the catalog
with richer privacy metadata (`privacyLevel`, `privacySummary`, `privacyNotes`,
`moderation`, and `modalities`). It supports `ETag` and returns `304` when
unchanged, which makes it convenient for a public model browser.
## Route prices and long-context tiers
The listed price of the serving route determines the charge. Input, output,
cache-read, and cache-write rates can differ; a model's lowest advertised rate is
not a promise that every eligible provider route has that rate. With an active
[subscription](/subscriptions), allowance usage is priced at your plan's price
for the route, which is never higher than the pay-as-you-go price.
Some routes publish prompt-size tiers in `pricing.long_context_tiers` on
`GET /v1/models`. The public catalog exposes `priceTiers` and
`priceTiersInEffect`. `in_effect` on an OpenAI-shaped tier, or
`priceTiersInEffect` in the public catalog, says whether tier billing is enabled.
If false, the standard rates apply. When enabled, read the serving route's
schedule: above a tier's prompt threshold, its rates apply to the whole request,
including output and cache buckets. At exactly the threshold, the lower tier
applies. See
[metering and long-context pricing](/metering#long-context-pricing).
The published context window and a price threshold are different limits. A
higher price tier does not grant a larger window; an intentional route cap
still applies. See [context limits](/context-limits).
## Privacy labels
Every route carries one privacy classification. The label tells you the
strongest guarantee that route actually provides.
| Label | `privacy_class` | What it means |
| --------- | --------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Anonymous | `anonymous` | Your identity is hidden, but the model provider can see the prompt while serving it. Provider retention is not guaranteed. |
| Private | `private` | Prompt and response content is not retained after the request completes. |
| TEE | `tee` | Inference runs inside a hardware-isolated, attestable enclave. |
| E2EE | `e2ee` | Your client encrypts the prompt so that only a verified enclave can decrypt it. |
> **AnonRouter never stores payloads**
>
> These labels describe the upstream provider handling. Regardless of the label,
> AnonRouter itself does not write prompts or responses to disk. On Anonymous
> routes the provider can still see the prompt, so provider zero-retention is not
> guaranteed there.
> **Read `privacyNotes`, not just the label**
>
> The label is a category; the per-route notes are the actual boundary, and on
> two of them the difference matters.
>
> **E2EE says the enclave you verified is the only thing that can decrypt, not
> that it is the machine running the model.** On some Venice routes the enclave
> that holds your key is a verified gateway rather than the model host: it
> decrypts and forwards the plaintext onward inside the provider. The route's
> `privacyNotes` say so.
>
> **`private` is a retention promise, not an enclave.** A `private` route carries
> no attestation. A provider whose name evokes confidential computing is not
> thereby a TEE route; the route's privacy label is the claim.
TEE and E2EE labels describe the route modality; they do not by themselves
claim a particular strength of attestation. See the [E2EE client guide](/e2ee)
for encrypted transport setup and [TEE verification and encrypted
inference](/tee-verification) for the live provider matrix, verification levels,
public evidence API, request receipts, and the opaque NEAR v2 protocol.
## Free models
Free variants use a separate `/:free` ID and cost $0, including
inference fees. `anonrouter/free` chooses an available free model for each
request; it never selects a paid route. `/auto` does not select free variants.
Enable free models once for your account on the dashboard **Privacy** page, a
free model's page, or the Free Models Router page. That opt-in is consent to
Anonymous routes, where the provider can see and may retain your prompts and
responses. No per-request consent field is required, but workspace privacy
boundaries still apply.
See [free models](/free-models) for setup, examples, eligibility, shared capacity,
workspace settings, and errors. See [choose a router](/routers) to compare the
Free Models Router with the paid routers.
## Capabilities
The `capabilities` block tells you what a model supports before you call it:
* `streaming`: server-sent event streaming.
* `tools`: tool-calling fields are accepted and forwarded to the model.
* `web`: the model can use web access.
* `embeddings`: the model is an embedding model.
* `reasoning`: whether reasoning is supported, whether the effort level is
configurable, which efforts are attested, and whether it can be disabled. See
[reasoning controls](/reasoning).
## Moderation
Moderation is a property of the model, not a router policy. Models published as
uncensored run unfiltered. The public catalog exposes a `moderation` field so you
can filter the catalog by it.
## Model health
`GET /v1/models/uptime?model=&days=` returns a passive success-rate and
latency series for a model, computed from request metadata.
---
# Free models
Enable free chat, choose a free model or the Free Models Router, and understand privacy and shared capacity.
Free models cost **$0, including inference fees**. Use `anonrouter/free` to let
AnonRouter choose an available free model, or copy an exact
`/:free` ID from the [model catalog](https://anonrouter.ai/models).
An eligible account can use them with an empty credit balance.
> **Free models use Anonymous routes**
>
> The upstream provider can see and may retain your prompts and responses.
> Free models do not provide the Private, TEE, or E2EE guarantees. AnonRouter's
> own content-handling boundary is described in the [privacy model](/privacy).
## Enable free models
1. Sign in to AnonRouter and open the dashboard **Privacy** page, a free model's
page, or the Free Models Router page.
2. Read the Anonymous-routing disclosure and enable free models for your account.
3. Use an inference key with [OpenAI-compatible access](/compatibility) in an
app, or use the [private ticket flow](/authentication) in your own integration.
Eligibility normally requires a verified, non-disposable email on the eligible
account for that inbox, or a credited purchase. An account that has made a
credited purchase may qualify even if it is a private account. If the dashboard
says the account is not eligible, complete the requirement it shows.
**The account opt-in is your Anonymous consent.** It applies to free requests
from every inference key on that account. You do not need to add a custom
`provider` field to a client that only accepts a URL, key, and model name.
`provider.minimum_privacy: "anonymous"` remains accepted, but does not replace
the account opt-in.
A workspace privacy boundary of **Private** or **Maximum**, or a request that
explicitly requires `private`, `tee`, or `e2ee`, still prevents free routing.
Enabling free models never weakens that boundary or changes how paid requests
are routed.
## Choose a free model
| Model value | What happens |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `anonrouter/free` | Chooses an eligible free model for each request. The selected model can change between requests. |
| Exact `/:free` ID | Uses only free routes of that model. Copy a currently listed ID; adding `:free` to an arbitrary paid ID does not create a free offering. |
| `/auto` or a paid router | Uses its own paid-model pool. Enabling free models does not add free routes to it. |
Free models serve chat completions. Streaming, tools, and image input depend on
the selected model's advertised capabilities; a free price does not imply
support. The Free Models Router does not accept explicit reasoning controls
such as `reasoning_effort`; choose an exact `:free` model whose advertised
capabilities support them. E2EE and a `models` fallback list are not supported
for free requests.
The advertised context and output limits belong to the free offering and can
be smaller than those of its paid sibling. If you omit an output limit, the
free output budget is at most **1,024 tokens**, bounded by the selected route's
maximum. An explicit output cap is not silently reduced; it must satisfy the
route's limits. See [context limits](/context-limits).
## Make a request
With a compatibility-enabled key, use the ordinary chat endpoint:
```bash
curl https://api.anonrouter.ai/v1/chat/completions \
-H "Authorization: Bearer $ANONROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "anonrouter/free",
"max_tokens": 512,
"messages": [{ "role": "user", "content": "Explain a binary search." }]
}'
```
For the private ticket flow, issue a ticket on
`https://control.anonrouter.ai/v1/inference/tickets` with the same model, output
limit, and provider policy, then send the chat to the API origin with that
ticket. Follow the [quickstart](/quickstart) for the complete flow.
Free usage is recorded at $0. The response reports the selected model and an
Anonymous route; it is never relabeled as Private because you opted in.
## Use free models in another app
For a client with OpenAI-compatible settings, enter:
| Setting | Value |
| -------- | --------------------------------------------------- |
| Base URL | `https://api.anonrouter.ai/v1` |
| API key | Your compatibility-enabled AnonRouter inference key |
| Model | `anonrouter/free` or an exact listed `:free` ID |
Some apps require the full chat URL instead of a base URL. For example,
[Janitor AI](/janitor-ai) uses
`https://api.anonrouter.ai/v1/chat/completions`. Keep the account opt-in enabled;
no additional Anonymous-consent field is needed in the app.
## Shared capacity and retries
Free capacity is shared and availability can change as upstream allowances or
promotions change. The standard account allowance is **20 requests per minute,
100,000 reserved tokens per minute, and 2 concurrent free requests**, shared
across its keys and free models. An active [subscription](/subscriptions)
raises these limits. Reserved tokens include input exposure and the
authorized output budget; they are not just the tokens eventually generated.
Shared upstream windows and account fair-share limits can stop requests before
that allowance is reached.
When capacity is exhausted, use the response's `Retry-After` value when present
and wait before retrying. Changing keys does not reset the account allowance.
For an exact model that has exhausted its share, you can explicitly choose
another `:free` model or `anonrouter/free`; neither option guarantees capacity.
The Free Models Router can choose another eligible free model before inference
starts. Once a concrete model is serving the request, provider fallback stays
on that same free model, and no provider switch occurs after output starts.
**There is no paid fallback.** To use a paid model, make a separate request
with a paid model or router ID.
## Workspace model pools
The Free Models Router respects your workspace privacy and provider settings.
A model pool that only names paid `/auto` models does not exclude free routing.
If the pool explicitly names free variants, it narrows the free candidates:
include keeps matching variants and exclude removes them. The
`anonrouter/free` pattern represents all free variants.
An exact `:free` request pins its model and does not use the automatic model
pool. Its privacy boundary, provider policy, account eligibility, and capacity
checks still apply.
## Troubleshooting
| Status | Code | What to do |
| ------ | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `403` | `free_models_not_enabled` | Enable free models for the account. |
| `403` | `free_models_not_eligible` | Follow the account eligibility requirement shown in the dashboard. |
| `403` | `privacy_boundary_not_met` | Use a paid route that meets the workspace boundary, or deliberately change that boundary if appropriate. |
| `400` | `anonymous_consent_required` | The request explicitly asks for a stronger privacy tier. Use a matching paid route, or remove that request constraint only if you intend Anonymous routing. |
| `400` | `free_router_excluded_by_settings` | Review the named model-pool, provider, or privacy setting in the error. |
| `400` | `free_router_no_capable_model` | No available free model supports this request's capabilities or size. Reduce the request or select a suitable model explicitly. |
| `429` | `free_capacity_exhausted`, `free_account_capacity_exhausted`, `free_daily_capacity_exhausted` | Wait for the indicated reset. Free capacity and account shares are independent of paid credit balance. |
| `503` | `free_route_unavailable` | Retry later or explicitly choose another available model. No paid fallback was used. |
For a context or output-cap error, reduce the request before trying again.
Repeated unchanged retries can trigger a [context-refusal cooldown](/context-limits#errors-and-retries).
---
# Choose a router
Choose a model automatically for cost, speed, coding, uncensored conversations, TEE inference, or free access.
A router is a model ID that chooses a concrete model for each request. Use it
where an app asks for a model name, or as `model` in a Chat Completions request.
The choice can change between requests as models and available routes change.
| Model ID | Use it for | How it chooses |
| -------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `/auto` (also `anonrouter/auto`) | General automatic selection | Uses your routing strategy, allowed model pool, and privacy preferences. See [intelligent routing](/routing). |
| `anonrouter/cheap` | Lower-cost requests | Chooses a healthy, eligible model with enough capability for the task, then favors lower expected cost. |
| `anonrouter/fast` | Faster responses | Favors healthy, capable models using measured response time where available, then configured model preferences and latency estimates. |
| `anonrouter/coding` | Programming tasks | Chooses among a curated set of coding models, considering quality, reliability, and cost. |
| `anonrouter/uncensored` | Uncensored conversations | Chooses among a curated set of uncensored or abliterated models, considering quality, reliability, and cost. |
| `anonrouter/tee` | Plaintext inference on TEE routes | Chooses only routes classified as TEE, considering quality, reliability, and cost. |
| `anonrouter/free` | Free inference | Chooses an available free model. Requires account opt-in and uses Anonymous routes, with no paid fallback. See [free models](/free-models). |
| An exact `creator/model` ID | A specific model | Keeps the model fixed; [provider routing](/provider-routing) chooses who serves it. |
Cheap does not mean the cheapest model regardless of capability. Fast does not
promise a latency or choose a model solely from a name: when sufficient recent
measurements exist, it considers both time to first token and expected generation
time. Actual cost and speed depend on the selected model and your request.
For a stable ID that follows reviewed releases within one family, use a
[Latest model alias](/models#latest-model-aliases), such as
`~deepseek/deepseek-flash-latest`. Unlike a router, it resolves to one reviewed
model target rather than selecting across families.
## Send a request
With a [compatibility-enabled key](/compatibility), set the router ID as the
model:
```bash
curl https://api.anonrouter.ai/v1/chat/completions \
-H "Authorization: Bearer $ANONROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "anonrouter/coding",
"messages": [{"role": "user", "content": "Explain this TypeScript error."}],
"max_tokens": 1024
}'
```
In the [private ticket flow](/authentication), issue the ticket for the same
router ID, then send that ID in the completion body with the ticket. Send the
same provider policy and output limit at both steps. A paid router needs a
positive available balance; its selected model is billed at the serving route's
listed prices, including any applicable [long-context prices](/metering#long-context-pricing).
The router itself has no single fixed per-token price.
## Privacy and saved settings
Paid routers honor your workspace privacy boundary, model pool, provider
restrictions, request filters, and price constraints. They do not widen a
workspace boundary to find a candidate. A router with no eligible member
returns an error instead of silently choosing outside those settings.
Cheap and Fast favor price or speed for provider selection when you omit a
request provider policy. Coding, Uncensored, and TEE use the workspace provider
sort. You can narrow selection with `routing.allow`, `routing.exclude`,
`routing.privacy_classes`, or a `provider` policy, subject to the workspace's
restrictions. An explicit `routing.strategy` does not change a named router's
purpose; use `/auto` to choose between cost, balanced, and quality strategies.
The TEE Router sends ordinary plaintext requests to TEE routes. It does not
perform client-side encryption or make a stock OpenAI client verify
attestation. E2EE uses an explicit model and a separate verification and
encryption flow. Read [TEE verification](/tee-verification) and
[E2EE inference](/e2ee) before relying on those guarantees.
For the Free Router, enabling free models on the account is consent to
Anonymous routing: the provider can see and may retain prompts. A request or
workspace that requires stronger privacy is refused. Free requests never
become paid requests when capacity runs out.
## What served the request
The response's `model` is the concrete model that served the request, not the
logical router ID. Response headers identify the selection:
* `x-anonrouter-selected-model`: the selected model.
* `x-anonrouter-routing: auto`: a model was selected automatically.
* `x-anonrouter-provider`: the serving provider.
* `x-anonrouter-privacy-class`: the serving route's privacy tier.
Provider fallback for a paid router stays on the chosen model and within that
router's eligible routes. It does not change to another model. Explicit
cross-model fallback lists in compatibility mode have their own
[fallback contract](/openrouter).
## Constraints and errors
Paid named routers are for plaintext chat. Use an explicit model for reasoning
controls or E2EE. Tools, streaming, and image input narrow the eligible pool to
routes that can serve the request. See [context limits](/context-limits) for
output caps and context errors.
| Error | Meaning |
| --------------------------------------- | ----------------------------------------------------------------------- |
| `400 reasoning_requires_explicit_model` | Select a concrete model before setting reasoning controls. |
| `400 router_excluded_by_settings` | Your model, provider, or routing settings exclude the router's members. |
| `400 router_no_capable_model` | No eligible member supports the request's required capabilities. |
| `403 privacy_boundary_not_met` | No eligible member meets the workspace's privacy boundary. |
| `503 router_route_unavailable` | No eligible route can serve this router request now. |
For free access and capacity errors, use the separate [free-model error table](/free-models).
---
# Intelligent routing
Select models automatically without silently widening privacy constraints.
Use `model: "/auto"` when AnonRouter should choose an eligible model for the
request. The values `auto` and `anonrouter/auto` are accepted aliases, and any
model string containing a `*` glob (for example `meta-llama/*`) also triggers
automatic selection over the matching routes.
For a goal-specific model ID that also works in apps with only a model field,
see [choose a router](/routers): Cheap, Fast, Coding, Uncensored, TEE, and Free.
This page covers choosing a model. To choose which provider serves a model, and
how AnonRouter fails over between providers, see
[provider routing and failover](/provider-routing).
```json
{
"model": "/auto",
"messages": [
{
"role": "user",
"content": "Review this distributed system design."
}
],
"routing": {
"strategy": "balanced",
"max_cost_usd": 0.02
}
}
```
The ticket you issue must also be automatic. Request a ticket with the same
`/auto` or glob `model`, then send the completion with the matching value.
## The routing object
The optional `routing` object shapes the candidate set and the choice:
| Field | Values | Purpose |
| ----------------- | ---------------------------------------------- | -------------------------------------------------------------- |
| `strategy` | `cost`, `balanced`, `quality` | Trade off model quality against price. Defaults to `balanced`. |
| `privacy_classes` | array of `anonymous`, `private`, `tee`, `e2ee` | Restrict eligible privacy tiers. |
| `max_cost_usd` | number | Drop candidates whose worst-case cost exceeds this. |
| `allow` | array of glob patterns | Restrict candidates to matching models or providers. |
| `exclude` | array of glob patterns | Remove matching models or providers. |
`strategy` controls quality and price, not privacy. `cost` prefers cheaper,
lower-tier models, `quality` prefers the highest quality tier, and `balanced`
sits between them and can step up for complex prompts.
```json
{
"model": "meta-llama/*",
"messages": [
{
"role": "user",
"content": "Analyze this TypeScript API."
}
],
"routing": {
"exclude": ["*/*-guard"],
"strategy": "cost"
}
}
```
Exclusions always win, and `allow`/`exclude` patterns match against both the
`provider/model` form and the bare model id.
## Privacy is never widened
When `privacy_classes` is omitted, the eligible set defaults to the
privacy-preserving tiers (`private`, `tee`, `e2ee`). Automatic routing never
silently adds a weaker tier. To let `/auto` also consider Anonymous routes, you
must ask for it explicitly:
```json
{
"model": "/auto",
"messages": [{ "role": "user", "content": "Summarize this text." }],
"routing": {
"privacy_classes": ["anonymous", "private"]
}
}
```
If no model satisfies the constraints, the request fails closed rather than
downgrading to a route you did not allow.
> **Choose a TEE or E2EE path explicitly**
>
> `/auto` does not select TEE or E2EE routes. For plaintext TEE inference, use
> an explicit TEE model or the [TEE Router](/routers). E2EE requires an explicit
> model and its provider-specific encryption and verification flow.
A TEE privacy label alone does not make an ordinary OpenAI client verify
attestation. See [TEE verification and encrypted inference](/tee-verification)
for the checks a verifying client performs and the provider-specific limits.
E2EE requests are bound to their provider and do not fall back to another route.
Automatic selection respects [context limits and output budgets](/context-limits).
An explicit output cap is preserved; AnonRouter does not shorten or summarize
the prompt to make it fit.
## Model suffixes
Add a suffix to a model id to choose how its provider route is picked, without
a `provider` object:
| Suffix | Picks |
| ----------- | ----------------------------------------------------------------------------------- |
| `:cheapest` | The cheapest route first. `:floor` is the same. |
| `:fastest` | The highest-throughput route first. `:nitro` is the same. |
| `:private` | Only Private or stronger routes, most private first (E2EE, then TEE, then Private). |
| `:tee` | Only TEE or E2EE routes. |
| `:e2ee` | Only E2EE routes. |
```json
{
"model": "deepseek/deepseek-v4.1-flash-1017:private",
"messages": [{ "role": "user", "content": "Summarize this contract." }]
}
```
`:private`, `:tee` and `:e2ee` are guarantees. If the model has no route at that
level, the request fails instead of using a weaker one.
A suffix never weakens your saved privacy settings. With a Private floor on your
account or workspace, `:cheapest` picks the cheapest Private route. Without one,
it picks the cheapest route at any privacy level you allow.
E2EE routes still need the encrypted client flow described in
[TEE verification and encrypted inference](/tee-verification).
Use one suffix per model id. A second suffix, or any suffix not listed here or
under free models below (for example `:online` or `:thinking`), makes the id
unknown and the request fails with `model_not_found`.
### Free models (`:free`)
`:free` is different from the suffixes above. It does not change how a model is
routed. It names a separate free model, `creator/model:free`, which exists only
where AnonRouter lists one; adding `:free` to a model without a free version
returns `model_not_found`. A free model does not combine with another suffix
(`400 free_variant_id_invalid`) and is never picked by `/auto`. Free models run
on Anonymous routes and need the account opt-in. See [free models](/free-models)
for setup, limits and errors.
## Response headers
Successful requests report the actual selection:
* `x-anonrouter-selected-model` is the chosen `provider/model`.
* `x-anonrouter-routing` is `auto` when a model was selected automatically, or
`exact` when you named a single model.
The response body's `model` field is also rewritten to the selected model. Any
classification the router computes to make the choice is used in memory and is
not stored as durable usage metadata.
## Saved routing preferences
An account can save default routing behavior that applies when a request does not
override it. Preferences are managed with a session.
| Method | Endpoint | Description |
| ------ | ----------------------------- | ------------------------------------------------- |
| `GET` | `/v1/routing/effective` | List models the caller's saved preferences allow. |
| `GET` | `/v1/routing/preferences` | Read saved routing preferences. |
| `PUT` | `/v1/routing/preferences` | Replace saved routing preferences. |
| `POST` | `/v1/routing/preview` | Preview a routing decision for a sample prompt. |
| `POST` | `/v1/routing/eligible-models` | List models eligible for draft preferences. |
Preferences carry a `privacy_level` that maps to a default privacy set:
* `open` allows `anonymous`, `private`, `tee`, and `e2ee`.
* `balanced` allows `private`, `tee`, and `e2ee`.
* `maximum` allows only `tee` and `e2ee`.
A `pool_mode` of `all`, `include`, or `exclude` with `model_patterns` narrows the
default candidate pool, and `strategy` sets the default quality tradeoff.
These are defaults for `/auto` only. A workspace can also set a
[privacy boundary](/workspaces#privacy-boundary) (`privacy_boundary`) that
applies to every request. `/auto` never goes below it: when the boundary is
stricter than `privacy_level`, `/auto` uses the boundary, and an `/auto`
selection below it is refused with `403 privacy_boundary_not_met`.
---
# Provider routing and failover
Choose which provider serves a model, and how AnonRouter moves to another provider when one rate-limits or fails, without lowering privacy.
Model routing and provider routing are separate steps. [`/auto`](/routing)
chooses a model. Provider routing chooses who serves the model you named. Several
providers can serve the same model, and switching between them never changes the
model id you send.
You can leave `provider` out to use your workspace's saved provider settings.
New workspaces use the [Default sort](#the-default-sort), which weighs privacy,
cost, speed, and reliability. Existing workspaces keep their saved sort. If a
provider fails before output reaches you, eligible requests can try another
provider within your fallback settings.
## Models and provider routes
A model has one canonical `creator/model` id, for example
`deepseek/deepseek-v3.2`. Each provider that serves it is a **provider route**
with its own privacy tier and price. `GET /v1/models` lists the routes under the
canonical model in `provider_routes`, and `auto_provider` names the provider
Auto would pick right now. See [models and privacy labels](/models).
There are no provider-prefixed public model ids. To choose a provider, use the
`provider` field described below.
## The `provider` request field
Send `provider` in the chat completion body. It is either a provider id (an
exact pin) or a policy object:
| Field | Values | Purpose |
| -------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `order` | array of provider ids | Try these providers first, in this order. Remaining eligible routes fill in by `sort`. |
| `only` | array of provider ids | Use only these providers. |
| `ignore` | array of provider ids | Never use these providers. |
| `allow_fallbacks` | boolean, default `true` | `false` makes a single attempt on the first eligible route. |
| `sort` | `privacy`, `price`, `latency`, `throughput` | Explicit request ordering. Omit `provider` to use the workspace sort, which can also be Default. |
| `minimum_privacy` | `anonymous`, `private`, `tee`, `e2ee` | The lowest tier any attempt may use. |
| `max_price` | `{ "input": number, "output": number }` | Ceiling in USD per million tokens. Routes above it are skipped. |
| `require_parameters` | boolean | Only use routes that support every parameter in the request, such as tools or reasoning. |
| `max_attempts` | `1` to `3` | How many providers may be tried for this request. Defaults to 3, or your workspace's limit. |
A bare string is an **exact pin** with no fallback. `"provider": "venice"` is
the same as `{ "only": ["venice"], "allow_fallbacks": false }`. Omitting
`provider` means Auto.
Provider ids are the catalog's `provider` values, such as `venice`, `tinfoil`, or
`deepinfra`. An id also matches that provider's variants and regions. Every
route is listed under its own provider id; there is no catch-all provider. NEAR
AI routes, for example, appear as `near-ai`, with route ids
`near-ai/`. The earlier id `other` is still accepted for NEAR AI in
`order`, `only`, `ignore` and pins, but the catalog and responses always report
`near-ai`.
The policy is strict. A provider in both `only` and `ignore`, or an `order`
entry that `only` excludes or `ignore` removes, is a `provider_policy_conflict`.
An unknown key or an out-of-range value, such as `max_attempts: 4`, is a
`400 invalid_request`. The `provider` object is never forwarded to the upstream
provider.
> **Ticket flow: bind the same policy**
>
> In the [private ticket flow](/authentication), send the same `provider` value
> in the ticket request (`POST /v1/inference/tickets`) and in the completion
> body. The ticket binds the policy, and a body whose policy differs is refused
> with `409 ticket_provider_policy_mismatch` before any provider work. In
> [compatibility mode](/compatibility) you send it in the body only.
## How Auto picks a provider
Provider selection first applies availability, workspace boundaries, provider
restrictions, required capabilities, and price ceilings. E2EE routes serve only
E2EE requests; a plaintext request never selects one.
The sort then decides how the remaining providers are ordered:
| Sort | Selection |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **Default** (workspace setting) | Weighs privacy, expected cost, measured speed, and reliability within your boundaries. See [the Default sort](#the-default-sort). |
| `privacy` | Keeps the strongest eligible tier, prefers healthy routes, then lower prices. |
| `price` | Lowest price within the privacy floor. |
| `latency` | Lowest measured time to first token over the last 24 hours. |
| `throughput` | Highest measured output tokens per second over the last 24 hours. |
Latency and throughput sorts use health and price when there are fewer than 20
recent measurements. An explicit non-privacy sort without `minimum_privacy`
uses Private as its floor. For [free models](/models#free-models), the account's
free-model opt-in supplies Anonymous consent.
When you omit the entire `provider` field, the workspace sort applies. A request
can explicitly select `privacy`, `price`, `latency`, or `throughput`; it cannot
send `"sort": "default"`. An exact provider pin and an explicit `order` retain
their meaning under every sort.
Context estimates help choose among eligible routes, but an estimate alone does
not reject ordinary plaintext chat. The provider determines whether the prompt
fits. Intentional gateway caps, including free-route limits, are still enforced.
AnonRouter preserves an explicit output cap and does not delete or summarize
your prompt to make it fit. See [context limits and output budgets](/context-limits)
for defaults, hard caps, and context errors.
Recently failed routes move behind healthy ones within the applicable ordering.
Under `privacy`, the privacy tier still comes first.
## Automatic failover
If the provider serving your request fails **before any output reaches you**,
AnonRouter tries the next route in the plan, up to 3 providers in total. You get
one response, from whichever provider succeeded. Before any output means: for a
non-streaming request, before the JSON body is sent; for a streaming request,
before the response headers are sent.
What happens depends on what the provider returned:
| Provider result | Tries the next provider? | Recently-failed penalty |
| ------------------------------------------------------------------------ | ------------------------ | ---------------------------- |
| Rate limited (`429`) | Yes | That route |
| Server error (`5xx`) | Yes | That route |
| Timeout | Yes | That route |
| Network failure before any output | Yes | That route |
| Malformed or incomplete response before any output | Yes | That route |
| Model unavailable (`404`) | Yes | That route |
| Provider authentication failure (`401`) | Yes | Every route of that provider |
| Provider quota exhausted (`402`) | Yes | Every route of that provider |
| Invalid request (`400`) | **No** | None |
| Other `4xx` (`403`, `409`, `413`, `422`, ...) | Yes | None |
| Content-policy refusal | **No** | None |
| Recognized context or output-limit error (even inside a `5xx` or stream) | **No** | None |
A `401` or `402` from a provider is about AnonRouter's own account with that
provider, never about your request or your key, so the whole provider is
penalized and the request moves on.
A `400` means the request itself is invalid for that model. Sending it to
another provider would show your prompt to one more party, almost always for the
same answer, so AnonRouter stops. You receive the provider's error as a
sanitized `400` (raw upstream error bodies are never returned). If the `400`
comes from a later attempt, after a fallback already happened, you receive
`503 provider_fallback_exhausted`.
A `400` or a refusal says nothing about the provider's health, so neither one
penalizes the route. Recognized context errors also stop without a provider
penalty or a context retry. See [context limits](/context-limits).
## The Default sort
New workspaces start with the **Default** provider sort, a blend that leans
private. Workspaces created earlier keep the sort they had (Privacy unless you
changed it), and you can switch on the Routing page.
1. Your privacy boundary, disabled providers, fallback floor and price ceilings
apply first, as always.
2. It prefers the more private tier. A TEE provider is chosen over a Private one
unless it costs more than 1.5 times as much, is more than twice as slow, or
keeps failing. An Anonymous provider is used only when every more private
provider has a price more than 3 times as high, is more than 3 times as
slow, or keeps failing, or when the model has no more private provider and
your privacy boundary allows Anonymous.
3. Within a tier it weighs the expected cost of a typical request (counting
cached-input pricing), measured speed and success rate over the last 24
hours. Providers with fewer than 20 recent requests are treated as average,
never guessed.
4. Providers that score within 10% of the best share traffic, which also helps
avoid rate limits on any one provider.
5. Fallback follows the same order and never steps below the first provider's
tier unless your fallback floor allows it.
The measurements aggregate traffic by provider route and model. They contain no
prompt content and nothing about your account. AnonRouter does not send a
conversation back to the same provider to improve cache hits, because that
would mean linking your requests.
For now Default is a workspace setting only; a request cannot send
`"sort": "default"`.
## Recently failed providers go last
When a route fails with one of the penalized results above, AnonRouter tries it
last for a while:
* 30 seconds after the first failure;
* each further failure within 10 minutes doubles it: 60, 120, 240 seconds, up
to 5 minutes;
* the count resets 10 minutes after the last failure.
During that time the route is tried after every healthy route, under every
`sort`. It is **never excluded**: if every route for the model is in this state,
they are tried in their normal order, so a model with a single route keeps
working.
The penalty never crosses a privacy boundary. It only reorders routes that
already passed your privacy floor and every other filter. Under the default
`privacy` sort, a recently failed route is tried after the healthy routes of its
own tier and still ahead of any weaker tier you allowed.
An explicit `order` is honored as written. The penalty reorders only the routes
that Auto or `sort` places. If you need a fixed provider sequence, set `order`
or pin a provider.
The penalty describes the provider, not you. AnonRouter records only the route
or provider, the kind of failure, a strike count, and an expiry. No account,
key, request id, or content.
## When there is no failover
* **After output has started.** Once the JSON body or the stream headers are
sent, a failure is reported to you rather than retried elsewhere.
* **E2EE requests.** The ciphertext and attestation are bound to one enclave. If
that provider fails before output you get
`503 provider_fallback_not_supported_for_e2ee`. Retry with fresh attestation
and a fresh ticket.
* **Below your privacy floor.** Failover never moves to a weaker tier than the
request and your workspace allow, and never below the workspace's privacy
boundary.
* **Disabled or ignored providers.** They are never used, as first choice or as
fallback.
* **An invalid request (`400`)** or a **content-policy refusal**.
* **Exact pins**, `allow_fallbacks: false`, or `max_attempts: 1`.
* **After you cancel the request.**
## Workspace defaults
Each workspace sets its defaults in the dashboard, split across two pages.
Every section says what it applies to.
* **Privacy page:**
* **Privacy boundary.** The weakest privacy tier any request from the
workspace may use: Standard (the default), Private, or Maximum.
See [privacy boundary](/workspaces#privacy-boundary).
* **Provider fallback:**
* **Allow fallback to another provider.** On by default. Off means one
attempt, and the request fails if that provider fails.
* **Retries on another provider.** Up to 1 or up to 2, default up to 2.
Shown while fallback is on. Each retry goes to a different provider for
the same model and follows the same privacy rules as the first attempt.
This is the API's `max_attempts` minus the first attempt.
* **How far fallback may step down.** The lowest tier a fallback may use.
The default, No downgrade, keeps every request at its own tier. Choices
below the privacy boundary are greyed out.
* **What we keep.** A summary of what AnonRouter retains, with a link to the
privacy policy.
* **Routing page:**
* **Auto router.** How [`/auto`](/routing) picks a model: the strategy, which
privacy tiers it may use (Standard, Private by default, or Maximum, never
below the privacy boundary), and which models it may use (all models, only
these, or all except these). Only affects requests to `/auto`.
* **Default provider sort.** Every request that does not send its own
`provider` settings. Default, Privacy, Price, Latency, or Throughput. See
[the Default sort](#the-default-sort).
* **Providers.** Every request. A disabled provider is never used for this
workspace, and a request cannot turn it back on.
The same settings are available with a session through
`GET` and `PUT /v1/routing/preferences?workspace_id=` (fields
`provider_allow_fallbacks`, `provider_max_attempts`,
`provider_minimum_privacy`, `provider_sort`, and `provider_ignore`). Without
`workspace_id` they apply to the Default Workspace. `PUT` replaces the whole
preferences object, so read it first and send it back with your changes.
Workspace defaults and request settings merge **restrictively**: a request can
make routing stricter, never looser.
* Request `only` is intersected with the workspace's allowed providers.
* Request `ignore` is added to the workspace's disabled providers.
* A request cannot lower the workspace's privacy floor or raise its price
ceiling.
* A request can turn fallback off or lower `max_attempts`, but cannot allow more
attempts than the workspace does.
See [workspaces](/workspaces).
## Billing across attempts
Before the first attempt, AnonRouter reserves enough of your balance for the
most expensive attempt your policy permits, so a fallback can never land outside
the amount you authorized. Failed attempts are not charged: AnonRouter absorbs
their cost. You pay only for the attempt that succeeded, at that route's price,
and the rest of the reservation is released. See
[metering and settlement](/metering).
## Response headers
Every completion reports the route that actually served it:
```text
x-anonrouter-provider: venice # provider that served the request
x-anonrouter-routing: exact # "auto" when /auto chose the model
x-anonrouter-provider-attempts: 2 # providers tried for this request
x-anonrouter-provider-fallback: true # whether a fallback provider served it
x-anonrouter-privacy-class: private # tier of the serving route
```
`x-anonrouter-provider` uses public provider ids, so NEAR AI routes report
`near-ai`. `x-anonrouter-privacy-class` is the tier of the route that
served the request: `anonymous`, `private`, `tee`, or `e2ee`. The ticket's
`privacy_class` is the weakest tier your policy could reach, so it never
overstates the guarantee.
## Errors
| Status | `type` | Meaning |
| ------ | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| `400` | `invalid_provider_policy` | The `provider` value is malformed, for example an invalid provider id |
| `400` | `provider_policy_conflict` | Contradictory fields, such as a provider in both `only` and `ignore` |
| `400` | `no_provider_route` | No eligible provider route is available for the model under your policy |
| `403` | `no_provider_route_meets_privacy` | No route meets the privacy floor |
| `403` | `privacy_boundary_not_met` | No route of the model meets the workspace's privacy boundary. Refused before any provider sees the request |
| `400` | `no_provider_route_meets_price` | No route is within `max_price` |
| `409` | `ticket_provider_policy_mismatch` | The body's `provider` differs from the policy the ticket was issued for |
| `503` | `provider_fallback_exhausted` | Every permitted attempt failed before output after a fallback, or a later attempt got a `400` |
| `503` | `provider_fallback_not_supported_for_e2ee` | An E2EE provider failed before output; E2EE cannot switch providers |
When only one provider was tried, you receive that provider's own sanitized
error instead of `provider_fallback_exhausted`.
## Examples
These use a [compatibility-mode](/compatibility) key. In the ticket flow, put
the same `provider` value in the ticket request as well.
```bash
# Auto: strongest privacy, healthy providers first, then lowest price.
curl -i https://api.anonrouter.ai/v1/chat/completions \
-H "Authorization: Bearer $ANONROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek/deepseek-v3.2",
"messages": [{ "role": "user", "content": "Hi" }]
}'
# Exact pin: one provider, no fallback.
curl https://api.anonrouter.ai/v1/chat/completions \
-H "Authorization: Bearer $ANONROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek/deepseek-v3.2",
"provider": "venice",
"messages": [{ "role": "user", "content": "Hi" }]
}'
# Cheapest route at or above Private, at most two providers.
curl https://api.anonrouter.ai/v1/chat/completions \
-H "Authorization: Bearer $ANONROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek/deepseek-v3.2",
"provider": { "sort": "price", "minimum_privacy": "private", "max_attempts": 2 },
"messages": [{ "role": "user", "content": "Hi" }]
}'
```
```typescript
const response = await fetch("https://api.anonrouter.ai/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ANONROUTER_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "deepseek/deepseek-v3.2",
// Try DeepInfra first, then Venice, then anything else Auto allows.
provider: { order: ["deepinfra", "venice"], allow_fallbacks: true },
messages: [{ role: "user", content: "Hello" }],
}),
});
console.log(response.headers.get("x-anonrouter-provider"));
console.log(response.headers.get("x-anonrouter-provider-fallback"));
```
```python
import os
import httpx
response = httpx.post(
"https://api.anonrouter.ai/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['ANONROUTER_API_KEY']}"},
json={
"model": "deepseek/deepseek-v3.2",
# Only these two providers, fastest first.
"provider": {"only": ["venice", "deepinfra"], "sort": "throughput"},
"messages": [{"role": "user", "content": "Hello"}],
},
)
print(response.headers.get("x-anonrouter-provider"))
print(response.headers.get("x-anonrouter-provider-attempts"))
```
A provider policy can also constrain [`/auto`](/routing). The model is chosen
only from models with an eligible route under the policy, and provider selection
then runs for the chosen model:
```json
{
"model": "/auto",
"provider": { "only": ["venice"], "minimum_privacy": "private" },
"messages": [{ "role": "user", "content": "Hello" }]
}
```
---
# Context limits and output budgets
Understand context windows, explicit output caps, provider refusals, and safe retries.
A model's context window describes the tokens it can process in a request,
including input and generated output according to the provider's rules.
Different routes of the same model can have different limits. Read
`context_window`, `max_output_tokens`, and capabilities from the
[model catalog](/models), rather than assuming a model name implies a limit.
## Your prompt is forwarded without shortening
AnonRouter does not automatically delete messages, summarize your conversation,
or truncate your prompt to make it fit. For ordinary plaintext chat, token
estimates help order eligible routes and choose an omitted output budget;
an estimate alone does not prove that the provider can or cannot accept a
request. The provider applies its tokenizer and context rules.
This applies to compatibility mode, the private ticket flow, browser chat, and
automatic routing, including plaintext TEE routes. E2EE and non-chat endpoints
have their own request contracts. Request-size, capability, balance, and rate
limits continue to apply independently of context fit.
> **Some gateway limits are hard caps**
>
> Free-model allowances, sponsored-offer limits, and deliberately capped
> provider routes are enforced before dispatch. Such a request can be refused
> with `context_too_large` before it reaches a provider. A larger underlying
> model window does not override the limit of the route you selected.
## Set an output budget
Use `max_tokens` or `max_completion_tokens` to set an explicit output maximum.
AnonRouter preserves the chosen cap when forwarding it, with the parameter
translation required by the provider. An unsupported cap is an error; the
gateway does not silently replace it with a smaller explicit cap.
A maximum is not a promise that the model will produce that many tokens. The
provider can stop earlier, and its context or reasoning rules can leave less
room for visible output. Some providers count reasoning tokens against the
completion budget; see [reasoning controls](/reasoning).
```json
{
"model": "",
"max_completion_tokens": 2048,
"messages": [
{ "role": "user", "content": "Explain this design." }
]
}
```
In the private ticket flow, bind the same model and output limit when issuing
the ticket. Follow the [quickstart](/quickstart) for the ticket request and
completion pair.
### If you omit the output limit
Ordinary chat uses a finite route-specific default: normally up to 8,192
tokens, or 16,384 for configured reasoning behavior. Known output maxima and
estimated remaining context can lower an automatic budget. Free requests use
their own smaller default of at most **1,024 tokens**.
Set an explicit cap when your application needs a predictable maximum. The
temporary [balance hold](/metering) can be larger than the eventual charge;
leaving the cap out does not mean unlimited output or an unbounded request.
## Routing and fallback
Context handling does not override model pinning, provider order, price
ceilings, workspace settings, or your privacy boundary. A request that names
only one provider cannot escape that pin to find a larger window elsewhere.
The gateway's routing estimate is not an exact token count or a guarantee of
fit.
A recognized provider context or output-cap refusal ends that request. It does
not automatically retry the prompt with another provider. Other eligible
pre-output failures can use the normal [provider-fallback rules](/provider-routing).
After output has begun, AnonRouter does not switch providers.
## Errors and retries
| Code | Meaning | Next step |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `context_too_large` | A hard gateway cap or the provider's context limit was exceeded. | Remove unnecessary input, lower the output budget where relevant, or explicitly choose a suitable larger-context route. |
| `max_output_tokens_exceeds_model_limit` | The requested output cap exceeds what the route accepts. | Use an output cap supported by that route. |
| `context_refusal_cooldown` | Repeated provider-confirmed context refusals have temporarily paused requests for the account and model or route. | Wait for the indicated cooldown and change the request before retrying. |
Recognized context errors are treated as request-limit failures, rather than
evidence that the provider is unhealthy. An unrecognized upstream failure can
still appear as a provider error. Disable automatic SDK retries and handle
errors deliberately; see [compatibility mode](/compatibility).
### Why did the response finish with `length`?
`finish_reason: "length"` can mean the output cap or a known context boundary
was reached. When a provider reports `stop` but its own token counts establish
that a known boundary was reached, AnonRouter can correct the reason to
`length`. Missing or estimated usage does not establish that correction.
Neither a `stop` reason nor reported token counts independently prove that an
upstream processed every part of a prompt. AnonRouter's guarantee here is that
it does not shorten the prompt itself.
## Long contexts can have different prices
Some routes charge higher rates when provider-reported prompt tokens cross a
threshold. Those rates can apply to the **whole request**, not just tokens
beyond the threshold. The temporary hold can use a higher tier than the final
charge because fit estimates and billing exposure are different questions.
Read [long-context pricing and settlement](/metering#long-context-pricing)
before sending large requests, and check the selected route's current prices.
---
# Reasoning controls
Set reasoning effort per request, bound to the ticket and validated against each model.
Some models can spend extra effort reasoning before they answer. AnonRouter
exposes a single, provider-neutral way to control that effort, and it validates
your choice against what the target model actually attests.
## Setting effort
Send one of two request fields on both the ticket and the completion:
* `reasoning_effort`: a string, one of `minimal`, `low`, `medium`, `high`,
`xhigh`, `max`, or `none` to turn reasoning off.
* `reasoning`: an object `{ "enabled": boolean, "effort": "..." }` for the same
control in structured form.
```json
{
"model": "deepseek/deepseek-v3.2",
"max_completion_tokens": 512,
"reasoning_effort": "high"
}
```
Omitting both fields uses the model's own default. Setting `reasoning_effort`
to `none` (or `reasoning.enabled: false`) disables reasoning where the model
allows it.
> **Check the model first**
>
> Reasoning fields only apply to models that attest reasoning support. Read
> `capabilities.reasoning` from `GET /v1/models` to confirm `supported`,
> `effort_configurable`, and the `supported_efforts` list before sending an
> effort.
## Reasoning is bound to the ticket
The reasoning selection is part of what a ticket authorizes. Set the same
reasoning fields when you issue the ticket and when you send the completion. If
they differ, the request is rejected with `ticket_reasoning_mismatch` so a
redemption cannot quietly change the reasoning behavior you paid for.
```bash
# Issue a ticket that authorizes high-effort reasoning.
curl "https://control.anonrouter.ai/v1/inference/tickets" \
-H "Authorization: Bearer $ANONROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek/deepseek-v3.2",
"max_completion_tokens": 512,
"reasoning_effort": "high"
}'
```
## Validation
AnonRouter validates the selection against the model's attested capabilities and
returns a `400` when it does not fit:
| Code | Meaning |
| ----------------------------------- | ----------------------------------------------------------------------------------------- |
| `reasoning_conflict` | The flat and structured fields disagree, or an effort was combined with a disable signal. |
| `reasoning_not_supported` | An effort was requested on a model that does not reason. |
| `reasoning_effort_not_configurable` | The model reasons but does not expose a configurable effort. |
| `reasoning_effort_unsupported` | The requested level is not in the model's supported efforts. |
| `reasoning_not_disableable` | Reasoning was disabled on a model that always reasons. |
The effort is passed to the model as a normalized field. AnonRouter does not add
any provider-specific reasoning knobs beyond the controls above.
---
# How privacy works
How AnonRouter separates identity from content, runs the content plane in an attested confidential VM, and where compatibility mode changes the guarantee.
Most model gateways authenticate you and forward your prompt from the same
place. That one system can associate your identity with your content. AnonRouter
offers a private ticket flow that separates those roles before the request
leaves your client, and it never intentionally writes prompts or responses to
disk.
This page is the whole picture in one place: the idea, the ticket that makes it
work, the request lifecycle, the guarantees, and where those guarantees stop.
## The core idea: separate who from what
A private router is not about promising to be careful with your data. It is
about arranging the system so the sensitive join never exists in the first
place.
In the private ticket flow, AnonRouter splits a request across roles that each
hold only one half of it:
* The **control plane** knows **who you are** (your key, balance, and policy).
It never receives your prompt.
* The **relay** sees **what you sent** (the prompt and response) but never learns
your key or account.
* The **provider worker** holds the **provider credential** and the authorized
payload, but never sees your account.
Because identity lives on one side and content on the other, neither side can
say who sent which prompt. The single-use ticket is what lets them cooperate
without ever introducing you to each other.
### The content side runs in a confidential VM
Separating the roles answers *"can any one component link you to a prompt?"* It
does not by itself answer *"what stops the content side from keeping the prompt
anyway?"*
The relay, compatibility broker, and provider workers run inside an **Intel TDX
confidential VM (CVM)**. TLS for `api.anonrouter.ai` terminates inside that CVM,
not at an ordinary proxy in front of it. The host operator cannot inspect the
running VM's memory to read prompts.
The part that matters is that this is **checkable rather than promised**: the
plane will attest to itself, credential-free, to anyone who asks.
```bash
NONCE=$(openssl rand -hex 32)
curl -s "https://api.anonrouter.ai/v1/gateway/attestation?nonce=$NONCE"
```
See [verify AnonRouter itself](/tee-verification#verify-anonrouter-itself-hop-1)
for what that document proves and how to check it without writing a verifier.
On a narrow screen, scroll the diagram horizontally to follow the control plane, confidential content plane, and provider boundary.
## What a ticket is
A ticket is a short-lived, single-use authorization that stands in for your
identity. You spend your key once, with the control plane, to obtain a ticket;
you then hand the ticket (not your key) to the relay that actually serves the
request.
Every ticket is:
* **Opaque.** It carries no readable account information.
* **Single-use.** It is redeemed exactly once and then rejected forever.
* **Short-lived.** It expires in roughly 30 seconds, so a leaked ticket has
almost no window.
* **Request-bound.** It is minted for a specific model, a specific output-token
limit, and a digest of the request's routing and reasoning policy. A ticket
cannot be replayed against a different or larger request than the one it was
issued for.
## The lifecycle of a request
1. **Authenticate once, privately.** Your client calls the control plane at
`POST /v1/inference/tickets` with your inference key, naming the model and
output limit. The control plane verifies the key and confirms you can afford
the request. See [authentication](/authentication) and
[the quickstart](/quickstart) for the exact calls.
2. **Reserve, then mint.** The control plane reserves the **worst-case** cost of
the request (the most it could cost at the requested output limit) against
your balance, then mints a single-use ticket bound to that request. This is
the last point at which your identity and the request meet.
3. **Redeem at the relay.** Your client sends the completion to the relay with
the ticket in the `x-anonrouter-ticket` header. The relay redeems the ticket
once, checks that the request matches what the ticket was bound to, and never
receives your key or account.
4. **Fence each provider attempt.** Before any provider call, the attempt is
fenced with its own single-use dispatch authorization. If the provider fails
before any output, [failover](/provider-routing#automatic-failover) may
authorize one more attempt on a different provider, but only within the plan
the ticket bound: within the workspace's retry limit (never more than 3
attempts in all), never below the privacy floor, within the amount already
reserved, and never for an E2EE request. A ticket cannot fan out beyond that
bound, and a replayed or altered attempt is refused. Each fallback does send
the request to one more provider, which is why a workspace can limit retries
or turn fallback off.
5. **Serve.** The provider worker, holding only the provider credential and the
authorized payload, calls the model provider and streams the response back
through the relay to you.
6. **Settle.** Billing is finalized from usage metadata (token counts, status,
latency) only. The reserved amount is settled to the real cost and the
remainder released. Content is never read to bill. See
[metering and settlement](/metering).
## Security properties
| Property | How it is enforced |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Unlinkability | Identity is held only by the control plane; content is held only by the relay. Neither can join the two, so no record maps you to a prompt. |
| Single use and replay safety | A ticket is redeemed exactly once and then permanently rejected. A replayed or expired ticket is refused before any provider work. |
| Request binding | The ticket is bound to the model, the output-token limit, and a policy digest. It cannot be upgraded to a bigger or different request after issuance. |
| Worst-case reservation | Funds are held for the maximum the request could cost before the first attempt, so serving never needs to consult your balance or identity again. |
| No content at rest | Prompts, responses, system prompts, files, tool arguments, and raw provider bodies are never written to disk. Keys are stored only as a hash. |
| Confidential execution | The relay, the compatibility broker and every provider worker run inside an Intel TDX confidential VM that terminates TLS in-enclave, so content never reaches a host an operator can read it off. [Verifiable on demand](/tee-verification#verify-anonrouter-itself-hop-1), credential-free. |
| Fail closed | If a privacy or authorization check cannot be satisfied, the request is refused rather than served on a weaker footing. |
## What is and is not retained
Operational records may contain account and credential identifiers, provider and
model identifiers, token counts, estimated and final cost, request status and
latency, request identifiers and timestamps, and billing, security, and
rate-limit metadata.
AnonRouter does not intentionally persist prompt or response text, system
prompts, uploaded file contents, tool arguments or outputs, raw provider request
or response bodies, or API keys, session cookies, and authorization headers. API
keys are shown once at creation and kept only as a hash.
## Privacy classifications
Separating identity from content protects you at the router. It does not change
what the model provider on the other end can do with the prompt while serving
it. That is what the per-route privacy label tells you. Every route carries
exactly one classification, and automatic routing never silently widens the set
you allowed.
| Label | `privacy_class` | What it means |
| --------- | --------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Anonymous | `anonymous` | Your identity is hidden, but the model provider can see the prompt while serving it. Provider retention is not guaranteed. |
| Private | `private` | Prompt and response content is not retained after the request completes. |
| TEE | `tee` | Inference runs inside a hardware-isolated, attestable enclave. |
| E2EE | `e2ee` | Your client encrypts the prompt so that only a verified enclave can decrypt it. |
> **Two layers, read both**
>
> The ticket flow decides whether AnonRouter can link you to a request. The
> privacy label decides what the provider serving the request can see. A private
> request over an Anonymous route is still unlinkable at the router, but the
> provider can read the prompt. See [models and privacy labels](/models) for the
> label on each route, the [E2EE client guide](/e2ee) for encrypted request
> setup, and [TEE verification and encrypted inference](/tee-verification) for
> enclave evidence, verification levels, and signatures.
## Compatibility mode: an explicit trade-off
Some clients cannot perform the two-step ticket exchange. [Compatibility
mode](/compatibility) lets those clients send a static key straight to the API
with no ticket. To do that, a broker must hold your key identity and your
plaintext request together in memory long enough to mint a ticket internally and
forward the request.
That broker logs metadata only and holds no database or provider credentials,
but this flow is **not unlinkable** the way the ticket flow is, because your
identity and your content briefly meet in one place. OpenAI-compatible access is
enabled by default on newly created keys so ordinary tools work immediately. You
can turn it off while creating a key when that key will use only the private
ticket flow.
## How this is different from a typical router
A conventional gateway is a trusted middleman: it authenticates you and
forwards your prompt from the same service,
so it can always tie your account to your content, and you rely on its policy
not to keep that link.
In AnonRouter's private ticket flow, the link is not merely unlogged, it is
architecturally absent: the component that knows you never sees your prompt,
and the component that sees your prompt never knows you. The ticket is the
mechanism that lets those two halves serve one request without ever being
introduced.
And the half that does see your prompt runs inside a confidential VM that will
prove what it is running, to anyone, without a credential. So "we do not keep
your prompts" is not a policy you have to believe. It is a property you can
check, and one we would have to visibly break to violate.
> **Two things this does not claim**
>
> **The provider still sees your prompt** on an ordinary route. Our worker
> forwards it upstream, where it is handled under that provider's policy. The
> [privacy label on each route](/models) is what tells you which, and an
> [E2EE route](/e2ee) is what avoids it.
>
> **Inside the enclave, our attested build handles your plaintext** in order to
> route and meter it. A build changed to exfiltrate it would change the
> measurements and stop verifying, so cheating is *detectable* rather than
> impossible. E2EE is the mode where our build is not in your trust set at all.
> **Try it**
>
> Walk the full flow yourself in [the quickstart](/quickstart), or read the
> per-route guarantees in [models and privacy labels](/models).
---
# End-to-end encrypted inference
Attest an enclave, encrypt locally, relay provider-bound ciphertext through AnonRouter, and decrypt the response in your client.
Use E2EE when your application must keep prompts and model output opaque to
AnonRouter. Your client verifies fresh enclave evidence, encrypts to the key in
that evidence, and sends only ciphertext plus fixed routing metadata. Plaintext
is recovered only by the selected model enclave and your client.
> **E2EE and hardware verification are different claims**
>
> A working encrypted transport proves that AnonRouter relayed ciphertext; the
> attestation level says how strongly the destination key was verified. Every
> E2EE transport on this page reaches `provider-attested`, not
> `hardware-verified`. Do not encrypt unless the evidence, nonce, route,
> measurements, and encryption key all pass the checks your policy requires.
## Provider support
| Provider | AnonRouter E2EE transport | Current attestation level | Streaming | Public request receipt |
| -------------- | -------------------------------------------- | -------------------------- | -------------- | --------------------------------------- |
| Chutes | ML-KEM-768 whole-body ciphertext | `provider-attested` | No | Provider does not expose one |
| Venice | secp256k1/AES-GCM field ciphertext | `provider-attested` | Yes | Yes, including completed streams |
| NEAR AI direct | X25519/XChaCha20 field ciphertext, version 2 | `provider-attested` | Yes | Not for the current streaming E2EE call |
| Tinfoil | Not exposed through AnonRouter | TEE reaches `sdk-verified` | Not applicable | Provider does not expose one |
> **Only Venice has callable E2EE routes right now**
>
> Venice is the only E2EE provider the live catalog publishes as callable. The
> Chutes and NEAR AI protocols below are implemented and documented, but neither
> has a callable route in the catalog right now, so treat every Chutes and NEAR
> example here as a wire-format reference. Read
> [`GET /v1/catalog/models`](/models#public-catalog) before choosing a provider.
> **E2EE names the enclave that decrypts, not the machine that infers**
>
> Your prompt is readable only by the enclave you verified and encrypted to. On
> some Venice routes that enclave is a **verified gateway rather than the model
> host**: it decrypts and forwards the plaintext onward inside the provider. The
> attestation is genuine and AnonRouter still holds only ciphertext, but the
> route's `privacyNotes` are what tell you where decryption actually happens.
Tinfoil's upstream EHBP transport encrypts the model selector. Its documented
outer metadata does not let AnonRouter prove that the encrypted model is the
one bound to the ticket and reservation. AnonRouter therefore fails closed
instead of advertising Tinfoil as E2EE.
## The safe request sequence
Every E2EE request follows the same security order:
1. Select one exact canonical model and provider route. E2EE never uses Auto.
2. Generate fresh client key material and a fresh 32-byte attestation nonce.
3. Fetch enclave evidence through the appropriate flow below.
4. Verify the raw evidence independently, including its nonce, endpoint,
measurements, debug state, and encryption-key binding. Enforce the minimum
`verification_level` your application accepts.
5. Mint a short-lived inference ticket for that same model and provider with
`e2ee: true`.
6. Encrypt locally and send the ticket, fixed E2EE headers, and ciphertext to
the relay. Do not send your API key to the relay in a split deployment.
7. Decrypt the response locally. If a request fails, repeat from step 2 with
fresh evidence, key material, nonces, and tickets.
AnonRouter never falls back an E2EE request to another provider, endpoint, or
plaintext route. That is essential: a key attested for one enclave cannot be
safely reused for a different destination.
The examples use separate control and confidential API origins:
```bash
export ANONROUTER_API_URL="https://api.anonrouter.ai/v1"
export ANONROUTER_CONTROL_URL="https://control.anonrouter.ai/v1"
export ANONROUTER_API_KEY="ar_REPLACE_WITH_YOUR_API_KEY"
```
## 1. Select an exact E2EE route
Read the live catalog before encrypting:
```bash
curl "$ANONROUTER_CONTROL_URL/models" \
-H "Authorization: Bearer $ANONROUTER_API_KEY"
```
Choose a route whose `privacy_class` is `e2ee`, and retain its canonical model,
provider, context window, and maximum output. Use the canonical model in the
ticket and public chat request. Never infer E2EE support from a `-TEE` suffix.
## 2. Fetch and verify attestation
### Public verified attestation
Clients can fetch a normalized server verdict together with the raw provider
evidence:
```bash
NONCE="$(openssl rand -hex 32)"
curl --get "$ANONROUTER_CONTROL_URL/tee/attestation" \
-H "Authorization: Bearer $ANONROUTER_API_KEY" \
--data-urlencode "model=qwen/qwen-2.5-7b" \
--data-urlencode "provider=venice" \
--data-urlencode "nonce=$NONCE"
```
The response binds `model`, `provider`, `route_id`, `privacy_class`, the
normalized `attestation` result, and the raw `evidence`. Require at least:
```text
attestation.status == "ok"
attestation.privacy_modality == "e2ee"
attestation.supports_client_opaque_e2ee == true
attestation.nonce == the nonce your client generated
attestation.verification_level is accepted by your policy
every required attestation.checks item passed
```
The normalized result is AnonRouter's verification verdict. A client claiming
independent verification must also run the provider and silicon-vendor checks
over `evidence`; merely reading the normalized fields is not independent
verification. See [TEE verification](/tee-verification) for the checks and
limits of each level.
### Attestation tickets
The call above sends your API key to the control origin. When you would rather
not present a credential to the origin that will carry content either, mint a
single-use, model-bound **attestation ticket** on control and redeem it on the
confidential origin:
```bash
ATTESTATION_TICKET="$(curl -fsS \
"$ANONROUTER_CONTROL_URL/inference/attestation-tickets" \
-H "Authorization: Bearer $ANONROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"qwen/qwen-2.5-7b","provider":"venice"}' \
| jq -r .ticket)"
NONCE="$(openssl rand -hex 32)"
curl -fsS "$ANONROUTER_API_URL/tee/attestation" \
-H "X-AnonRouter-Ticket: $ATTESTATION_TICKET" \
-H "Content-Type: application/json" \
-d "{\"nonce\":\"$NONCE\"}"
```
This relay returns `{ "evidence": ... }` without interpreting it. Independently
verify the provider's TDX and NVIDIA evidence, the endpoint, measurements,
nonce, and the encryption-key commitment before using the model key. The ticket
is short-lived, single-use, and valid only for the selected route.
NEAR's direct E2EE key uses the same ticket flow with an Ed25519 encryption-key
commitment. It is documented here for completeness; no NEAR route is callable in
the current catalog.
Do not substitute `X-Model-Pub-Key` from an unverified response. That header is
for NEAR's cloud gateway and is deliberately rejected on AnonRouter's direct
path.
## 3. Mint the inference ticket
After attestation passes, mint a separate inference ticket:
```bash
INFERENCE_TICKET="$(curl -fsS \
"$ANONROUTER_CONTROL_URL/inference/tickets" \
-H "Authorization: Bearer $ANONROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "qwen/qwen-2.5-7b",
"provider": "venice",
"e2ee": true,
"operation": "chat",
"max_tokens": 256
}' | jq -r .ticket)"
```
The ticket and encrypted request must name the same model, provider, operation,
reasoning controls, and output limit. A ticket expires quickly and can be
redeemed only once.
For Chutes whole-body E2EE, set `max_tokens` to the route's complete published
maximum. Token usage is itself encrypted, so AnonRouter reserves and captures
the full conservative context ceiling rather than estimating plaintext from
ciphertext length.
## NEAR AI version 2
NEAR encrypts each text field separately:
1. Convert the attested model Ed25519 public key to X25519.
2. Generate a fresh ephemeral X25519 key for every encrypted field.
3. Derive the shared secret with X25519 ECDH.
4. Derive 32 bytes with HKDF-SHA256, no salt, and
`info = "ed25519_encryption"`.
5. Encrypt UTF-8 plaintext with XChaCha20-Poly1305 and a fresh 24-byte nonce.
6. Lowercase-hex encode this wire value:
```text
[ephemeral X25519 public key: 32 bytes]
[XChaCha20-Poly1305 nonce: 24 bytes]
[ciphertext and 16-byte authentication tag]
```
Generate an Ed25519 client keypair and retain the private key only in your
client. Send the public key and encrypted message fields:
```bash
curl -N "$ANONROUTER_API_URL/chat/completions" \
-H "X-AnonRouter-Ticket: $INFERENCE_TICKET" \
-H "X-AnonRouter-E2EE-Provider: near-ai" \
-H "X-Signing-Algo: ed25519" \
-H "X-Client-Pub-Key: $CLIENT_ED25519_PUBLIC_KEY_HEX" \
-H "X-Encryption-Version: 2" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"openai/gpt-oss-120b\",
\"messages\": [{\"role\":\"user\",\"content\":\"$ENCRYPTED_FIELD_HEX\"}],
\"max_tokens\": 256,
\"stream\": true
}"
```
Decrypt encrypted `content` or `reasoning_content` values in the SSE response
with the retained client Ed25519 private key, converted to X25519. Reject a
field that is too short, malformed, or fails Poly1305 authentication.
AnonRouter validates only the fixed header vocabulary and ciphertext shape. It
does not receive a private key or parse the encrypted message as plaintext.
The direct enclave domain must exactly match the reviewed route; a missing or
mismatched discovery result fails closed.
See NEAR's [E2EE chat-completions guide](https://docs.near.ai/cloud/guides/e2ee-chat-completions/)
and [model verification guide](https://docs.near.ai/cloud/verification/model/)
for the independent client verifier.
## Chutes ML-KEM whole-body transport
Chutes has no callable route in the current catalog, so this section is a
wire-format reference.
The verified Chutes evidence contains `e2e_instances`. Select an instance only
after verifying its UUID, ML-KEM-768 public key, short-lived nonce set, and
`SHA256(caller nonce || ML-KEM public key)` commitment in TDX report data.
Build the request with Chutes' transport:
1. Generate an ML-KEM-768 response keypair.
2. Add its base64 public key as `e2e_response_pk` inside the JSON request.
3. Gzip the JSON.
4. Encapsulate to the attested instance's ML-KEM-768 public key.
5. Derive the request key with HKDF-SHA256. The salt is the first 16 bytes of
the ML-KEM ciphertext and `info = "e2e-req-v1"`.
6. Encrypt with ChaCha20-Poly1305 using a fresh 12-byte nonce.
7. Concatenate `[ML-KEM ciphertext: 1088][nonce: 12][ciphertext + tag]`.
The encrypted inner request uses the provider model identifier, such as
`Qwen/Qwen3-32B-TEE`; the outer ticket stays bound to the canonical
`qwen/qwen3-32b` route. Relay the bytes without JSON or base64 wrapping:
```bash
curl "$ANONROUTER_API_URL/e2ee/chat/completions" \
-H "Content-Type: application/octet-stream" \
-H "X-AnonRouter-Ticket: $INFERENCE_TICKET" \
-H "X-AnonRouter-E2EE-Provider: chutes" \
-H "X-Chutes-Instance-Id: $ATTESTED_INSTANCE_ID" \
-H "X-Chutes-E2E-Nonce: $SINGLE_USE_NONCE" \
--data-binary @encrypted-request.bin \
--output encrypted-response.bin
```
Decapsulate the first 1088 response bytes with the retained response secret
key. Derive the response key with the first 16 ciphertext bytes as salt and
`info = "e2e-resp-v1"`, authenticate/decrypt with ChaCha20-Poly1305, then
gunzip and parse the JSON locally.
The endpoint is non-streaming. Use each Chutes nonce once and retry only after
fetching fresh evidence. The official
[Chutes E2EE transport](https://github.com/chutesai/chutes-e2ee-transport)
repository is the reference implementation for framing and crypto.
## Venice field encryption
Fetch public verified attestation for the exact Venice E2EE model and use its
`attestation.attested_encryption_key` only after every required check passes.
For each field:
1. Generate an ephemeral secp256k1 keypair.
2. Derive the ECDH shared secret with the attested model public key.
3. Derive 32 bytes with HKDF-SHA256 and `info = "ecdsa_encryption"`.
4. Encrypt with AES-256-GCM and a fresh 12-byte nonce.
5. Hex encode `[uncompressed ephemeral public key: 65][nonce: 12][ciphertext + tag]`.
Send a streaming request with the exact Venice header vocabulary:
```bash
curl -N "$ANONROUTER_API_URL/chat/completions" \
-H "X-AnonRouter-Ticket: $INFERENCE_TICKET" \
-H "X-AnonRouter-E2EE-Provider: venice" \
-H "X-Venice-TEE-Client-Pub-Key: $CLIENT_PUBLIC_KEY_HEX" \
-H "X-Venice-TEE-Model-Pub-Key: $ATTESTED_MODEL_PUBLIC_KEY_HEX" \
-H "X-Venice-TEE-Signing-Algo: ecdsa" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"venice/e2ee-gpt-oss-20b-p\",
\"messages\": [{\"role\":\"user\",\"content\":\"$ENCRYPTED_FIELD_HEX\"}],
\"max_tokens\": 256,
\"stream\": true
}"
```
Decrypt response fields locally with your client private key. Venice requires
streaming for its E2EE transport. Follow Venice's
[TEE and E2EE model guide](https://docs.venice.ai/guides/features/tee-e2ee-models)
for provider-side encryption and attestation details.
After a completed Venice stream, you can retrieve its enclave receipt through
the [public TEE signature service](/tee-verification#retrieve-a-request-receipt).
The receipt proves the provider's signed enclave events; it does not claim that
Venice signed AnonRouter's exact public wire bytes.
## What AnonRouter can and cannot see
| Data | Control plane | Relay | Provider worker | Verified enclave |
| ------------------------- | ------------- | ----------- | ------------------- | ----------------- |
| Account/API key | Yes | No | No | No |
| Single-use ticket | Issues it | Redeems it | Dispatch fence only | No |
| E2EE ciphertext | No | Yes, opaque | Yes, opaque | Decrypts it |
| Plaintext prompt/response | No | No | No | Yes |
| Provider credential | No | No | Yes | Provider-specific |
Applications should avoid logging ciphertext too. Although encrypted, it can
remain sensitive if a client key is later compromised. Raw prompts, responses,
private keys, and decrypted output should never be added to attestation caches
or telemetry.
## Launch limits and failure behavior
* Field E2EE accepts text `user` and `system` messages, requires a positive
explicit output limit, and requires streaming. Tool calls, tool messages,
images, and plaintext message fields are rejected.
* Chutes whole-body E2EE is non-streaming and intended for text chat. The relay
never parses the encrypted JSON to infer capabilities or token usage.
* Automatic routing and transparent provider fallback are disabled. A retry
requires fresh attestation and a fresh inference ticket.
* Reusing an attestation ticket, inference ticket, Chutes nonce, or ephemeral
field key is unsafe and rejected where the protocol exposes that state.
* `tee_signature_not_supported` means the provider has no compatible request
receipt; it does not mean attestation or encrypted inference failed.
* `provider-attested` is an honest current ceiling, not an alias for
`hardware-verified`.
## Production checklist
* Pin reviewed provider measurements, endpoint identities, release digests,
and verifier versions in your client policy.
* Use a cryptographically secure random generator for every nonce and key.
* Compare the attested route and model to the ticket before encryption.
* Reject expired evidence, debug-enabled evidence, a failed required check, an
unexpected verification level, or a changed encryption key.
* Keep API keys on your trusted control-side backend and private keys on the
encrypting client.
* Treat AEAD authentication failure as a terminal security error; do not return
partial plaintext.
* Never retry through a weaker privacy class.
For the exact attestation checks, measurement policies, request receipts, and
provider-by-provider limitations, continue with
[TEE verification and encrypted inference](/tee-verification).
---
# TEE verification and encrypted inference
Verify enclave evidence and per-request receipts, understand verification levels, and use the opaque E2EE transports.
AnonRouter exposes provider-neutral enclave attestation and request-signature
services. Provider evidence is fetched by a credential-isolated worker,
verified fail-closed, and returned to you so your client can independently
repeat the verification.
> **TEE is not automatically hardware-verified**
>
> A route's privacy modality and its verification level are separate claims. This
> API never turns a provider boolean, model suffix, or confidential-compute flag
> into silicon proof. Most provider routes (hop 2) stop below
> `hardware-verified`: several run GPU enclaves whose NVIDIA chain is not
> available to verify, and chaining only the CPU quote would claim more than was
> checked. Where a provider route does reach `hardware-verified`, as Phala AI
> routes do, each check says which machine it covers. See
> [Phala AI](#phala-ai).
>
> Hop 1, AnonRouter's own routing plane, is different, and *is* reachable at
> `hardware_verified` with a DCAP engine. See
> [Verify AnonRouter itself](#verify-anonrouter-itself-hop-1).
> **Building an E2EE client?**
>
> Follow the dedicated [E2EE client guide](/e2ee) for the ticket sequence,
> provider wire formats, required headers, retry rules, and production
> checklist. This page remains the attestation and receipt reference.
## Two hops, verified separately
A request passes through two parties, and verifying one tells you nothing about
the other.
| | Question it answers | How to check it |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
| **Hop 1**: AnonRouter's own routing plane | Is the router I am talking to the exact reviewed build, running inside an Intel TDX confidential VM, bound to my nonce and my origin? | `GET /v1/gateway/attestation` (credential-free) |
| **Hop 2**: the downstream provider route | Did the model provider terminate my request inside a verified enclave running measurements I pinned? | the provider attestation and receipt APIs below |
A verified hop 2 says nothing about who routed the request there. A verified
hop 1 says nothing about where inference actually ran. Most of this page is
about hop 2; the section immediately below is hop 1.
## Verify AnonRouter itself (hop 1)
AnonRouter's content plane runs inside an Intel TDX confidential VM. The relay,
the compatibility broker, and **every provider worker** run inside that TD, and
TLS terminates *in-enclave* rather than at a platform gateway in front of it.
This is why the privacy properties on [the privacy page](/privacy) are not just
policy. Prompts do not reach ordinary AnonRouter infrastructure, and no operator
can read them off a running host, and you do not have to take that on trust,
because the plane will attest to itself on demand.
### The endpoint
```bash
NONCE=$(openssl rand -hex 32)
curl -s "https://api.anonrouter.ai/v1/gateway/attestation?nonce=$NONCE"
```
`GET /v1/gateway/attestation` is **credential-free**: a client should be able to
check the plane *before* trusting it with anything, including before it has an
account. The `nonce` is required and must be exactly 64 hex characters (32
bytes); a request without one is a `400`.
The response is a JSON document of roughly 95 KB:
| Field | What it carries |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `binding` | the claims being attested: your `nonce`, `app_id`, `instance_id`, `compose_hash`, `release_id`, `origin`, the plane's `public_key`, `transport`, and `tls_spki_sha256` |
| `binding_hash` | the digest committed into the quote's `report_data` |
| `quote` | the raw Intel TDX quote |
| `event_log` | the boot event log, which replays to the quote's RTMRs |
| `app_compose`, `vm_config` | the measured deployment definition |
| `issued_at_ms` | freshness |
### What a verdict establishes
Verifying that document is what turns "AnonRouter says it runs in an enclave"
into something you checked:
* **the quote is fresh and yours**: your nonce appears inside the quote's
`report_data`, as the digest of the canonical binding, so a recorded response
cannot be replayed at you;
* **TLS terminates inside the TD**: `binding.transport` is `in-tee-tls`, not
`gateway-tls`. There is no platform proxy in front of the enclave holding your
plaintext;
* **the certificate belongs to the attested TD**: the leaf SPKI you observed on
your own connection equals `binding.tls_spki_sha256`. This is the check that
turns "somebody named a certificate" into "the enclave holds the key for *this*
connection";
* **the build is the reviewed one**: `compose_hash`, `app_id`, `release_id` and
the platform measurements match pins you hold locally, not values the server
chose;
* **the event log is consistent**: it replays to the quote's RTMRs.
> **The pins must not come from the server being checked**
>
> A server that could hand you the list of builds you accept could always name
> itself. Hold the plane to measurements you obtained independently. The SDK ships
> reviewed pins inside the package for exactly this reason, and fails rather than
> falling back to whatever the origin claims.
### Checking it without writing a verifier
Both SDK packages install `anonrouter-verify`, which does the whole document and
exits nonzero unless the assurance you asked for held:
```bash
anonrouter-verify gateway --origin https://api.anonrouter.ai \
--dcap --require hardware_verified
echo $? # 0 met, 1 not met, 2 the command was wrong
```
Hop 1 is the one place a `hardware_verified` result is reachable, and only with a
DCAP engine that actually chained the quote's ECDSA signature to Intel's roots
with an accepted TCB status. Without an engine the honest ceiling is
`cryptographically_checked`, and a policy demanding hardware verification fails
closed rather than quietly settling for less. See the
[SDK verification guide](/sdk/confidential) and the
[command reference](/sdk/verify-cli).
> **The pin SDK v0.1.0 ships is older than the running plane**
>
> A shipped pin is a snapshot of a reviewed release, and the content plane has
> been released since `v0.1.0` was tagged. The pin in that version names the
> previous build, so the command above **fails closed** against production today
> on `compose_hash_pinned` and `release_pinned` rather than passing. That is the
> design working (the alternative would be an SDK that accepts whatever the
> origin currently claims), but until a refreshed pin ships, gate on a policy you
> reviewed yourself with `--policy`. [Check the running build's `compose_hash`
> and `release_id`](#the-endpoint) against the release you reviewed.
### What hop 1 does not cover
* **It says nothing about the provider.** That is hop 2, below.
* **It does not make a TEE route end-to-end encrypted.** Inside the enclave, the
relay handles your plaintext to route and meter it. What that buys you is that
a build changed to exfiltrate it would change the measurements and stop
verifying: cheating is *detectable*, not impossible. Only an
[E2EE route](/e2ee) keeps content opaque to AnonRouter's build whatever code it
is running.
* **It says nothing about what the provider does with the prompt.** On an
ordinary route the worker forwards plaintext to the upstream provider, which
sees it under that provider's own policy. Check the
[privacy label on the route](/models).
* **Attestation names the build; it does not review it.** Reading the source
behind a pinned compose hash is a separate act.
## Support matrix
| Provider | Maximum verification | AnonRouter modality | Per-request receipt |
| ------------------------ | ----------------------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------- |
| Chutes confidential text | `provider-attested` | Non-streaming ML-KEM whole-body `e2ee` | Not exposed by the provider; returns `tee_signature_not_supported` |
| Tinfoil | `sdk-verified` | `tee` over TLS/JSON | Not exposed by the provider; returns `tee_signature_not_supported` |
| Venice | `provider-attested` | `e2ee` | EIP-191 enclave receipt for completed non-stream and streaming requests |
| NEAR AI direct | `provider-attested` | Opaque `e2ee`, version 2 | EIP-191 receipt with exact AnonRouter wire-hash binding for completed non-stream requests |
| NEAR AI attested-3p | No direct model-enclave attestation | `tee` | No receipt through the direct flow |
> **NEAR AI has no callable route in the current catalog**
>
> The NEAR verifier and the version 2 opaque protocol below are implemented and
> documented, but the live catalog publishes **no callable NEAR AI route**. Every
> NEAR example on this page is therefore a protocol reference rather than a
> request you can run today. Read
> [`GET /v1/catalog/models`](/models#public-catalog) for what is actually
> callable before pinning a provider.
The confidential routes the catalog publishes as callable today are Phala AI
`tee`, Tinfoil `tee` and Venice `e2ee`. Chutes has no callable route; its rows
below are a protocol reference. The one callable confidential embedding route is
Tinfoil's `nomic-ai/nomic-embed-text`, which is `tee` rather than `e2ee`.
Tinfoil documents EHBP/HPKE transport, but its model selector is encrypted and
the documented outer headers do not bind that model to an AnonRouter ticket or
reservation. AnonRouter fails closed rather than expose a proxy through which a
caller could ticket one model and encrypt a request for another. Tinfoil routes
are therefore not advertised as E2EE.
## Verification levels
| Level | What the result proves |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `hardware-verified` | Quote signatures, certificate and revocation chains, acceptable TCB, GPU evidence, nonce and key bindings, debug state, and pinned measurements all passed to silicon-vendor trust roots. AnonRouter's own plane (hop 1) reaches this level with a DCAP engine. On a provider route, read each check's scope to see which machine it covers. |
| `sdk-verified` | The official provider SDK completed its required cryptographic verification and the result matched AnonRouter's pinned release policy. |
| `provider-attested` | Required structure, freshness, nonce/key, route, certificate-possession, and pinned-measurement checks passed, but the complete silicon-vendor chain was not independently established. |
| `unverified` | A required check, policy value, or evidence item was absent or failed. |
| `unsupported` | The selected provider or route does not expose the requested capability. |
The Intel/NVIDIA chain port remains intentionally unwired. The currently
available path does not provide all collateral, trust-root, revocation, and TCB
handling needed to make the stronger claim. Incomplete evidence fails closed at
`provider-attested` or `unverified`.
## Fetch fresh attestation evidence
Both public TEE endpoints require an API key with the `inference` scope. Use a
fresh random nonce containing 32 to 64 bytes, encoded as hexadecimal:
```bash
curl "https://control.anonrouter.ai/v1/tee/attestation?model=openai/gpt-oss-120b&provider=tinfoil&nonce=$NONCE" \
-H "Authorization: Bearer $ANONROUTER_API_KEY"
```
The same evidence is available from the confidential origin without presenting a
key to it, using an [attestation ticket](/e2ee#attestation-tickets) instead.
The response identifies the exact provider route and privacy modality and
contains:
* `verification_level` and every individual check;
* normalized measurements and public signing identities;
* the measurement-policy version and expiry;
* the exact raw provider evidence for independent client verification.
Attestation results are cached for five minutes without prompts, responses,
private keys, or raw request bodies. A hostname or model name alone never earns
a verification level.
## Retrieve a request receipt
After a supported completion, use the provider's request ID returned with that
completion:
```bash
curl "https://control.anonrouter.ai/v1/tee/signature/$PROVIDER_REQUEST_ID?model=qwen/qwen-2.5-7b&provider=venice" \
-H "Authorization: Bearer $ANONROUTER_API_KEY"
```
Optional `request_hash` and `response_hash` query parameters are additional
client cross-checks. They never replace the content-free hashes calculated and
stored by AnonRouter.
### NEAR exact-wire receipts
For a non-stream request, the credential worker hashes the exact JSON sent to
the direct enclave and the exact raw response before parsing. Receipt lookup
requires an unexpired route binding, a fresh matching attestation, exact hash
equality, and successful EIP-191 recovery to the attested signing address.
For a verified NEAR receipt, `hash_binding_checked: true` means the signed
request and response hashes equal the exact bytes observed by AnonRouter. NEAR
streaming does not yet provide the completed-response binding needed for this
claim and fails with `tee_signature_binding_unavailable`.
### Venice enclave receipts
Venice receipts are recovered with EIP-191 against the secp256k1 key committed
to the earlier TDX attestation. The signed statement, completion ID, model, and
`request.received`/`response.returned` event hashes must agree.
Venice signs an enclave-internal ingress/egress representation rather than the
exact public API wire bytes. A valid receipt therefore normally returns
`verified: true`, `verification_level: "provider-attested"`, and
`hash_binding_checked: false`. The response exposes both `bound_*` provider
hashes and `gateway_*` wire hashes instead of claiming equality Venice did not
sign.
Completed Venice streams are supported. Receipt metadata crosses the worker
boundary in a private sideband that is never emitted as a customer SSE event.
Aborted or incomplete streams produce no signature binding.
> **Unsupported is not a broken TEE route**
>
> Tinfoil inference and public attestation work normally. Tinfoil, like Chutes,
> does not expose a compatible per-completion receipt, so signature lookup returns the
> structured `501 tee_signature_not_supported` response. AnonRouter never
> fabricates a substitute gateway signature.
## NEAR v2 opaque E2EE
NEAR direct routes expose the provider-neutral v2 field-encryption modality.
Request a nonce-bound attestation ticket for the exact direct route, then send
ciphertext with this validated header set:
```text
X-AnonRouter-E2EE-Provider: near-ai
X-Signing-Algo: ed25519
X-Client-Pub-Key: <32-byte Ed25519 public key as 64 lowercase hex chars>
X-Encryption-Version: 2
```
Do not send `X-Model-Pub-Key`. That field belongs to NEAR's cloud gateway;
AnonRouter obtains the model key from the direct enclave's attestation.
For every encrypted field, the client uses ephemeral X25519 ECDH and
HKDF-SHA256 with `info = "ed25519_encryption"`, then encodes this lowercase-hex
wire value:
```text
[ephemeral X25519 public key: 32 bytes]
[XChaCha20-Poly1305 nonce: 24 bytes]
[ciphertext + authentication tag]
```
Encryption and decryption stay client-side. AnonRouter forwards only the
validated headers and opaque ciphertext, does not parse message fields as
plaintext, and never receives either private key.
Endpoint discovery must exactly match the reviewed direct enclave domain.
Missing or mismatched discovery fails with
`e2ee_enclave_endpoint_unavailable`; an E2EE request never falls back to the
cloud gateway, a plaintext route, or another provider.
## Chutes ML-KEM whole-body E2EE
Chutes has no callable route in the current catalog, so this section is a
protocol reference.
Confidential Chutes text routes are ciphertext-only through AnonRouter. First,
use the normal model-bound attestation-ticket flow. The returned raw evidence
contains `e2e_instances`: instance UUIDs, ML-KEM-768 public keys, and short-lived
single-use nonces. Accept a key only after verifying the response and its
`SHA256(caller nonce || ML-KEM key)` TDX report-data commitment.
Encrypt with Chutes' official transport format, then submit the exact bytes:
```bash
curl "https://api.anonrouter.ai/v1/e2ee/chat/completions" \
-X POST \
-H "Content-Type: application/octet-stream" \
-H "X-AnonRouter-Ticket: $INFERENCE_TICKET" \
-H "X-AnonRouter-E2EE-Provider: chutes" \
-H "X-Chutes-Instance-Id: $ATTESTED_INSTANCE_ID" \
-H "X-Chutes-E2E-Nonce: $SINGLE_USE_NONCE" \
--data-binary @encrypted-request.bin \
--output encrypted-response.bin
```
The credential worker resolves the ticket-bound model to its live `chute_id`,
requires `confidential_compute: true`, overwrites the chute, path, and streaming
headers, and adds the Chutes credential. Only the two validated instance/nonce
values cross from the client. Request and response bodies remain byte-for-byte
opaque to AnonRouter, and there is no provider fallback.
This first endpoint is non-streaming. Chutes keeps its non-stream token usage
inside the encrypted response, so the ticket must reserve the model's full output
ceiling. AnonRouter reserves the full context at the most expensive token rate
and conservatively captures it after success; it never estimates plaintext usage
from compressed ciphertext length. The ticket-mint request must therefore set
`max_tokens` to the model's published maximum.
The opt-in live suite performs a real ML-KEM-768 request/response round trip and
decrypts only in the test client; the relay receives only the encrypted blobs.
## What each provider check establishes
### Chutes
AnonRouter checks every returned instance: TDX v4 structure and debug state,
pinned MRTD/RTMR0-3 measurements, the nonce plus ML-KEM key commitment, the
certificate SPKI commitment, RSA possession of the decoded attested body,
inner/outer quote and GPU equality, certificate validity, and complete instance
key discovery. The missing complete Intel/NVIDIA vendor chain keeps the result
at `provider-attested`. For an `e2ee` result, the verified ML-KEM key commitment
also has to pass; the route never claims `hardware-verified`.
### Tinfoil
The credential worker runs the official `tinfoil@1.2.1` verifier. An
`sdk-verified` result requires `securityVerified === true`, all five SDK
verification steps, the selected endpoint, and exact pinned release digest,
code fingerprint, enclave fingerprint, and TLS key. A missing SDK or document
never defaults to success.
### NEAR direct
AnonRouter checks TDX v4 structure and debug state, NVIDIA evidence, the
nonce/key/TLS commitment, exact model and direct endpoint, `mr_config_id`, the
serialized `app_compose` hash, and pinned MRTD/RTMR0-3 measurements for that
model. Current evidence does not independently prove the model-weight digest or
complete silicon chain, so the maximum remains `provider-attested`.
### Venice
AnonRouter checks TDX v4 structure and debug state, caller nonce and model,
NVIDIA evidence, nested report-data equality, and a secp256k1 public key whose
derived address matches the signer committed into TDX report data. Venice's own
`verified` boolean is not accepted as proof.
> **Where a Venice E2EE route actually decrypts**
>
> E2EE guarantees that only the enclave you verified and encrypted to can read
> the prompt. On some Venice routes **that enclave is a verified gateway rather
> than the machine running the model**: it decrypts your request and forwards the
> plaintext onward inside Venice's own infrastructure. The attestation is real
> and AnonRouter still never sees plaintext, but "only the model host can read
> it" is a stronger claim than the evidence supports. The catalog states this per
> route in `privacyNotes`; read it before treating an E2EE label as a
> model-host guarantee.
### Phala AI
Phala AI routes are classified `tee`. Each request passes through Phala's
gateway, which forwards it to a confidential model session. Before any content
is sent, AnonRouter:
* verifies the gateway's Intel TDX quote to Intel's roots, with a fresh nonce,
the gateway's TLS key, its measured deployment and its published forwarding
code (`hardware-verified` is required);
* verifies the identity of one model session and that session's NVIDIA GPU
attestation, then pins the request to that session.
The model machine's CPU attestation and production profile are enforced by the
verified gateway, not checked by AnonRouter directly. Those checks carry the
`model-machine` scope; AnonRouter's own checks carry `provider-gateway`. On some
routes the model machine is run by a separate compute provider.
Prompts are decrypted inside the verified gateway and model boundaries, so these
routes are not client-opaque E2EE. Phala AI exposes no per-request receipt from
the serving enclave, so signature lookup reports it as unsupported. A failed
verification refuses the request before any content is sent.
## What the API provides
The TEE API returns fresh authenticated evidence, the raw evidence for your own
verification, measurements and signing identities, and provider receipt lookup.
On top of that, AnonRouter enforces explicit verification levels, pinned
measurement policies, route bindings, server-side EIP-191 recovery, and exact
wire-hash checks where the provider signs those bytes.
What you can check depends on the provider: NEAR direct and Venice expose
cryptographically verifiable request receipts, while Chutes, Tinfoil and Phala
AI expose attestation without a compatible per-request receipt.
## Trust boundaries
* The relay, the compatibility broker and every provider worker run **inside the
Intel TDX confidential VM**, which terminates TLS in-enclave. Prompts do not
reach ordinary AnonRouter infrastructure, and the plane attests to that on
demand ([hop 1](#verify-anonrouter-itself-hop-1)).
* Provider credentials stay on their provider-specific worker.
* Control receives only content-free metadata; it serves no request content at
all, and answers `404` for `/v1/chat/completions`.
* Raw prompts, responses, API keys, decrypted E2EE content, and unsafe upstream
bodies are never written to verification caches or logs.
* The dispatch fence still runs immediately before billable provider work.
* Measurement-policy changes require reviewed immutable pins rather than
trusting a live provider assertion in place.
See the [E2EE client guide](/e2ee), [models and privacy labels](/models),
[intelligent routing](/routing), and [the complete privacy architecture](/privacy)
for how these capabilities affect route selection and the rest of the request
lifecycle.
---
# Metering and settlement
Reservations, listed prices, long-context tiers, usage estimates, and billing across provider attempts.
AnonRouter reserves funds before a paid request runs and settles from usage
metadata afterward. The control plane needs token counts, the serving route,
status, and prices to bill you; it does not need your prompt or response text.
## Reserve, measure, settle
1. **Reserve.** AnonRouter holds funds from your prepaid balance using an input
bound, the authorized output budget, and the permitted provider routes. The
hold covers the most expensive single attempt in the funded plan. If funds
cannot cover a later fallback, that fallback can be left out of the plan.
A hold is not an additional charge.
2. **Measure.** The content path reads the provider’s usage counts and measures
latency. When usage is missing, it can supply estimates. Only usage metadata
goes to the control plane for settlement.
3. **Settle.** AnonRouter calculates the charge at the serving route’s pinned
prices, charges no more than the reservation, and releases the unused funds.
Retrying settlement does not charge the same request again.
Prices are pinned when the request is authorized, including the schedules for
its permitted routes. A later catalog update does not change an in-flight
request’s prices. Use the listed route prices when comparing models; the
[service fee](/credits#the-service-fee) applies to credit purchases.
A large hold can exceed the final bill substantially. The input bound used to
protect your balance is separate from the estimate used to decide whether a
prompt fits a model. See [context limits](/context-limits). [Free model
variants](/free-models) use separate allowances and require explicit opt-in.
With an active [subscription](/subscriptions), the same reserve and settle loop
runs against the plan's usage allowance instead of your prepaid balance, at your
plan's price for the route. The reservation counts toward the rolling 5-hour,
rolling 7-day, and monthly limits until the request settles. A request that does
not fit all three limits does not start. Whether it then uses your purchased
credits depends on the plan's
[limit setting](/subscriptions#when-you-reach-a-limit).
## Token prices and caching
Input and output tokens have separate rates. When the provider reports cache
usage, cache-read and cache-write tokens use their respective listed rates;
those tokens are not also charged as ordinary input. If a separate cache rate
is absent, the input rate applies. A cache discount depends on the usage the
provider reports, so do not assume a repeated prompt will receive one.
Image generation and speech, where available, use their listed unit prices
rather than this text-token calculation.
## Long-context pricing
Some routes charge higher rates when the **total prompt token count** exceeds a
published threshold. That total includes ordinary input, cache reads, and cache
writes. At exactly the threshold, the lower tier still applies. With several
thresholds, the highest one the prompt exceeds selects the rate schedule.
The selected schedule prices the **whole request**: every input, cache, and
output token uses its rate in that tier. Only charging the tokens above the
threshold would give the wrong bill. For example, with a 200,000-token threshold,
a 200,001-token prompt selects the higher schedule for all of its input and its
output; a 200,000-token prompt does not.
The reservation can account for a higher tier that the input bound could reach.
Final tier selection uses provider-reported prompt usage, so a large hold alone
does not establish that your prompt crossed a threshold. If exact usage is lost,
a ceiling capture does not apply a higher tier solely because the bound could
have crossed it.
## Missing usage and interrupted requests
Provider-reported usage is used when available. When a provider omits token
counts, AnonRouter can charge estimates from the content path. Missing usage does
not make a completed response free, and an estimate may differ from the
provider’s own tokenization. Do not rely on a cache discount for usage the
provider did not identify as cached.
Stopping a stream or disconnecting does not necessarily cancel work already
sent to the provider. Delivered partial output can be billed. If the request was
dispatched or delivery began and reliable usage cannot be recovered, settlement
can conservatively capture the serving attempt’s authorized ceiling, up to the
reservation. A confirmed provider rejection before delivery releases the hold.
If a content node is lost and its dispatched request’s usage is unknown, the
node-loss recovery path releases that request’s reservation rather than charging
an unknown amount.
Inference cannot take your prepaid balance negative: the final charge never
exceeds the amount successfully reserved.
## Billing across provider attempts
The reservation covers the most expensive permitted attempt in the funded plan,
not the sum of every possible attempt. If an upstream attempt fails and a fallback
succeeds, you are billed for the serving attempt at that route’s pinned prices.
AnonRouter absorbs the failed-attempt costs rather than charging you for each
try. A route cannot be dispatched outside the authorized price envelope. See
[provider routing](/provider-routing#billing-across-attempts).
## Identity and content boundaries
In the [ticket flow](/authentication), control identifies your account and issues
a one-use authorization without receiving the prompt. The relay and credential
worker process content using that authorization rather than your stable account
credential. Usage metadata returns to control, which maps the reservation to
your balance.
[Compatibility mode](/compatibility) has a different boundary: its broker receives
your API key and prompt together to authorize and forward the request. It uses
the same reservation and settlement rules, but does not provide the ticket
flow’s separation of stable identity from content. Settlement itself remains
content-free in both flows.
On an E2EE route, the relay sees ciphertext rather than plaintext and bounds the
reservation from ciphertext length. Provider-reported usage can still settle the
request. Confidential-compute verification and provider privacy practices are
separate from billing; see the [privacy model](/privacy) and
[models and privacy labels](/models).
## What the usage record contains
Settlement writes one usage metadata record per request. Read your records with
`GET /v1/usage`, or view them in the dashboard.
```json title="A settled usage record"
{
"request_id": "req_...",
"model": "meta-llama/llama-3.3-70b",
"operation": "chat",
"input_tokens": 812,
"output_tokens": 143,
"cached_tokens": 0,
"cost_usd": 0.00042,
"status": "ok",
"latency_ms": 1180,
"created_at": "2026-07-30T12:00:00.000Z"
}
```
The billing record contains no prompt or response text, tool arguments or tool
outputs, uploaded file contents, raw provider bodies, or API key. Token counts
can reflect estimates when the provider did not report usage. See
[credits and payments](/credits) for balances, purchase orders, and receipts.
---
# Compatibility mode
Point an OpenAI SDK at AnonRouter with a static key and no ticket.
Compatibility mode lets an off-the-shelf OpenAI client talk to AnonRouter with a
plain `Authorization: Bearer` key and no ticket step. A dedicated broker accepts
the key, mints a single-use ticket internally, and forwards the request. It is
the simplest way to integrate, at a cost to unlinkability.
> **Privacy tradeoff**
>
> Compatibility mode moves the ticket exchange inside the confidential broker.
> The broker receives your API-key identity and plaintext request together in
> memory, then mints and redeems the one-use authorization on your behalf. It
> logs metadata only and holds no database or provider credentials, but this
> flow is not unlinkable like the [private ticket flow](/authentication).
## Key setting
Compatibility mode is gated two ways:
1. The operator must enable it on the deployment. Check
`GET /v1/auth/capabilities`, which reports `compat.enabled`.
2. The individual inference key must be marked for OpenAI-compatible access.
The dashboard selects this by default when you create a key, and lets you turn
it off for a ticket-only credential.
A key created with the setting off receives a `permission_error` telling you to
use the ticket flow or create a compatibility-enabled key.
## Use the OpenAI SDK
Point the SDK at the router base URL and pass your key. Do not send a ticket
header. Sending both a key and a ticket is rejected.
```python
from openai import OpenAI
client = OpenAI(
api_key="ar_your-compat-enabled-key",
base_url="https://api.anonrouter.ai/v1",
max_retries=0,
)
completion = client.chat.completions.create(
model="meta-llama/llama-3.3-70b",
messages=[{"role": "user", "content": "Hello from AnonRouter"}],
)
print(completion.choices[0].message.content)
```
```typescript
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.ANONROUTER_API_KEY!,
baseURL: "https://api.anonrouter.ai/v1",
maxRetries: 0,
});
const completion = await client.chat.completions.create({
model: "meta-llama/llama-3.3-70b",
messages: [{ role: "user", content: "Hello from AnonRouter" }],
});
console.log(completion.choices[0].message.content);
```
Disable automatic SDK retries (`max_retries=0` in Python, `maxRetries: 0` in
JavaScript). An ambiguous disconnect can follow a paid generation; automatically
repeating it can create another charge. A ticket is single-use and cannot be
replayed as an idempotency key.
Streaming works normally. The stream ends with `data: [DONE]` only after billing
has settled. If a stream fails, the broker closes the connection without a final
`[DONE]`, so treat an early end as an incomplete response.
## What works in compatibility mode
| Endpoint | Supported |
| ----------------------------- | ------------------------------------------------ |
| `POST /v1/chat/completions` | Yes, streaming and non-streaming |
| `POST /v1/embeddings` | Yes |
| `GET /v1/models` | Yes, as a plain bearer call to the control plane |
| `POST /v1/completions` | No, returns `404`. Use chat completions. |
| `POST /v1/images/generations` | No, use the [Studio](/studio) flow |
| `POST /v1/audio/speech` | No, use the [Studio](/studio) flow |
> **OpenAI-compatible, not Anthropic**
>
> AnonRouter exposes the OpenAI Chat Completions surface. There is no Anthropic
> Messages endpoint. Models from any creator, including Claude-family routes, are
> reached by their model id through `POST /v1/chat/completions`.
## Request restrictions
The compatibility broker forwards requests through the same strict chat and
embeddings contract as the ticket flow:
* Chat accepts text strings or arrays of `text` and `image_url` parts. Image
parts belong on user messages and require a vision-capable model and route.
* Images may use HTTPS URLs or base64 `data:` URLs containing PNG, JPEG, WebP,
or GIF. E2EE requests do not support image content.
* Unknown top-level chat fields are stripped rather than forwarded. Fields
such as `n`, `logprobs`, and `logit_bias` are not honored. Embeddings has its
own strict schema.
* Legacy `functions` and `function_call` are stripped. Use `tools` and
`tool_choice`; your client executes tools and sends their results back.
* `response_format` supports JSON object and JSON Schema shapes where the
provider supports them; the provider, rather than AnonRouter, enforces the
requested output schema.
* The model must be enabled and support the operation. A catalog listing does
not imply every provider route supports images, tools, or reasoning controls.
### Image input example
This uses the `client` configured above and a local image. Choose a currently
callable vision-capable model from the catalog and set its id in
`ANONROUTER_VISION_MODEL`.
```python
import base64
import os
from pathlib import Path
image = base64.b64encode(Path("image.png").read_bytes()).decode("ascii")
completion = client.chat.completions.create(
model=os.environ["ANONROUTER_VISION_MODEL"],
max_completion_tokens=256,
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Describe this image."},
{"type": "image_url", "image_url": {
"url": f"data:image/png;base64,{image}",
"detail": "auto",
}},
],
}],
)
print(completion.choices[0].message.content)
```
Keep images within the selected provider's limits. AnonRouter accepts at most
10 image parts per message, HTTPS URLs up to 2,048 characters, and data URLs up
to 7,000,000 characters, subject to the deployment's overall request size limit.
Image bytes are request content; they are forwarded to the provider without
being logged or persisted.
## Error format
Errors use the OpenAI error envelope:
```json
{
"error": {
"message": "This API key is not enabled for OpenAI-compatible access.",
"type": "permission_error",
"param": null,
"code": "compat_scope_required"
}
}
```
The `type` is one of `authentication_error`, `permission_error`,
`invalid_request_error`, `insufficient_quota`, `rate_limit_error`, or
`api_error`. The HTTP status code is authoritative. Server-class errors carry a
generic message so no internal detail leaks.
---
# 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.
```text
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
1. Create an AnonRouter account and an inference key in the dashboard. Keys are
prefixed `ar_` and shown once.
2. Keep **OpenAI-compatible access** selected when you create the key. This is
the static-key mode OpenRouter users expect and is selected by default. See
[Compatibility mode](/compatibility) for the tradeoff.
> **Why keep the 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. Compatibility is selected by default so ordinary tools work,
> but you can turn it off when creating a key intended only for the private
> ticket flow. You can run both: a static key for tools that need it and a
> ticket-only key for workloads where unlinkability matters.
## The two-line port
```python
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)
```
```typescript
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.anonrouter.ai/v1", // was https://openrouter.ai/api/v1
apiKey: process.env.ANONROUTER_API_KEY!, // was OPENROUTER_API_KEY
maxRetries: 0, // see "Disable automatic retries"
});
const completion = await client.chat.completions.create({
model: "anthropic/claude-sonnet-4.5", // same id on both catalogs
max_tokens: 256,
messages: [{ role: "user", content: "Hello" }],
});
console.log(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:
```bash
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 |
| `some/model:fastest`, `:cheapest`, `:private`, `:tee`, `:e2ee` | AnonRouter extras, see [Model suffixes](/routing#model-suffixes) |
| `some/model:free` | Only listed free models, plus `anonrouter/free`. Enable free models on your account first. See [Free models](/free-models) |
| `some/model:online` | Not supported, `model_not_found`. Add the [`anonrouter:web_search` tool](/web-search) instead |
| `: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`, `tee`, `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 [Provider routing](/provider-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 |
Provider failover behaves like OpenRouter's. If the provider serving a request
rate-limits or returns a server error before any output, the next provider for
the same model is tried automatically, and a provider that failed recently is
tried last (for 30 seconds, longer if it keeps failing). A malformed-request
`400` is returned to you rather than retried. The differences: failover never
moves a request to a lower privacy tier, and at most 3 providers are tried. See
[provider routing and failover](/provider-routing).
### 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 plugins or file parsing. For web search, use the [`anonrouter:web_search` tool](/web-search) |
| `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](/studio) routes rather than OpenAI-compatible endpoints.
## What you gain
* **A privacy floor instead of a preference.** Every route is labeled
`anonymous`, `private`, `tee`, or `e2ee`, and `provider.minimum_privacy` is
enforced with no silent downgrade, including during failover. See
[Privacy](/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.cost` in 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](/authentication) 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.
---
# Credits and payments
Prepaid credits, listed model prices, payment fees, optional monthly subscriptions, crypto top-ups, and your balance.
AnonRouter runs on prepaid credits. One credit is one US dollar. Your balance is
denominated in USD, and pay-as-you-go inference draws down against it. Optional
[monthly subscriptions](/subscriptions), when offered, include a usage allowance
instead.
## How usage is charged
Text models are billed per token. Image generation is a flat price per image, and
speech is priced per character. A paid request first reserves an amount
from your balance, then settles from usage metadata at the route’s listed prices.
If the provider omits usage, estimates can be billed; an interrupted request can
also be charged. The charge cannot exceed its reservation.
[Metering and settlement](/metering) explains the details.
Usage is charged at the listed prices shown for the model and provider route you
select. The price shown for a route is what you pay: nothing is added per
request, and the only fee is the [service fee](#the-service-fee) when you buy
credits. With an active [subscription](/subscriptions), the allowance is used at
your plan's price for the route instead. Input, output, cache reads, and cache
writes can have different rates. Some routes also have
long-context price tiers: crossing a prompt threshold changes the rates for the
whole request, including output. Check the model’s pricing before sending a long
prompt, and see [context limits](/context-limits) for how requests fit a route.
[Free model variants](/free-models) have separate limits and opt-in requirements.
A paid model does not become free just because your balance is empty.
## The service fee
Buying credits adds a service fee, currently a promotional **5% with a $0.75
minimum**, down from the regular 8% with a $1 minimum. The fee is shown as a
separate line item before you pay. It applies to credit purchases only, not to
individual inference requests.
Promotional pricing can end, so read `fee_percent` and `minimum_fee_usd` from
`GET /v1/billing/catalog` rather than hard-coding either number.
## Subscriptions
Monthly plans with a usage allowance are described on the
[Subscriptions](/subscriptions) page. Purchased credits stay separate from a
plan's allowance.
If your dashboard shows **New rates on next top-up**, your current credits are
still billed at the prices that applied when you bought them. Your next credit
purchase, or starting a plan, moves your account to the current prices.
## Buying credits
Credits are purchased from the dashboard, not through the API. Preset packages
are $5, $10, $25, $50, $100, and $250, and you can enter a custom whole-dollar
amount up to $1,000.
Depending on the payment methods currently enabled, you can pay by:
* **Card.** A hosted checkout through a third-party card processor.
* **Crypto.** An invoice through a self-hosted crypto payment processor,
payable with Bitcoin on-chain or Bitcoin Lightning. A crypto quote is valid for
15 minutes. Credits appear after the invoice settles and the payment is verified.
> **Availability and exact limits come from the catalog**
>
> `GET /v1/billing/catalog` returns the live minimum and maximum purchase
> amounts, the fee percent, the minimum fee, the available packages, and which
> rails are currently accepting payment. `payments_enabled` covers card checkout
> and `crypto_payments_enabled` covers the crypto invoice. Check these flags
> before starting checkout. Read those values rather than hard-coding availability
> or limits.
> **Crypto is not automatically anonymous**
>
> Paying in crypto does not by itself make a purchase anonymous. Public
> blockchains and payment infrastructure can observe the limited data described
> in the privacy policy.
## Crypto payment questions
### When will my credits arrive?
Bitcoin on-chain payments need one confirmation. Lightning payments can settle
immediately. In both cases, AnonRouter must receive and verify the settled
invoice notification before adding credits. Poll the order or check your
purchase history; sending the payment alone does not mean the balance has updated.
### What if I pay too much?
A verified overpaid invoice credits the amount of credits you purchased, once.
The excess does not buy additional credits and is not automatically refunded.
Pay the amount shown on the invoice.
### What if I pay late or send too little?
Late, partial, and invalid payments do not receive automatic credits. Contact
support with the order ID so the payment can be reviewed. If your wallet shows a
completed payment but your order remains pending, check the order before creating
another purchase or sending a second payment.
## Reading your balance and usage
A management key can read your balance and usage programmatically.
```bash
curl "https://control.anonrouter.ai/v1/balance" \
-H "Authorization: Bearer $ANONROUTER_MANAGEMENT_KEY"
```
```json title="GET /v1/balance"
{
"account_id": "acct_...",
"available_usd": 42.5,
"reserved_usd": 0.0,
"updated_at": "2026-07-30T12:00:00.000Z"
}
```
`GET /v1/usage` returns a list of recent requests as metadata only, each with
`request_id`, `model`, `operation`, token counts, `cost_usd`, `status`, and
latency. Add `?workspace_id=` to scope it to one [workspace](/workspaces).
## Receipts and orders
With a session, the dashboard exposes order and receipt history:
| Method | Endpoint | Description |
| ------ | ------------------------------------- | ---------------------------------------- |
| `GET` | `/v1/billing/catalog` | Packages, limits, fee, and payment terms |
| `GET` | `/v1/billing/orders` | List purchase orders |
| `GET` | `/v1/billing/orders/:orderId` | Poll a single order's status |
| `GET` | `/v1/billing/receipts` | List receipts |
| `GET` | `/v1/billing/receipts/:receiptNumber` | Fetch one receipt |
To watch a purchase complete, poll the order until its `status` becomes `paid`
and `credited_at` is set.
---
# Subscriptions
Optional monthly plans with a usage allowance at lower per-model prices.
A subscription is an optional monthly plan. You pay a fixed price each month and
get the same amount as a usage allowance, spent at your plan's prices. Plans,
when offered, are listed on the [pricing page](https://anonrouter.ai/pricing)
and on the [Billing page](https://anonrouter.ai/home/account/billing) of your
dashboard, where you subscribe and manage your plan. Without a plan, you pay as
you go from [prepaid credits](/credits).
| Plan | Price | Monthly allowance | Rolling 5 hours | Rolling 7 days |
| ---- | ---------- | ----------------- | --------------- | -------------- |
| Chat | $8/month | $8 | $3 | $5 |
| Plus | $20/month | $20 | $8 | $10 |
| Max | $100/month | $100 | $40 | $50 |
Chat is sized for chatting and creative writing. Every plan can use every
model, endpoint and the API.
## Plan prices
Each plan has its own price for each model route. A plan's price for a route is
never higher than the pay-as-you-go price, and bigger plans can have lower
prices. Some routes cost the same on every plan. Your allowance is spent at the prices in
effect when each request is made, so how far it goes depends on the models you
use and how you use them.
One allowance covers the website chat and the API. Your usage records show
what each request would have cost at pay-as-you-go prices, and the Billing page
shows what your plan has saved you this month.
## Limits
Three limits apply at the same time: the rolling 5-hour limit, the rolling
7-day limit and the monthly allowance. A request must fit all three before it
starts, and its reserved amount counts toward them until it settles. If a
request does not fit, lower its `max_tokens` or wait for a limit to reset.
## When you reach a limit
**Use credits when a limit is reached** is on for a new subscription. With it
on, a request that does not fit your plan's limits is paid from your prepaid
credits at pay-as-you-go prices instead. The whole request moves to credits; it
is never split between the two. You can turn the setting off on the Billing
page. With it off, or without enough credits, the request is refused with
`402 subscription_limit_exhausted`, which names the limit you reached.
## Changing or cancelling
* **Upgrade.** A bigger plan starts a new month right away. You pay its price
minus a credit for the unused part of your current plan, and the new
allowance is available as soon as the payment goes through.
* **Downgrade.** A smaller plan takes effect at your next renewal. You keep your
current plan until then, and you can cancel the scheduled change.
* **Cancel any time.** Cancelling stops the next renewal. Your plan and its
remaining allowance stay usable until the end of the month you paid for.
* **No rollover.** Unused allowance ends with the month.
Plans are paid by card and renew monthly on your card.
## What does not change
* **Rate limits.** A subscription does not change your account's rate limits.
An active plan does raise your [free model](/free-models) limits.
* **Privacy.** A subscription does not change your privacy settings or route
any request to a less private provider.
* **Web search.** Each successful [web search](/web-search) costs $0.006 on
every account, with or without a plan.
* **Credits.** Purchased credits stay separate and keep the
[service fee](/credits#the-service-fee). The plan only spends them when
**Use credits when a limit is reached** is on.
AnonRouter may change plan prices, allowances, limits, available models and
per-model prices. A change to a plan's monthly price applies only from your next
renewal after notice, and you can cancel before it applies. The Supplemental
Subscription Terms shown when you subscribe apply in addition to the Terms of
Service.
---
# Image and speech generation
Generate images over the ticketed relay and synthesize speech through the router.
Beyond text, AnonRouter can generate images and synthesize speech from media
models in the catalog. Both features are gated by the deployment operator, so
confirm availability before you build on them.
The raw examples use separate control and content origins. The API key goes only
to the control origin; prompts, text, images, and audio go only to the
confidential API origin.
```bash
export ANONROUTER_API_KEY="ar_..."
export ANONROUTER_API_URL="https://api.anonrouter.ai/v1"
export ANONROUTER_CONTROL_URL="https://control.anonrouter.ai/v1"
```
> **Check availability first**
>
> `GET /v1/capabilities` reports whether image generation is enabled
> (`image_generation.enabled`). Image and speech models also appear in
> `GET /v1/models` with `model_type` of `image` or `tts`.
## Image generation
Image generation uses the same private ticket flow as chat, so the prompt and the
generated image travel over the relay and never reach the control plane. Issue a
ticket with `operation: "image"`, then post the prompt with the ticket. The
ticket binds the model, size, and response format, and the request must match.
```bash
# 1. Issue an image ticket.
TICKET=$(curl -s "$ANONROUTER_CONTROL_URL/inference/tickets" \
-H "Authorization: Bearer $ANONROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "alibaba/z-image-turbo",
"operation": "image",
"size": "1024x1024",
"response_format": "b64_json"
}' | jq -r .ticket)
# 2. Generate the image.
curl "$ANONROUTER_API_URL/images/generations" \
-H "Content-Type: application/json" \
-H "x-anonrouter-ticket: $TICKET" \
-d '{
"model": "alibaba/z-image-turbo",
"prompt": "A watercolor fox in a misty forest",
"size": "1024x1024",
"response_format": "b64_json"
}'
```
The response returns base64 image data:
```json
{
"created": 1782097200,
"model": "alibaba/z-image-turbo",
"data": [
{ "b64_json": "iVBORw0KGgo...", "mime_type": "image/png" }
]
}
```
* `size` is `WIDTHxHEIGHT`, each dimension between 128 and 2048, default
`1024x1024`.
* `response_format` is always `b64_json`. No hosted URL is returned.
* Image models are billed at a flat price per image.
## Speech
Speech uses the same two-step privacy split. The ticket request carries the
model, voice, output format, and UTF-16 input length, but not the text. The text
then goes to the confidential API with only the one-use ticket.
```bash
SPEECH_TICKET=$(curl -s "$ANONROUTER_CONTROL_URL/inference/tickets" \
-H "Authorization: Bearer $ANONROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"operation": "speech",
"model": "venice/kokoro-text-to-speech",
"input_chars": 22,
"voice": "af_sky",
"response_format": "mp3"
}' | jq -r .ticket)
curl "$ANONROUTER_API_URL/audio/speech" \
-H "x-anonrouter-ticket: $SPEECH_TICKET" \
-H "Content-Type: application/json" \
-d '{
"model": "venice/kokoro-text-to-speech",
"input": "Hello from AnonRouter.",
"voice": "af_sky",
"response_format": "mp3"
}' \
--output speech.mp3
```
* `input` is the text to synthesize, up to 20,000 characters.
* `input_chars` in the ticket is the exact UTF-16 code-unit length. The official
SDK handles this correctly for emoji and other astral characters.
* `voice` is an optional, provider-specific string.
* `response_format` is `mp3`. The response body is raw audio with the matching
`content-type`.
* Speech is billed at a flat price per million input characters.
> **No video endpoint**
>
> Video generation is not part of the API. Only text, image, embedding, and
> speech models are callable.
---
# Workspaces
Organize API keys and usage into named containers within one account.
A workspace is a named container that groups API keys and usage inside a single
account, similar to a project or an environment. Every account starts with a
Default Workspace, and keyless activity rolls up to it. An account can hold up to
10 workspaces.
Workspaces are managed with a session from the dashboard.
## Endpoints
| Method | Endpoint | Description |
| -------- | -------------------- | -------------------------------------- |
| `GET` | `/v1/workspaces` | List workspaces |
| `POST` | `/v1/workspaces` | Create a workspace |
| `PATCH` | `/v1/workspaces/:id` | Rename or edit a workspace |
| `DELETE` | `/v1/workspaces/:id` | Delete a workspace and revoke its keys |
A workspace looks like:
```json
{
"id": "ws_...",
"name": "Production",
"slug": "production",
"description": "Live traffic keys",
"is_default": false,
"created_at": "2026-07-30T12:00:00.000Z"
}
```
Slugs are lowercase alphanumeric with hyphens and must be unique within the
account. The Default Workspace cannot be deleted.
## Scoping keys and usage
* Create an inference key inside a workspace by passing `workspace_id` when you
create it. See [authentication](/authentication).
* Filter usage to one workspace with `GET /v1/usage?workspace_id=`.
Deleting a workspace revokes the API keys that belong to it, so move or recreate
any keys you still need first.
## Routing and privacy defaults
### Privacy boundary
The privacy boundary is the weakest privacy tier any request made with the
workspace's keys may use. Set it on the **Privacy** page:
| Boundary | `privacy_boundary` | Tiers a request may use |
| ------------------ | ------------------ | ----------------------------- |
| Standard (default) | `null` | Anonymous, Private, TEE, E2EE |
| Private | `balanced` | Private, TEE, E2EE |
| Maximum | `maximum` | TEE, E2EE |
The boundary applies to every request from every key in the workspace and to
the browser chat, which uses your Default Workspace. That covers chat with a
named model, `/auto`, embeddings, images, speech, and
[compatibility mode](/compatibility). A request can ask for a stricter tier, but
it cannot go below the boundary: not with `routing.privacy_classes`, not with
`provider.minimum_privacy`, and not by naming a model. Failover never goes
below it either.
If no route of the requested model meets the boundary, the request is refused
with `403 privacy_boundary_not_met` before any provider sees it. The message
names the boundary, never your prompt:
```text
This workspace's privacy boundary (Private) does not allow this model's privacy tier. A workspace owner can change the boundary on the Privacy page.
```
The boundary is separate from the `/auto` privacy level on the **Routing**
page, which only affects `/auto` and which a request can replace. When the
boundary is stricter, `/auto` uses the boundary.
`GET /v1/routing/preferences` returns the boundary as `privacy_boundary`
(`null` for Standard). `PUT` accepts it; leaving it out keeps the stored value.
### Provider defaults
Each workspace has its own provider routing defaults, which apply to every
request made with that workspace's API keys. In the dashboard:
* The **Privacy** page holds the privacy boundary and, under Provider
fallback, whether fallback to another provider is allowed, how many times it
may retry on another provider (up to 1 or up to 2), and how far a fallback may
step down.
* The **Routing** page holds the Auto router (how `/auto` picks a model,
including its own privacy level), the default provider sort, and the
providers that are disabled for the workspace.
With a session, read and replace them through
`GET` and `PUT /v1/routing/preferences?workspace_id=`. Without
`workspace_id` the call applies to the Default Workspace.
Workspace defaults merge restrictively with each request's `provider` field: a
request can make routing stricter, for example by ignoring another provider or
turning fallback off, but it can never lower the workspace's privacy floor or
allow more providers than the workspace does. See
[provider routing and failover](/provider-routing#workspace-defaults).
---
# Connected apps
Authorize external apps with a private AnonRouter identity and delegated inference, or register an app of your own.
Connected Apps lets an external application use AnonRouter without asking for
your API key or learning your AnonRouter profile. The app receives a private,
app-specific identifier and permission to run inference in a dedicated
workspace.
Developers can use the same feature, called **AnonRouter Connect**, to add
**Sign in with AnonRouter** to a confidential server-side application. Connect
uses the OAuth 2.1 authorization-code flow, OpenID Connect, and PKCE.
> **Not enabled on the production deployment yet**
>
> AnonRouter Connect is operator-gated and is currently switched off in
> production. The OpenID Connect discovery document and every Connect endpoint
> answer `404` on the live deployment, so a Sign in with AnonRouter integration
> cannot complete an authorization today. This page documents the interface so
> an integration can be built against it; check the discovery URL before
> shipping one.
> **Connected Apps is not API-key sharing**
>
> An authorized app receives short-lived Connect tokens. It cannot read, create,
> change, or revoke your normal API keys.
Open your connections and developer apps at
[Dashboard → Account → Connected apps](https://anonrouter.ai/home/account/connected-apps).
## What an authorized app receives
Every grant has two fixed scopes: `openid` and `inference`.
| The app can | The app cannot |
| -------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Receive an anonymous identifier specific to its security domain | Read your name, email address, or internal AnonRouter account ID |
| List the models available for delegated inference | Read your balance, billing records, recovery information, or account settings |
| Request inference through a dedicated workspace created for that app | See or manage your normal API keys and workspaces |
| Use your account's funds within any cap you approve | Call billing, recovery, session, or administrative APIs |
The identifier in the OpenID Connect `sub` claim is pairwise: unrelated app
security domains receive different identifiers for the same AnonRouter account.
This makes the identifier useful as the app's private account key without making
it a global AnonRouter identity.
The dedicated workspace separates the app's usage from your other workspaces.
AnonRouter uses a hidden attribution credential internally, but that credential
is never returned to the app.
## Connect to an external app
You normally start on the external application's website by selecting **Sign in
with AnonRouter**.
1. The app redirects you to AnonRouter. If needed, sign in on the AnonRouter
domain. Do not enter your AnonRouter password or recovery phrase directly
into the external app.
2. Check the app name. AnonRouter explains that the app will receive a private,
app-specific identity rather than your profile.
3. Review the app name, callback hostname, requested permissions, and any linked
Privacy Policy or Terms of Service.
4. Optionally enable a **Lifetime spending cap**. The cap includes successful
usage and the cost reserved for requests that are still running.
5. Optionally enable **Expires after** and choose how many days the grant should
remain active.
6. Select **Authorize** to continue or **Cancel** to deny the request. AnonRouter
returns the browser only to the app's exact registered callback address.
Authorizing an app creates its dedicated workspace. A later authorization for
the same app reuses that workspace while creating a new explicit grant.
> **An uncapped grant can use your available balance**
>
> Leaving the lifetime cap off does not create unlimited credit, but the app can
> continue using available funds until the grant expires or is disconnected.
> Set a cap when you want a firm lifetime ceiling.
## Review or disconnect an app
The **Connected apps** table shows each authorization's app, dedicated
workspace, scopes, successful and reserved spend, approved cap, last use, and
status. A grant can be **Active**, **Expired**, or **Disconnected**.
To end access:
1. Open [Connected apps](https://anonrouter.ai/home/account/connected-apps).
2. Find the active grant.
3. Select **Disconnect** and confirm.
Disconnecting invalidates the grant's access and refresh tokens immediately.
The dedicated workspace and its usage history remain on your account for
attribution and auditing. You can reconnect later by starting a new
authorization from the app.
Signing out of AnonRouter only ends the browser session; it does not disconnect
an app. Password recovery, account disablement, app deletion, secret rotation,
and security-sensitive app configuration changes can also revoke grants.
## Register your own app
AnonRouter Connect is intended for confidential applications with a server-side
component that can protect a client secret. A browser-only SPA, native app, or
other public client must put the Connect exchange and token storage behind a
trusted backend. Never embed a client secret in browser JavaScript,
downloadable application code, or a mobile binary.
To register an app:
1. Open [Connected apps](https://anonrouter.ai/home/account/connected-apps) and
scroll to **Your apps**.
2. Select **Create New App**.
3. Enter the app name that users should see during authorization.
4. Enter one or more exact redirect URIs, one per line.
5. Optionally add a Privacy Policy URL and Terms of Service URL. AnonRouter
shows these links during authorization and in the user's connection menu.
6. Select **Create app**.
7. Copy the client ID and client secret before closing the dialog. The secret
is displayed once and cannot be retrieved later.
Store the client secret in a server-side secret manager or protected
environment variable. Do not commit it, log it, place it in a URL, or send it to
the browser.
### Registration rules
* An account can have up to 10 enabled developer apps.
* App names are limited to 120 characters and cannot impersonate AnonRouter.
* An app can register between 1 and 20 exact redirect URIs.
* Redirect URIs cannot contain wildcards, embedded credentials, or fragments.
Paths, ports, query strings, and trailing slashes are significant.
* Every redirect URI for one app must use the same hostname. AnonRouter derives
the pairwise-identity security domain from that hostname.
* Production redirect and policy URLs require HTTPS and cannot target
localhost. A local AnonRouter development environment permits HTTP only on
`localhost`, `127.0.0.1`, or `[::1]`.
### Rotate a secret
Use **Rotate secret** when a secret is lost, exposed, or due for replacement.
The old secret stops working immediately, all current grants for the app are
revoked, and users must authorize the app again. The replacement secret is also
displayed once.
### Delete an app
Deleting an app disables its credentials and disconnects everyone who
authorized it. This cannot be undone from the dashboard. Register a new app and
ask users to authorize it if service needs to resume.
## Integrate Sign in with AnonRouter
Use a maintained OpenID Connect client library instead of constructing the
protocol by hand.
| Setting | Production value |
| --------------------- | ------------------------------------------------------------------------ |
| Issuer | `https://control.anonrouter.ai/connect` |
| Discovery | `https://control.anonrouter.ai/connect/.well-known/openid-configuration` |
| Scopes | `openid inference` |
| Response type | `code` |
| PKCE method | `S256` |
| Client authentication | `client_secret_basic` |
| Control base URL | `https://control.anonrouter.ai/v1` |
| API base URL | `https://api.anonrouter.ai/v1` |
Use the discovery document for the authorization, token, UserInfo, revocation,
and JWKS endpoints. Do not hard-code those endpoint paths.
Connect is a control-plane feature end to end. The issuer, the discovery
document, and the two endpoints a Connect access token can reach (the model
list and the ticket mint) are all served from `control.anonrouter.ai`. Content
goes to `api.anonrouter.ai` and nowhere else, so an integration needs both base
URLs. See [Run delegated inference](#run-delegated-inference).
### Authorization flow
1. On your server, generate a PKCE verifier and `S256` challenge, a random
`state`, and an OpenID Connect `nonce`.
2. Redirect the browser to the discovered authorization endpoint with your
client ID, one exact registered redirect URI, `response_type=code`, scopes
`openid inference`, the PKCE challenge, `state`, and `nonce`.
3. At the callback, reject any state mismatch. Exchange the short-lived code
from your server using `client_secret_basic`, the original redirect URI, and
the original PKCE verifier.
4. Verify the ID-token signature against the discovered JWKS, then validate its
issuer, audience, expiry, and nonce. Use `sub` only as this app's private
account key.
5. Store access and refresh tokens only on your server. Give the browser an
opaque application session cookie instead of the Connect tokens.
6. Replace the stored refresh token every time you refresh. Connect refresh
tokens rotate; reusing a consumed token revokes the complete token family.
7. Use the discovered revocation endpoint when the user disconnects inside your
app, then clear the app's local session and tokens.
Access tokens are intentionally short-lived. Refresh tokens let your server
continue the approved grant without asking the user to sign in every few
minutes; they do not bypass expiry, spending limits, account funding, app
status, or user revocation.
## Run delegated inference
Send the Connect access token as `Authorization: Bearer ` to the control
base URL. It can list models and request a content-free, single-use inference
ticket. It cannot call protected account APIs.
For example, request a ticket for a chat completion:
```bash
curl https://control.anonrouter.ai/v1/inference/tickets \
-H "Authorization: Bearer $ANONROUTER_CONNECT_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model": "/auto",
"operation": "chat",
"max_tokens": 512
}'
```
Then send the returned `ticket`, without the Connect access token, to the content
request in `x-anonrouter-ticket`. The ticket is short-lived, single-use, and
bound to the request constraints:
```bash
curl https://api.anonrouter.ai/v1/chat/completions \
-H "x-anonrouter-ticket: $ANONROUTER_TICKET" \
-H "Content-Type: application/json" \
-d '{
"model": "/auto",
"max_tokens": 512,
"messages": [{"role": "user", "content": "Hello"}]
}'
```
Do not send the Connect access token with the content request. This keeps the
stable app authorization on the control plane while the content relay receives
only the opaque single-use ticket. The hosts enforce the split rather than
relying on you to honor it: `control.anonrouter.ai` does not serve
`/v1/chat/completions`, and `api.anonrouter.ai` does not serve
`/v1/inference/tickets`. Sending either request to the other host returns `404`.
See [How privacy works](/privacy) for the full request boundary.
## Troubleshooting
### The callback is rejected
Compare the callback with the registered URI byte for byte, including scheme,
hostname, port, path, query string, and trailing slash. Production callbacks
must use HTTPS. If a callback destination changes, users must authorize the new
configuration.
### Inference is denied after authorization
Check the user's account balance, grant status, optional expiry, spending cap,
and the app's status. Reserved in-flight requests count toward the cap. A user
must start a new authorization to approve a different cap or expiry.
### The client secret is lost
Secrets cannot be recovered. Rotate the secret under **Your apps**, store the
new value immediately, and have users reconnect.
### Users were disconnected after an app change
Secret rotation, app deletion, and security-sensitive callback changes revoke
existing grants by design. This prevents old consent from carrying over to new
credentials or redirect destinations.
### The authorization interaction expired
Authorization interactions and codes are short-lived and single-use. Return to
the external app and select **Sign in with AnonRouter** again instead of
reloading or revisiting the old consent URL.
---
# Connect your apps
Use AnonRouter with the chat clients, coding tools, self-hosted interfaces, and personal agents you already use.
AnonRouter plugs into many existing AI tools through their custom
OpenAI-compatible provider setting. In most cases, you only need an API base
URL, a compatibility-enabled AnonRouter key, and a model id.
> **The common connection values**
>
> **API base URL:** `https://api.anonrouter.ai/v1`\
> **API key:** a compatibility-enabled AnonRouter inference key beginning with
> `ar_`\
> **Model:** `/auto` or an exact id from the [model catalog](https://anonrouter.ai/models)
> **Building an app with user authorization?**
>
> If you are developing a server-side app and want users to authorize it without
> sharing an API key, [Connected Apps](/connected-apps) is the interface:
> AnonRouter Connect provides private sign-in, delegated inference, spending
> controls, and revocation through OAuth 2.1 and OpenID Connect. It is not
> enabled on the production deployment yet, so read that page before planning
> around it.
Some apps ask for the full Chat Completions URL instead of a base URL, store
credentials differently, or require extra model capability settings. Use the
app-specific guides below for the exact field names and paths.
## How the plug-and-play setup works
Most integrations use [compatibility mode](/compatibility): the app sends a
standard OpenAI Chat Completions request with your AnonRouter key, and
AnonRouter routes it to the model you selected. Streaming responses and tool
calls return in the OpenAI-compatible format the client expects.
This makes AnonRouter work with clients that cannot perform a custom
authentication exchange. It also means the compatibility broker temporarily
receives the stable key identity and request content together in memory. Use a
dedicated key, give it an appropriate credit limit, and read the
[privacy model](/privacy) before sending sensitive content.
Clients or plugins that can make two coordinated requests may instead use the
native private ticket flow. The stable key stays on the control plane, while
the content request carries only a single-use ticket. Each integration guide
identifies which connection path it uses.
## Choose a model
Use `/auto` when you want AnonRouter to select a suitable route within your
allowed privacy and capability constraints. Use an exact model id when you
need predictable behavior or a specific model provider.
Before using tools, file access, or unattended agent workflows, confirm that
the selected route supports tool calling and has enough context for the app's
prompts and tool output. See [models and privacy labels](/models) and
[intelligent routing](/routing) for the selection rules.
## Developer tools
## Chat clients and interfaces
## Personal agents
## What generally carries across apps
| Capability | What to expect |
| ----------------- | --------------------------------------------------------------------------------- |
| Model discovery | Available when the app calls AnonRouter's authenticated `GET /v1/models` endpoint |
| Streaming chat | Supported through OpenAI-compatible Chat Completions |
| Tool calling | Requires support from both the client and the selected model route |
| Automatic routing | Select `/auto` when the app accepts a custom model id |
| Embeddings | Available to clients that expose a compatible embedding-provider setting |
| Key limits | Use a dedicated key and set a credit ceiling appropriate for the app |
## Using a tool that is not listed
If a tool supports a custom OpenAI-compatible provider, start with the common
base URL, key, and model values above. Check whether its URL field expects the
`/v1` base or the full `/v1/chat/completions` path, and whether it requires
native tool calling or the Responses API.
AnonRouter implements a deliberate subset of the OpenAI API. A configurable
base URL does not guarantee every app feature will work, so test ordinary text
chat first and then enable tools, files, retrieval, or agent actions one at a
time.
---
# OpenCode Setup with AnonRouter
Connect OpenCode to AnonRouter as a custom OpenAI-compatible provider, configure coding models, enable tools, and protect source-code privacy.
OpenCode can use AnonRouter as a custom OpenAI-compatible provider. Add one
compatibility-enabled key, declare the models you want in `opencode.json`, and
switch among them from OpenCode's `/models` picker.
> **The short version**
>
> Run `/connect`, choose **Other**, use provider id `anonrouter`, and paste a
> compatibility-enabled `ar_...` key. Then add the JSON configuration below,
> restart OpenCode, and run `/models`.
## Before you begin
You need:
1. A current [OpenCode installation](https://opencode.ai/docs/).
2. An AnonRouter account with available credits.
3. An inference key with **OpenAI-compatible access** enabled.
4. At least one tool-capable model id from the [AnonRouter catalog](https://anonrouter.ai/models).
OpenCode sends a conventional bearer key and cannot perform AnonRouter's
two-step private ticket exchange. Use a dedicated compatibility key with a
credit limit appropriate for an agent that can make repeated requests.
## 1. Save the AnonRouter credential
Start OpenCode and run:
```text
/connect
```
Then:
1. Scroll to **Other** in the provider list.
2. Enter `anonrouter` as the provider id.
3. Paste the compatibility-enabled AnonRouter inference key.
The provider id must exactly match the key used in the JSON configuration.
OpenCode stores credentials entered through `/connect` in
`~/.local/share/opencode/auth.json`.
> **Protect OpenCode's credential store**
>
> Restrict access to the OpenCode data directory and never commit
> `auth.json`. A hidden or masked key is still a reusable bearer credential.
## 2. Add the custom provider
Create `opencode.json` in the project root, or add the provider to the existing
file:
```json
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"anonrouter": {
"npm": "@ai-sdk/openai-compatible",
"name": "AnonRouter",
"options": {
"baseURL": "https://api.anonrouter.ai/v1"
},
"models": {
"anthropic/claude-opus-4.8": {
"name": "Claude Opus 4.8"
},
"anthropic/claude-sonnet-5": {
"name": "Claude Sonnet 5"
},
"moonshotai/kimi-k2.5": {
"name": "Kimi K2.5"
},
"qwen/qwen-3-coder-480b-turbo": {
"name": "Qwen 3 Coder 480B"
}
}
}
}
}
```
`@ai-sdk/openai-compatible` tells OpenCode to use the OpenAI Chat Completions
protocol. The `baseURL` must end at `/v1`; OpenCode adds
`/chat/completions` itself.
The model map is explicit because OpenCode does not automatically import a
custom provider's complete catalog. Replace stale examples with exact current
ids from AnonRouter. Only include models you want displayed in the picker.
## Global or project configuration
OpenCode merges its configuration sources. The most useful locations are:
| Scope | File |
| ---------------------------------- | ----------------------------------- |
| Every project for the current user | `~/.config/opencode/opencode.json` |
| One repository | `opencode.json` in the project root |
A project configuration overrides conflicting global settings. OpenCode's
official documentation says project configuration is safe to commit, but that
assumes it contains no embedded secrets. Keep the API key in `/connect` storage
or an environment variable.
## Environment-variable alternative
Instead of `/connect`, add `apiKey` under the provider's `options`:
```json
{
"options": {
"baseURL": "https://api.anonrouter.ai/v1",
"apiKey": "{env:ANONROUTER_API_KEY}"
}
}
```
Set the value before starting OpenCode:
```bash
export ANONROUTER_API_KEY=ar_REPLACE_WITH_YOUR_API_KEY
opencode
```
This JSON is only the `options` fragment; merge it into the complete provider
configuration rather than replacing `opencode.json` with it.
## 3. Select a model
Restart OpenCode after changing the provider configuration, then run:
```text
/models
```
Choose a model under **AnonRouter**. OpenCode identifies custom models as
`provider_id/model_id`; for example, the first configuration entry is selected
as:
```text
anonrouter/anthropic/claude-opus-4.8
```
The portion sent to AnonRouter remains `anthropic/claude-opus-4.8`.
You can also set a default at the root of `opencode.json`:
```json
{
"model": "anonrouter/anthropic/claude-opus-4.8"
}
```
## Choose models for agentic coding
OpenCode notes that coding agents need models that are strong at both code and
tool calling. For each AnonRouter route, verify:
* **Tools** are supported.
* The context window can hold the relevant files, instructions, and tool output.
* The output limit is sufficient for the edits you expect.
* The route's privacy and moderation labels match your project.
Model capabilities and ids change over time. Treat the examples as a starting
configuration and verify them against the live catalog before relying on them
for unattended work.
## How tools work
OpenCode describes local actions, such as reading files, editing code, searching,
and running shell commands, with OpenAI tool definitions. AnonRouter forwards
supported tool definitions and model tool calls. OpenCode executes the selected
tools locally and sends their results back in a later request.
AnonRouter does not execute OpenCode tools. Your OpenCode permissions determine
what the agent can read or change, so review them separately from the model
provider configuration.
## Compatibility limits
| Feature | Status |
| ---------------------------- | ------------------------------------------------------------ |
| Streaming Chat Completions | Supported |
| Tool calling | Supported on tool-capable routes |
| Static bearer authentication | Requires compatibility access on the key |
| Inline image message arrays | Not supported by AnonRouter's current compatibility contract |
| Legacy text completions | Not supported |
| OpenAI Responses API | Not used by this configuration |
Keep the provider package as `@ai-sdk/openai-compatible`. OpenCode's official
guide reserves `@ai-sdk/openai` for providers using `/v1/responses`, which is
not the AnonRouter endpoint configured here.
## Source-code privacy boundary
OpenCode requests may include source files, patches, repository instructions,
terminal output, test failures, tool schemas, and earlier conversation context.
Anything included in the prompt reaches AnonRouter and the selected model
provider.
This setup uses [AnonRouter compatibility mode](/compatibility). The
compatibility broker temporarily receives key identity and plaintext request
together. AnonRouter does not intentionally retain prompts or responses, but
this static-key flow is not unlinkable like the native ticket flow.
Use OpenCode's permission controls, exclude secrets and generated artifacts from
agent context, and choose an AnonRouter route whose privacy label fits the code
you are sending.
## Troubleshooting
### AnonRouter does not appear under `/models`
Confirm `opencode.json` contains the `anonrouter` provider and at least one
entry under `models`. Saving a credential with `/connect` does not create the
provider configuration by itself.
### Authentication returns 401 or 403
Run `opencode auth list` to confirm the credential is stored under the
`anonrouter` id. Verify the key begins with `ar_` and has **OpenAI-compatible
access** enabled.
### Requests return 404
Use `https://api.anonrouter.ai/v1` for `options.baseURL`. Do not append
`/chat/completions`.
### Model not found
The object key under `models` must be an exact current AnonRouter API id. Replace
stale values with ids from the live catalog and restart OpenCode.
### File edits or shell tools do not work
Choose a route that advertises tool calling and review OpenCode's local tool
permissions. A successful text response alone does not prove the model emitted
a valid tool call.
### Context-length errors
Reduce the files and history included in the request or select a model with a
larger advertised context window. If you add OpenCode `limit` fields, copy the
current model limits rather than guessing them.
## OpenCode setup FAQ
### Do I need an OpenCode plugin?
No. OpenCode already supports custom OpenAI-compatible providers.
### Can I configure several AnonRouter models?
Yes. Add several entries to the provider's `models` object, then switch among
them with `/models`.
### Should I commit `opencode.json`?
Project configuration can be committed when it contains only provider and model
settings. Keep the key in `/connect` storage or an environment variable.
### Is this AnonRouter's private ticket flow?
No. OpenCode sends a static bearer key and therefore uses compatibility mode.
## Sources and further reading
* [OpenCode: Providers and custom providers](https://opencode.ai/docs/providers)
* [OpenCode: Configuration and file locations](https://opencode.ai/docs/config/)
* [OpenCode: Selecting and configuring models](https://opencode.ai/docs/models)
* [AnonRouter compatibility mode](/compatibility)
* [AnonRouter model catalog](https://anonrouter.ai/models)
AnonRouter is an independent service and is not affiliated with OpenCode.
---
# Janitor AI Proxy Setup with AnonRouter
Connect Janitor AI to AnonRouter with an importable JSON proxy configuration, the correct API URL, model settings, and troubleshooting steps.
Janitor AI
Proxy integration guide
Use AnonRouter as a custom OpenAI-compatible proxy in Janitor AI. The fastest
setup is to import one JSON object containing your AnonRouter API URL, API key,
and model. After importing it, Janitor AI sends chat completions through
AnonRouter instead of its built-in model.
> **The short version**
>
> In Janitor AI, open **API Settings → Proxy → Import**, paste the JSON below,
> replace the example key with your own compatibility-enabled AnonRouter key,
> and refresh the page after saving.
## Janitor AI proxy JSON
Copy this JSON into Janitor AI's **Import** box. Replace only the example
`apiKey` value unless you want to choose a specific model instead of `/auto`.
```json
{
"name": "AnonRouter",
"apiUrl": "https://api.anonrouter.ai/v1/chat/completions",
"apiKey": "ar_REPLACE_WITH_YOUR_API_KEY",
"model": "/auto"
}
```
The JSON must remain valid: use straight double quotes, keep the commas, and do
not add comments. Your completed configuration contains a live API key, so do
not post it, send it to someone else, or include it in a screenshot.
## Before you begin
You need:
1. An AnonRouter account with available credits.
2. An inference API key with **OpenAI-compatible access** enabled. Janitor AI
sends a static bearer key and cannot perform AnonRouter's private ticket
handshake, so this setting is required.
3. A Janitor AI character that permits proxy use. A character creator can
disable third-party proxies for that character.
Create the key in your AnonRouter dashboard and copy it when it is displayed.
Keys begin with `ar_` and are shown only once.
> **Enable OpenAI-compatible access**
>
> A normal ticket-only inference key will not work in Janitor AI. Enable
> **OpenAI-compatible access** when creating the key, or turn it on for the key
> in your dashboard before importing the configuration.
## Import the configuration in Janitor AI
1. Open a character chat in Janitor AI.
2. Open **API Settings** and select **Proxy**.
3. Under **Proxy configurations**, choose **Import**.
4. Paste the JSON from this page.
5. Replace `ar_REPLACE_WITH_YOUR_API_KEY` with your AnonRouter inference key.
6. Select **Import**, save the API settings, and refresh the Janitor AI page.
7. Choose **AnonRouter** as the active proxy configuration and send a message.
Janitor AI notes that a new or changed proxy URL takes effect only after a page
refresh. If the configuration saves but the chat still uses another provider,
refresh before troubleshooting anything else.
## Manual setup values
If you prefer Janitor AI's **+ New** form, enter the same values manually:
| Janitor AI field | Value |
| ------------------- | ---------------------------------------------------------------------------------------------- |
| Name | `AnonRouter` |
| Proxy URL / API URL | `https://api.anonrouter.ai/v1/chat/completions` |
| API Key | Your compatibility-enabled `ar_...` inference key |
| Model | `/auto` or an exact model id from the [AnonRouter model catalog](https://anonrouter.ai/models) |
Use the complete chat-completions endpoint shown above. `https://api.anonrouter.ai/v1`
is the SDK base URL, but Janitor AI's proxy configuration expects the full URL
ending in `/v1/chat/completions`.
## Choose a model
The example uses `/auto`, AnonRouter's intelligent route. It selects an
available model that satisfies the request without silently lowering the
route's privacy requirement.
To pin chats to one model, import one of the configurations below. Each tab is
a complete JSON object; Janitor AI imports one proxy configuration at a time.
You can reuse the same AnonRouter API key in each configuration.
```json
{
"name": "AnonRouter — Auto",
"apiUrl": "https://api.anonrouter.ai/v1/chat/completions",
"apiKey": "ar_REPLACE_WITH_YOUR_API_KEY",
"model": "/auto"
}
```
```json
{
"name": "AnonRouter — Venice Role Play Uncensored",
"apiUrl": "https://api.anonrouter.ai/v1/chat/completions",
"apiKey": "ar_REPLACE_WITH_YOUR_API_KEY",
"model": "venice/venice-role-play-uncensored"
}
```
```json
{
"name": "AnonRouter — Llama 3.3 70B",
"apiUrl": "https://api.anonrouter.ai/v1/chat/completions",
"apiKey": "ar_REPLACE_WITH_YOUR_API_KEY",
"model": "meta-llama/llama-3.3-70b"
}
```
```json
{
"name": "AnonRouter — Claude Sonnet 4.5",
"apiUrl": "https://api.anonrouter.ai/v1/chat/completions",
"apiKey": "ar_REPLACE_WITH_YOUR_API_KEY",
"model": "anthropic/claude-sonnet-4.5"
}
```
```json
{
"name": "AnonRouter — Gemini 3.5 Flash",
"apiUrl": "https://api.anonrouter.ai/v1/chat/completions",
"apiKey": "ar_REPLACE_WITH_YOUR_API_KEY",
"model": "google/gemini-3.5-flash"
}
```
```json
{
"name": "AnonRouter — DeepSeek V3.2",
"apiUrl": "https://api.anonrouter.ai/v1/chat/completions",
"apiKey": "ar_REPLACE_WITH_YOUR_API_KEY",
"model": "deepseek/deepseek-v3.2"
}
```
```json
{
"name": "AnonRouter — GPT-4o",
"apiUrl": "https://api.anonrouter.ai/v1/chat/completions",
"apiKey": "ar_REPLACE_WITH_YOUR_API_KEY",
"model": "openai/gpt-4o"
}
```
Model ids are case-sensitive and may change as providers update their
catalogs. Copy the current id from the [model catalog](https://anonrouter.ai/models)
rather than guessing it. Import as many of the configurations above as you
want; each one appears as a separate Janitor AI proxy option.
### Find uncensored models
AnonRouter labels uncensored models in the catalog so you can distinguish them
from moderated and unknown routes. Use the catalog's uncensored filter to review
the current options, pricing, capabilities, and privacy labels:
[Browse uncensored models on AnonRouter](https://anonrouter.ai/models?moderation=uncensored)
The example above uses
[`venice/venice-role-play-uncensored`](https://anonrouter.ai/models/venice/venice-role-play-uncensored),
but availability can change. Copy the current model id from the filtered catalog
before importing your configuration. An uncensored label describes the model
route; it does not remove Janitor AI's character-level proxy restrictions or
make generated content risk-free.
### Can one JSON import contain several models?
No. Janitor AI's proxy configuration accepts one `model` string per imported
JSON object. A model array, a comma-separated list, or AnonRouter's API-level
`models` fallback field is not part of Janitor AI's proxy-import format.
Import each configuration separately if you want named options for several
models. If you want one configuration that can route across multiple models,
use `/auto`; Janitor AI still sends `/auto` as one model id, and AnonRouter makes
the route selection behind the API.
## Use free models
Enable [free models](/free-models) once on your AnonRouter account, then import
this proxy configuration:
```json
{
"name": "AnonRouter — Free Models",
"apiUrl": "https://api.anonrouter.ai/v1/chat/completions",
"apiKey": "ar_REPLACE_WITH_YOUR_API_KEY",
"model": "anonrouter/free"
}
```
The account opt-in is consent to Anonymous routing: the upstream provider can
see and may retain your prompts and responses. Janitor AI does not need a
custom `provider.minimum_privacy` field. A Private or Maximum workspace
boundary still prevents free routing.
You can instead use an exact listed `/:free` ID. Free requests
cost $0, can work with an empty balance on an eligible account, and never fall
back to a paid model. Capacity is shared: wait for the response's indicated
reset when you receive a free-capacity error. A long character definition or
chat history can exceed a free route's [context limit](/context-limits).
For paid goal-specific selection, the same model field accepts the
[Coding, Uncensored, and other routers](/routers).
## Privacy and proxy behavior
AnonRouter does not intentionally log or retain your prompts or responses.
However, Janitor AI requires a conventional static-key request, so this setup
uses AnonRouter's [compatibility mode](/compatibility). The compatibility broker
briefly receives your API-key identity and plaintext request together in memory
to issue an internal ticket and forward the request. This flow is not unlinkable
like AnonRouter's default [private ticket flow](/authentication).
Your character definition, persona, chat history included in the request, and
new message can pass through the client, AnonRouter, and the selected model
provider. Review Janitor AI's own privacy terms and the privacy label of your
selected AnonRouter route before sending sensitive information.
AnonRouter is an independent service and is not affiliated with Janitor AI.
## Troubleshooting Janitor AI proxy errors
### The configuration saved, but it is not active
Refresh the Janitor AI page, reopen the chat, and select **AnonRouter** as the
active proxy. Janitor AI applies proxy URL changes after a page refresh.
### 401 authentication error
The API key is missing, mistyped, revoked, or includes extra whitespace. Create
a new AnonRouter inference key if you no longer have the original secret, then
paste the complete value beginning with `ar_`.
### 403 compatibility error
The key does not have OpenAI-compatible access enabled. Turn on compatibility
for that inference key in the AnonRouter dashboard or create a new key with the
option enabled. See [Compatibility mode](/compatibility) for the privacy
tradeoff.
### 404 or proxy URL error
Use exactly:
```text
https://api.anonrouter.ai/v1/chat/completions
```
Do not stop at `/v1`, and do not append `/chat/completions` twice.
### Model not found
Set `model` to `/auto` or copy an exact current model id from the
[AnonRouter model catalog](https://anonrouter.ai/models). A display name is not
necessarily the API model id.
### Proxy use is unavailable for a character
The character creator may have disabled proxies. AnonRouter counts as a
third-party proxy in Janitor AI, so it cannot bypass that character-level
restriction.
### Rate limit or insufficient credits
For a paid model, check your AnonRouter balance and usage dashboard. `/auto`
and the paid routers need credits. For a free model, topping up does not reset
shared capacity: follow `Retry-After` when present, or explicitly select
another available free model. See [free-model errors](/free-models#troubleshooting).
## Janitor AI proxy setup FAQ
### What is the AnonRouter proxy URL for Janitor AI?
The full Janitor AI proxy URL is
`https://api.anonrouter.ai/v1/chat/completions`. Use that exact URL in the JSON
`apiUrl` field or the manual Proxy URL field.
### Does AnonRouter count as a proxy on Janitor AI?
Yes. Janitor AI treats AnonRouter as a third-party OpenAI-compatible proxy.
That means it works through the Proxy configuration and is unavailable on
characters whose creators have disabled proxy use.
### Can Janitor AI use AnonRouter's `/auto` model?
Yes. Set the JSON `model` field to `/auto`. AnonRouter will choose an eligible
route for each request. To keep every chat on one model, use an exact catalog
model id instead.
### Can I use a normal AnonRouter inference key?
Only if **OpenAI-compatible access** is enabled on that key. Janitor AI cannot
request and attach AnonRouter's single-use ticket itself.
### Is the AnonRouter Janitor AI proxy free?
The integration has no separate setup fee. Paid models use AnonRouter credits
at the selected route's listed prices. To use $0 inference, enable free models
and select `anonrouter/free` or a listed `:free` ID as described above. Free
requests use Anonymous routes, have shared capacity, and never fall back to paid
models.
---
# SillyTavern Setup with AnonRouter
Connect SillyTavern to AnonRouter as a custom OpenAI-compatible Chat Completion API, load models, test the connection, and fix common errors.
SillyTavern
API integration guide
Use AnonRouter as SillyTavern's **Custom (OpenAI-compatible)** Chat Completion
source. SillyTavern assembles your character, persona, chat history, and other
enabled prompt material, then sends the resulting messages through AnonRouter
to the model you select.
> **The short version**
>
> In SillyTavern, open **API Connections**, choose **Chat Completion** and
> **Custom (OpenAI-compatible)**, set the endpoint to
> `https://api.anonrouter.ai/v1`, paste a compatibility-enabled AnonRouter key,
> connect, select a model, and run **Test Message**.
## SillyTavern connection settings
Use these values in SillyTavern's API Connections panel:
| SillyTavern setting | Value |
| ------------------------- | --------------------------------------------------------------------- |
| API | **Chat Completion** |
| Chat Completion Source | **Custom (OpenAI-compatible)** |
| Custom endpoint / API URL | `https://api.anonrouter.ai/v1` |
| API key | A compatibility-enabled AnonRouter inference key beginning with `ar_` |
| Model | An exact id from the loaded model list, or `/auto` |
| Prompt Post-Processing | **None** to start |
| Bypass API status check | Off unless SillyTavern warns even though requests work |
> **Enter the base URL, not the request path**
>
> Use `https://api.anonrouter.ai/v1`. Do **not** enter
> `https://api.anonrouter.ai/v1/chat/completions`; SillyTavern adds the Chat
> Completions path itself. This matches SillyTavern's official guidance for
> custom OpenAI-compatible endpoints.
## Before you begin
You need:
1. A working [SillyTavern installation](https://docs.sillytavern.app/installation/).
2. An AnonRouter account with available credits.
3. An AnonRouter inference key with **OpenAI-compatible access** enabled.
4. A chat-capable model from the [AnonRouter model catalog](https://anonrouter.ai/models),
or AnonRouter's `/auto` route.
Create or edit the inference key in your AnonRouter dashboard and enable
**OpenAI-compatible access**. SillyTavern sends a conventional bearer key and
cannot perform AnonRouter's private single-use ticket handshake itself.
> **Protect the saved key**
>
> SillyTavern saves API keys in the user's `secrets.json` file and hides them in
> the interface after refresh by default. Protect the SillyTavern server and its
> data directory; hiding a key in the UI does not encrypt it on disk.
## Connect AnonRouter in SillyTavern
1. Start SillyTavern and open its web interface.
2. Select **API Connections** in the top bar.
3. Set **API** to **Chat Completion**.
4. Set **Chat Completion Source** to **Custom (OpenAI-compatible)**.
5. Enter `https://api.anonrouter.ai/v1` in the custom endpoint field.
6. Paste your compatibility-enabled AnonRouter inference key into the API key
field.
7. Leave **Additional Parameters** and custom headers empty for the first test.
8. Select **Connect**. AnonRouter supports authenticated `GET /v1/models`, so
SillyTavern should use the response to populate its custom model list.
9. Choose a model id, then select **Test Message**.
10. Select a character and send a normal chat message.
SillyTavern's official custom-endpoint documentation recommends **Test Message**
as the connectivity check. A successful status check only confirms that the
endpoint and credentials work; the test message also confirms that the selected
model can generate a response.
## Choose a model
After connecting, select an exact model id from the list returned by
`GET /v1/models`. Model ids are case-sensitive. Availability and ids can change,
so use the live [model catalog](https://anonrouter.ai/models) rather than typing
a display name from memory.
### Use intelligent routing
Enter `/auto` in SillyTavern's custom Model ID field to let AnonRouter choose an
eligible route for each request. SillyTavern still sends one model id;
AnonRouter performs the route selection behind the API.
### Use a specific model
Choose a specific id when you want chats to stay on one model. For example:
```text
venice/venice-role-play-uncensored
```
Check the model's current context window, pricing, capabilities, moderation
label, and privacy label before using it. The direct catalog page for this
example is
[`venice/venice-role-play-uncensored`](https://anonrouter.ai/models/venice/venice-role-play-uncensored).
### Find uncensored models
Use AnonRouter's moderation filter to review current uncensored routes:
[Browse uncensored models on AnonRouter](https://anonrouter.ai/models?moderation=uncensored)
An uncensored label describes the route's model-level moderation behavior. It
does not make every output accurate or safe, and it does not override
SillyTavern settings or the selected provider's documented limitations.
### Save several model configurations
SillyTavern 1.12.6 and later includes **Connection Profiles** in the API
Connections menu. A profile can save the API type, model, server URL, secret
key selection, settings preset, and Chat Completion post-processing selection.
Create a separate profile for each model you use frequently, then switch
profiles instead of re-entering the endpoint and model.
After changing a saved profile, use its **Update** action. SillyTavern's
official documentation notes that switching away without updating can discard
unsaved profile changes.
## How the request flows
1. **SillyTavern builds the prompt.** It combines enabled character data,
persona information, chat history, lorebook or World Info entries, author's
notes, and other prompt settings into Chat Completion messages.
2. **SillyTavern calls AnonRouter.** Using the `/v1` base URL, it sends the
request to `POST /v1/chat/completions` with your bearer key and selected
model id.
3. **Compatibility mode authenticates the request.** AnonRouter's compatibility
broker validates the static key, creates an internal single-use ticket, and
forwards the request through the normal inference path.
4. **The selected provider generates the reply.** The response is returned to
SillyTavern, either as one response or as a stream, and SillyTavern renders it
in the character chat.
This is a Chat Completion integration. AnonRouter does not expose the legacy
`POST /v1/completions` endpoint, so SillyTavern's **Text Completion** API mode is
not the correct choice for this setup.
## Recommended request settings
Start with a simple text-chat configuration, verify it, and then change one
setting at a time.
| Setting | Recommendation |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Streaming | Supported; enable it if you want tokens to appear as they arrive |
| Prompt Post-Processing | Start with **None**; try **Merge consecutive messages** only if a selected route reports a role-ordering problem |
| Additional body parameters | Leave blank initially; unsupported or misspelled fields can cause a `400` response |
| Number of responses (`n`) | Keep at `1`; AnonRouter does not support `n > 1` |
| Context and output limits | Keep them within the selected model's advertised limits |
| Inline images or multimodal content | Do not use them in this integration; AnonRouter currently requires string message content |
If you add custom body parameters later, use SillyTavern's documented YAML
format and verify each field against [AnonRouter compatibility mode](/compatibility).
Do not add another `Authorization` header; the API key field already supplies
the bearer credential.
## Privacy and key storage
This setup uses [AnonRouter compatibility mode](/compatibility). Unlike the
default private ticket flow, the compatibility broker briefly receives your
API-key identity and plaintext request together in memory. The flow is therefore
not unlinkable, even though AnonRouter does not intentionally retain prompts or
responses.
SillyTavern constructs the request on the SillyTavern server. The request can
include character definitions, your persona, recent chat history, World Info,
author's notes, and your new message. AnonRouter and the selected model provider
receive the content included in that request. Review the route's privacy label
before sending sensitive information.
SillyTavern's official FAQ says saved keys live in the per-user
`secrets.json` file; the default path is `data/default-user/secrets.json`.
Anyone who can read that server filesystem may be able to access the key. Keep
SillyTavern updated, restrict access to the server, and never publish that file
or include a live key in screenshots or support logs.
AnonRouter is an independent service and is not affiliated with SillyTavern.
## Troubleshooting SillyTavern connection errors
### 401 authentication error
The key is missing, malformed, revoked, or includes extra whitespace. Paste the
complete inference key beginning with `ar_`, or create a replacement key if the
original secret is no longer available.
### 403 compatibility error
The inference key does not have OpenAI-compatible access enabled. Turn on that
permission for the key in the AnonRouter dashboard or create a new key with the
option enabled.
### 404 or invalid endpoint URL
Use exactly:
```text
https://api.anonrouter.ai/v1
```
Do not add `/chat/completions`. SillyTavern appends the endpoint path.
### The model list is empty
Confirm the endpoint, key, and compatibility permission, then select
**Connect** again. AnonRouter supports `GET /v1/models`, but that endpoint still
requires a valid bearer key. If the connection works and only SillyTavern's
status warning remains, its official documentation says **Bypass API status
check** can suppress that warning; bypassing does not repair bad credentials.
### Model not found
Reconnect to refresh the model list, select a current exact id, or enter
`/auto`. Do not use a catalog display name as the model id.
### Test Message works, but a character chat fails
The full character request is larger and can include more features than the
test. Reduce the context size, remove image attachments, keep the response count
at one, and clear custom body parameters. SillyTavern's terminal shows the
request-processing error, but review it carefully before sharing because logs
can reveal prompt content.
### 400 invalid request error
Return **Additional Parameters** to blank and test a plain text chat. AnonRouter
rejects unsupported request shapes, including multimodal message arrays and
multiple requested responses.
### Rate limit or insufficient credits
Check the AnonRouter usage dashboard and balance. You can retry later, choose a
different model, or switch to `/auto` after confirming the account has credits.
## SillyTavern setup FAQ
### What URL should I use for AnonRouter in SillyTavern?
Use `https://api.anonrouter.ai/v1` as the custom OpenAI-compatible endpoint.
Do not include `/chat/completions`.
### Which SillyTavern API type should I select?
Select **Chat Completion**, then **Custom (OpenAI-compatible)** as the source.
Do not select Text Completion because AnonRouter does not implement the legacy
Completions endpoint.
### Can SillyTavern use AnonRouter's `/auto` model?
Yes. Enter `/auto` as the custom model id. AnonRouter selects the route while
SillyTavern continues to treat it as one configured model.
### Can I configure several AnonRouter models?
Yes. Select models individually, or create a SillyTavern Connection Profile for
each model and switch between the saved profiles.
### Can I use a normal AnonRouter inference key?
Only when **OpenAI-compatible access** is enabled on that key. SillyTavern
cannot perform AnonRouter's private ticket handshake.
### Does Bypass API status check bypass AnonRouter authentication?
No. It only suppresses SillyTavern's status warning. AnonRouter still validates
the bearer key and its compatibility permission on every request.
## Sources and further reading
This guide was checked against SillyTavern's official documentation and current
release source:
* [SillyTavern: Custom OpenAI-compatible endpoint](https://docs.sillytavern.app/usage/api-connections/openai/)
* [SillyTavern: API Connections and prompt construction](https://docs.sillytavern.app/usage/api-connections/)
* [SillyTavern: Connection Profiles](https://docs.sillytavern.app/usage/core-concepts/connection-profiles/)
* [SillyTavern FAQ: API-key storage](https://docs.sillytavern.app/usage/faq/#where-are-my-api-keys-stored-why-cant-i-see-them)
* [SillyTavern release source: custom Chat Completion settings](https://github.com/SillyTavern/SillyTavern/blob/release/public/scripts/openai.js)
* [AnonRouter compatibility mode](/compatibility)
* [AnonRouter models and privacy labels](/models)
---
# Open WebUI Setup with AnonRouter
Connect Open WebUI to AnonRouter, discover models, run the Docker image with a preset connection, and understand the privacy boundary for RAG.
Open WebUI
Provider integration guide
Use AnonRouter as an **OpenAI-compatible** provider in Open WebUI. Open WebUI
can discover AnonRouter models, send streaming Chat Completions, use supported
tools, and add retrieved document context to a chat.
> **Ready-made connection values**
>
> In **Admin Settings → Connections → OpenAI**, add a connection with URL
> `https://api.anonrouter.ai/v1` and a compatibility-enabled AnonRouter
> inference key. Leave **Model IDs (Filter)** empty to discover models, or add
> only the ids you want users to see.
Open WebUI does not currently document an importable provider-connection file.
Its JSON import/export is for Workspace model presets, which is a different
feature. Use the values above in the connection form or set the documented
environment variables for a new deployment.
## Open WebUI connection settings
| Open WebUI setting | Value |
| ------------------ | --------------------------------------------------------------------- |
| Connection type | **OpenAI** |
| URL | `https://api.anonrouter.ai/v1` |
| API key | A compatibility-enabled AnonRouter inference key beginning with `ar_` |
| Model IDs (Filter) | Optional allowlist of exact AnonRouter model ids |
Use the `/v1` base URL. Open WebUI appends `/models` while verifying the
connection and `/chat/completions` when chatting.
## Before you begin
You need:
1. A working [Open WebUI installation](https://docs.openwebui.com/getting-started/quick-start/).
2. An AnonRouter account with available credits.
3. An inference key with **OpenAI-compatible access** enabled.
Open WebUI uses a conventional bearer key and does not perform AnonRouter's
two-step private ticket exchange. Enable compatibility access only on the key
you intend to use for this connection.
## Connect through the admin interface
1. Sign in to Open WebUI as an administrator.
2. Open **Admin Settings → Connections → OpenAI**.
3. Select **Add Connection**.
4. Enter `https://api.anonrouter.ai/v1` as the URL.
5. Paste your compatibility-enabled `ar_...` key.
6. Optionally add exact model ids under **Model IDs (Filter)**.
7. Save the connection and select an AnonRouter model in a new chat.
Open WebUI verifies an OpenAI-compatible connection with an authenticated
`GET /models` request. AnonRouter implements `GET /v1/models`, so the model
selector should populate automatically. A filter is still useful on a shared
instance because it limits which discovered models appear to users.
To let AnonRouter choose a suitable route, add `/auto` to **Model IDs (Filter)**
and select it in the model picker. To pin a route, copy its exact id from the
[AnonRouter model catalog](https://anonrouter.ai/models).
## Docker example
The Open WebUI image accepts the same provider settings as environment
variables:
```bash
docker run -d \
-p 3000:8080 \
-e OPENAI_API_BASE_URL=https://api.anonrouter.ai/v1 \
-e OPENAI_API_KEY=ar_REPLACE_WITH_YOUR_API_KEY \
-v open-webui:/app/backend/data \
--name open-webui \
--restart always \
ghcr.io/open-webui/open-webui:main
```
Then open `http://localhost:3000`. The named volume preserves Open WebUI data
across container replacements.
These provider variables are persistent configuration values. On an existing
installation, values already stored in Open WebUI's database can override later
environment changes; edit the connection in **Admin Settings** when updating an
established deployment.
> **Treat environment variables as secrets**
>
> A key passed on the command line can be visible in shell history and local
> container metadata. For a shared or production deployment, use the secret
> mechanism provided by your container platform and create a dedicated
> AnonRouter key with an appropriate credit limit.
Because AnonRouter is a remote HTTPS service, Docker does not need
`host.docker.internal` for this connection. That hostname is only relevant when
the provider itself runs on the Docker host.
## Private-RAG setup and limits
Open WebUI's Knowledge feature extracts files, splits them into chunks, embeds
the chunks, and stores vectors for retrieval. When a chat uses a knowledge
base, the relevant text is inserted into the model request.
For the smallest remote-data footprint:
1. Keep Open WebUI's default local SentenceTransformers embedding engine, or
configure another embedding service you control.
2. Create a collection under **Workspace → Knowledge** and upload the files.
3. Attach only the collection needed for the chat, or bind it to a dedicated
model configuration.
4. Review the retrieved citations and route privacy label before using
sensitive material.
This keeps document parsing, vector creation, and vector storage on your Open
WebUI deployment. It does **not** make generation fully local: the retrieved
passages included in a prompt are sent through AnonRouter to the selected model
provider.
### Use AnonRouter for embeddings
AnonRouter also exposes `POST /v1/embeddings` in compatibility mode. If you
choose **OpenAI** as Open WebUI's RAG embedding engine, use the same base URL and
a current embedding-model id from the model catalog. In environment-based
deployments, Open WebUI documents these settings:
```dotenv
RAG_EMBEDDING_ENGINE=openai
RAG_OPENAI_API_BASE_URL=https://api.anonrouter.ai/v1
RAG_OPENAI_API_KEY=ar_REPLACE_WITH_YOUR_API_KEY
RAG_EMBEDDING_MODEL=REPLACE_WITH_AN_EMBEDDING_MODEL_ID
```
Embedding this way sends document chunks to AnonRouter and the selected
embedding provider. Use local embeddings if that is outside your privacy
boundary. After changing embedding models, re-index existing documents so old
and new vectors are not mixed.
## Supported features and limits
| Feature | Status with AnonRouter |
| -------------------------------- | --------------------------------------------------- |
| Model discovery | Supported through `GET /v1/models` |
| Chat Completions | Supported, streaming and non-streaming |
| Tool calling | Supported on tool-capable routes |
| Embeddings | Supported with an embedding model |
| Inline multimodal message arrays | Not supported by the current compatibility contract |
| Legacy text completions | Not supported |
| OpenAI image or audio endpoints | Not exposed through compatibility mode |
Open WebUI can expose settings a selected route ignores or rejects. Start with
plain text chat, confirm the connection, then enable tools or other features one
at a time.
## Privacy and multi-user deployments
This integration uses [compatibility mode](/compatibility). The compatibility
broker receives the stable key identity and plaintext request together long
enough to authorize and forward it. Prompts and responses are not intentionally
retained, but this path is not unlinkable like AnonRouter's native ticket flow.
On a shared Open WebUI server, administrators choose the provider connection
and can make it available to other users. Use a least-privilege inference key,
set a per-key credit limit, restrict admin access, and protect the Open WebUI
database and persistent data volume.
## Troubleshooting
### Connection verification returns 401 or 403
Confirm that the full `ar_...` key is present, has not been revoked, and has
**OpenAI-compatible access** enabled. New keys select it by default; a key
created with the setting off cannot use the static-key endpoint.
### Connection verification returns 404
Use `https://api.anonrouter.ai/v1`, not the full
`/v1/chat/completions` request URL.
### Models are missing
Save the connection again to repeat model discovery. If you set **Model IDs
(Filter)**, only those exact ids appear. Remove a stale id or copy a current id
from the live model catalog.
### A tool or file chat fails
Verify that the selected route supports tool calling and that the resulting
message content remains within AnonRouter's supported Chat Completions shape.
For RAG, test retrieval and ordinary chat separately before combining them.
## Open WebUI setup FAQ
### What URL should I enter?
Use `https://api.anonrouter.ai/v1`.
### Can Open WebUI discover AnonRouter models automatically?
Yes. Open WebUI checks the provider's `/models` route and AnonRouter implements
the corresponding authenticated `GET /v1/models` endpoint.
### Is this the private ticket flow?
No. Open WebUI sends a static bearer key, so the connection uses AnonRouter
compatibility mode.
### Does self-hosting Open WebUI keep every RAG document local?
Local embeddings can keep extraction and vector storage local, but passages
retrieved into a prompt are sent to the remote generation route. External
embeddings also receive the chunks they embed.
## Sources and further reading
* [Open WebUI: connect an OpenAI-compatible provider](https://docs.openwebui.com/getting-started/quick-start/connect-a-provider/starting-with-openai-compatible/)
* [Open WebUI: Knowledge and RAG](https://docs.openwebui.com/features/workspace/knowledge/)
* [Open WebUI: deployment hardening](https://docs.openwebui.com/getting-started/advanced-topics/hardening/)
* [Open WebUI: environment-variable reference](https://docs.openwebui.com/reference/env-configuration/)
* [AnonRouter compatibility mode](/compatibility)
* [AnonRouter models](https://anonrouter.ai/models)
AnonRouter is an independent service and is not affiliated with Open WebUI.
---
# RisuAI Setup with AnonRouter
Install the AnonRouter preview provider plugin for RisuAI and use the native single-use ticket API for character chats.
RisuAI's plugin API can register a custom AI provider and make arbitrary HTTP
requests. The AnonRouter plugin uses that capability to perform the native
two-step ticket flow instead of sending a stable key beside your character
prompt.
> **Native tickets, not compatibility mode**
>
> The plugin sends your `ar_...` key to AnonRouter's control plane to mint a
> short-lived ticket. It then sends the prompt to the relay with only that
> ticket. A normal ticket-only inference key works; **OpenAI-compatible
> access is not required**.
> **Risu Web adds another trust boundary**
>
> RisuAI's web build normally sends plugin network traffic through Risu's
> `/proxy2` service. That intermediary can observe and correlate the ticket and
> completion calls. The desktop runtime sends directly. The plugin preserves
> AnonRouter's control-plane/relay split, but it cannot promise end-to-end
> unlinkability through every RisuAI runtime.
## Download the RisuAI plugin
[Download `anonrouter-native.js`](/integrations/risuai/anonrouter-native.js)
This is an AnonRouter preview integration, not a plugin reviewed or endorsed by
the RisuAI project. The downloadable source uses plugin API v3.0 and is readable JavaScript.
Review it before importing it, especially because it handles an inference key
and sends character-chat content to a remote model.
## Install and configure
1. Download the plugin file above.
2. In RisuAI, open **Plugin Settings → Import Plugin**.
3. Select `anonrouter-native.js`.
4. Open the imported plugin's settings.
5. Set `api_key` to a normal AnonRouter inference key beginning with `ar_`.
6. Set `model` to an exact id from the [AnonRouter model catalog](https://anonrouter.ai/models).
7. In RisuAI's provider/model settings, choose **AnonRouter (Private Ticket
Flow)**.
8. Start with a short character-chat message and confirm the full reply appears.
For roleplay, browse current
[uncensored models](https://anonrouter.ai/models?moderation=uncensored). One
example is
[`venice/venice-role-play-uncensored`](https://anonrouter.ai/models/venice/venice-role-play-uncensored),
but availability and model metadata can change. Copy the live API id instead of
guessing from a display name.
## Already running `0.1.0`? Reimport
> **`0.1.0` cannot generate at all**
>
> Version `0.1.0` sent the ticket request to the wrong origin. It called
> `POST https://api.anonrouter.ai/v1/inference/tickets`, which the content edge
> does not route, so every generation failed at the first call with
> `AnonRouter: ticket request failed: HTTP 404` and the completion never ran.
> Version `0.1.1` mints against `https://control.anonrouter.ai/v1` and is the
> first version that works.
The plugin header carries an `//@update-url` pointing back at this site, and
RisuAI checks it by reading the first bytes of the file and comparing
`//@version` against the copy you have installed. `0.1.1` is greater than
`0.1.0`, so the check has something to offer.
**Reimporting by hand is the path that always works**, and it is the one to use
if you are not sure:
1. Download `anonrouter-native.js` from the link above again.
2. In RisuAI, open **Plugin Settings**, remove the existing **AnonRouter
(Private Ticket Flow)** plugin, and import the downloaded file.
3. Re-enter `api_key` and `model`; plugin arguments do not survive a reimport.
4. Reselect the provider and send one short message to confirm a reply arrives.
Two things worth knowing about the automatic check, so a silent result is not
mistaken for "there is no update":
* **RisuAI's web build makes this check from browser script**, which means it is
subject to the update URL's cross-origin headers. If the check appears to do
nothing on Risu Web, reimport by hand.
* **RisuAI only offers an update when the published version is strictly
greater** than the installed one. Two installs of the same version number
never prompt, whatever the file contains, which is why the corrected plugin
ships as a new version rather than as a quiet replacement of `0.1.0`.
## What the plugin does
For every generation, the provider handler receives RisuAI's OpenAI-format
`prompt_chat` array and generation settings. It then:
1. Calls `POST https://control.anonrouter.ai/v1/inference/tickets` with the
inference key, model id, and output-token limit, but no prompt.
2. Reads the short-lived, single-use ticket from the response.
3. Calls `POST https://api.anonrouter.ai/v1/chat/completions` with the prompt,
model id, matching token limit, and `x-anonrouter-ticket` header, but no key.
4. Returns the assistant's text to RisuAI.
This is the same trust separation documented in [AnonRouter
authentication](/authentication): the control plane sees key identity without
content, while the relay sees content with an opaque one-use ticket.
The plugin sends the bearer header only on the ticket request and passes
RisuAI's cancellation signal to both calls. RisuAI's declared
`saveSecretHeader` API is not implemented in the current runtime, so this
version cannot use it as write-only credential storage.
## Current plugin scope
Version `0.1.1` deliberately keeps the smallest dependable provider
surface:
| Capability | Status |
| ------------------------------------- | ------------------------------------------- |
| Current plugin version | `0.1.1` |
| RisuAI plugin API | v3.0 |
| AnonRouter authentication | Native ticket flow |
| Text character chat | Supported |
| Temperature and maximum output tokens | Supported |
| Cancellation | Supported |
| Streaming display | Not yet; the full reply is returned at once |
| Vision/inlay attachments | Not forwarded |
| Tool calls | Not exposed by this provider version |
| Automatic model discovery | Not included; enter one exact model id |
Keeping the model as a plugin argument makes the privacy binding explicit: the
same id is sent during ticket issuance and completion. Change the argument when
you want to switch models.
## Key and content handling
The plugin itself runs in RisuAI's sandboxed plugin environment. The `api_key`
argument is currently an ordinary visible text input stored in RisuAI's plugin
configuration, not a secure, write-only secret field. The plugin can read it to
request a ticket. Protect the device, RisuAI data, and plugin settings as you
would any client that holds an API credential.
RisuAI asks for periodic permission before a plugin provider runs. That consent
dialog does not encrypt the saved key and does not change the network path.
The key is not attached to the completion request. RisuAI's generated prompt
can still include character definitions, persona details, lorebook entries,
memory, chat history, and your latest message. That content reaches AnonRouter's
relay and the selected provider.
AnonRouter does not intentionally retain prompts or responses. Review the
selected model's privacy label before sending sensitive material.
## Why this is not AnonRouter compatibility mode
RisuAI can also call ordinary OpenAI-compatible endpoints. That setup
would require a compatibility-enabled key and would put key identity and
plaintext together at AnonRouter's compatibility broker. The plugin exists so
RisuAI can use the stronger native ticket separation its custom-provider API
makes possible.
## Troubleshooting
### The provider does not appear
Confirm the file was imported through **Plugin Settings**, the plugin is
enabled, and its metadata begins with `//@api 3.0`. Reload plugins or restart
RisuAI after upgrading from an older plugin API.
### “Add a valid ar\_ inference key”
Paste the complete inference key into the plugin's `api_key` argument. A
management key beginning with `arm_` cannot run inference.
### “Ticket request failed: HTTP 404”
You are running `0.1.0`, which mints against the wrong origin. No key, model or
setting can fix it. Reimport the plugin as described in
[Already running `0.1.0`?](#already-running-010-reimport).
### Ticket request returns 401 or 402
The key may be malformed, revoked, out of credits, or at its credit limit. A
compatibility permission is not needed for this plugin.
### Ticket model mismatch
Enter an exact current model id. The plugin binds the same id and output limit
to both requests; a stale or unavailable route can still be rejected.
### The reply is delayed and appears all at once
That is expected in version `0.1.1`. RisuAI's provider API can accept a
`ReadableStream`, but this plugin uses a non-streaming completion to
keep ticket handling and error reporting straightforward.
## RisuAI setup FAQ
### Do I need to enable OpenAI-compatible access?
No. The plugin performs the native ticket exchange, so a standard inference key
is the preferred credential.
### Does this preserve AnonRouter unlinkability?
It preserves the separation inside AnonRouter: the control plane receives the
key without the prompt, and the relay receives the prompt with only a
single-use ticket. The local client necessarily has both. Risu Web's default
network proxy can also see both calls, so use a direct desktop runtime when that
additional correlation boundary matters.
### Can the plugin contain several models?
This version uses one configurable model id. Change the plugin argument to
switch routes. Automatic discovery and a model picker are possible future
enhancements, not current behavior.
## Sources and further reading
* [RisuAI Plugin Development Guide](https://github.com/kwaroran/RisuAI/blob/main/plugins.md)
* [RisuAI plugin API v3 type definitions](https://github.com/kwaroran/RisuAI/blob/main/src/ts/plugins/apiV3/risuai.d.ts)
* [RisuAI plugin runtime](https://github.com/kwaroran/RisuAI/blob/main/src/ts/plugins/apiV3/v3.svelte.ts)
* [RisuAI network implementation](https://github.com/kwaroran/RisuAI/blob/main/src/ts/globalApi.svelte.ts)
* [AnonRouter authentication and tickets](/authentication)
* [AnonRouter model catalog](https://anonrouter.ai/models)
AnonRouter is an independent service and is not affiliated with RisuAI.
---
# Chub AI Reverse Proxy Setup with AnonRouter
Connect Chub AI to AnonRouter through its OpenAI reverse-proxy option, load models, choose uncensored routes, and understand the privacy tradeoff.
Chub AI supports an OpenAI reverse-proxy connection for character chat.
Configure that connection with AnonRouter's `/v1` base URL, a
compatibility-enabled inference key, and an exact model id.
> **Connection values**
>
> Use base URL `https://api.anonrouter.ai/v1`, a compatibility-enabled
> `ar_...` bearer key, and a current model id from the AnonRouter catalog. Chub
> adds `/models` and `/chat/completions`; do not paste the full completion URL.
## Chub connection settings
| Chub setting | Value |
| ------------------------ | --------------------------------------------------------------------- |
| API family | **OpenAI** |
| OpenAI access method | **Reverse Proxy** |
| Reverse-proxy base URL | `https://api.anonrouter.ai/v1` |
| Reverse-proxy bearer key | A compatibility-enabled AnonRouter inference key beginning with `ar_` |
| Model | An exact AnonRouter model id |
Chub's displayed labels can change between web and app builds. If the form uses
slightly different wording, match each input by purpose: the proxy's base URL,
its bearer credential, and the model id.
## Connect AnonRouter in Chub AI
1. Open a character chat in Chub.
2. Open the top-right chat menu and select **Secrets**.
3. Expand the **OpenAI** connection.
4. Under **OpenAI Access API**, choose **Reverse Proxy** instead of **My Own API
Key**.
5. Enter `https://api.anonrouter.ai/v1` in the reverse-proxy URL input.
6. Enter your compatibility-enabled `ar_...` key in the reverse-proxy token or
key input.
7. Choose an exact AnonRouter model id.
8. Save the connection and send a short test message.
The current Chub implementation requests `GET {base}/models` with the bearer
key for discovery and sends character generations to `POST {base}/chat/completions`. AnonRouter supports both corresponding `/v1` routes.
> **Enable OpenAI-compatible access**
>
> Chub sends one stable bearer key with each chat request and cannot perform
> AnonRouter's private ticket exchange. Enable **OpenAI-compatible access** on
> this inference key or the request will be refused.
## Choose a roleplay model
Use the live [AnonRouter model catalog](https://anonrouter.ai/models) to compare
context windows, prices, capabilities, moderation labels, and privacy labels.
For routes labeled uncensored, use the catalog's
[uncensored filter](https://anonrouter.ai/models?moderation=uncensored).
One roleplay-focused example is:
```text
venice/venice-role-play-uncensored
```
Its direct catalog page is
[`venice/venice-role-play-uncensored`](https://anonrouter.ai/models/venice/venice-role-play-uncensored).
Availability and metadata can change, so use the current API id shown by Chub's
model list or the live catalog.
An **uncensored** label describes the route's model-level moderation behavior.
It is not a guarantee that every response is accurate, safe, or suitable for
every use.
## Prompt and generation behavior
Chub can include more than the latest message in its request. Depending on your
chat and configuration, the prompt can contain:
* System and post-history instructions.
* Character definitions and example dialogue.
* Persona details and chat history.
* Activated lorebook entries and chat memory.
* Generation settings such as temperature, top-p, and penalties.
Start with ordinary text chat and conservative default settings. AnonRouter's
current compatibility contract requires string message content, keeps `n` at
one, and does not expose legacy text completions. Do not rely on inline image or
audio content through this connection.
## Privacy and key storage
Chub's official documentation warns that reverse proxies can see IP addresses,
keys, and chat content and may log or leak them. AnonRouter is the proxy in this
configuration, so review [AnonRouter's compatibility-mode tradeoff](/compatibility)
instead of treating any proxy as automatically private.
The compatibility broker temporarily receives key identity and plaintext
request together. AnonRouter does not intentionally retain prompts or
responses, but this is not the unlinkable native ticket flow.
Chub stores the reverse-proxy bearer token for reuse by the client. Protect your
Chub account and device, use a dedicated AnonRouter key with a credit limit,
and revoke it if it appears in a screenshot, export, or support log.
## Troubleshooting
### 401 or 403 response
Confirm the complete `ar_...` inference key is in the reverse-proxy token field
and **OpenAI-compatible access** is enabled for that key.
### 404 response
Use `https://api.anonrouter.ai/v1`. Chub appends `/models` and
`/chat/completions`; entering the full request path causes it to construct the
wrong URL.
### Model list does not load
Recheck the base URL and bearer key, then reopen the model selector. If Chub
allows manual entry in your build, paste an exact current API id from the
AnonRouter catalog.
### Model not found
The saved id is stale or is a display name rather than an API id. Refresh the
list or choose another current route.
### A chat fails after a simple test worked
The assembled character prompt may exceed the route's context window or
include an unsupported parameter or content type. Reduce context, disable
attachments, and return generation settings to their defaults before testing
again.
## Chub AI setup FAQ
### Is AnonRouter entered as an OpenAI API key?
Choose the **OpenAI → Reverse Proxy** path. Enter AnonRouter's base URL and the
separate AnonRouter bearer key; do not point Chub at OpenAI's service.
### What URL should I use?
Use `https://api.anonrouter.ai/v1`, without `/chat/completions`.
### Can I use a normal ticket-only AnonRouter key?
No. Chub uses a static bearer key, so that key must have **OpenAI-compatible
access** enabled.
### Is AnonRouter affiliated with Chub AI?
No. This is a third-party reverse-proxy integration using Chub's documented
OpenAI connection option.
## Sources and further reading
* [Chub AI: API Connections](https://docs.chub.ai/docs/the-basics/api-connections)
* [Chub AI: character-chat settings](https://docs.chub.ai/docs/the-basics/just-chatting)
* [AnonRouter compatibility mode](/compatibility)
* [AnonRouter uncensored models](https://anonrouter.ai/models?moderation=uncensored)
AnonRouter is an independent service and is not affiliated with Chub AI.
---
# LibreChat Setup with AnonRouter
Add AnonRouter to LibreChat with a ready-made custom endpoint YAML configuration, model discovery, user-provided keys, and privacy guidance.
LibreChat
Custom endpoint guide
LibreChat supports OpenAI-compatible services as custom endpoints. Add
AnonRouter in `librechat.yaml`, keep the API key in `.env`, and restart
LibreChat to make the provider appear in the endpoint selector.
> **Ready-made configuration**
>
> Copy the YAML and environment-variable snippets below, replace the example
> key, mount `librechat.yaml` when using Docker, and restart LibreChat.
## Shared-key configuration
Add this custom endpoint to `librechat.yaml`:
```yaml
version: 1.3.13
cache: true
endpoints:
custom:
- name: "AnonRouter"
apiKey: "${ANONROUTER_API_KEY}"
baseURL: "https://api.anonrouter.ai/v1"
models:
default:
- "/auto"
- "venice/venice-role-play-uncensored"
fetch: true
titleConvo: true
titleModel: "/auto"
modelDisplayLabel: "AnonRouter"
```
Then place the key in LibreChat's `.env` file:
```dotenv
ANONROUTER_API_KEY=ar_REPLACE_WITH_YOUR_API_KEY
```
The key must be an AnonRouter inference key with **OpenAI-compatible access**
enabled. LibreChat sends it as a normal bearer credential.
The two models under `default` are safe initial entries for the selector.
Because `fetch: true` asks the provider for its model list, LibreChat can also
load the current ids from AnonRouter's authenticated `GET /v1/models` endpoint.
Remove the roleplay example if it is not appropriate for your users.
## Let each user provide a key
For a multi-user LibreChat deployment, replace the `apiKey` line with:
```yaml
apiKey: "user_provided"
```
LibreChat then asks each user for their own provider key instead of sharing the
server operator's AnonRouter key. Every user still needs an inference key with
OpenAI-compatible access enabled.
> **Choose the credential model deliberately**
>
> An environment-variable key centralizes billing and makes every permitted
> LibreChat user spend from the same key. `user_provided` separates credentials
> and balances by user. In either case, use per-key credit limits and do not put
> a live key directly in `librechat.yaml`.
## Docker mount
LibreChat's official Docker setup uses an override file to mount the config.
Ensure `docker-compose.override.yml` contains:
```yaml
services:
api:
volumes:
- type: bind
source: ./librechat.yaml
target: /app/librechat.yaml
```
Apply the changes with:
```bash
docker compose down
docker compose up -d
```
For a non-Docker local installation, stop the backend and start it again after
editing the files.
## How the fields map to AnonRouter
| LibreChat field | Purpose |
| ------------------- | ------------------------------------------------------- |
| `name` | Label shown in LibreChat's endpoint selector |
| `apiKey` | `.env` substitution or `user_provided` sentinel |
| `baseURL` | AnonRouter's OpenAI-compatible `/v1` base URL |
| `models.default` | Initial exact model ids shown by LibreChat |
| `models.fetch` | Fetch additional current ids from `/v1/models` |
| `titleModel` | Model used when LibreChat generates conversation titles |
| `modelDisplayLabel` | Provider label shown beside models |
Do not set `provider: "anthropic"`. AnonRouter exposes OpenAI-compatible Chat
Completions even when the selected model was created by Anthropic.
## Choose models
Use `/auto` when you want AnonRouter to select an eligible route for each
request. Use exact provider-qualified ids when you want a fixed model.
Browse the [live model catalog](https://anonrouter.ai/models) for current
pricing, context windows, features, and privacy labels. For character chat,
the catalog can be filtered to
[uncensored models](https://anonrouter.ai/models?moderation=uncensored).
Model availability changes over time. Keep `fetch: true`, and update stale
entries under `models.default` if a pinned route is removed or renamed.
## Feature compatibility
| LibreChat feature | AnonRouter behavior |
| --------------------- | ------------------------------------------------------------ |
| Streaming chat | Supported |
| Model discovery | Supported with `models.fetch: true` |
| Tool calling | Supported on routes that advertise tools |
| User-provided keys | Supported by LibreChat configuration |
| Vision message arrays | Not supported by AnonRouter's current compatibility contract |
| Legacy completions | Not supported |
LibreChat supports more custom-endpoint options than AnonRouter honors. Begin
with the minimal configuration above. If a feature fails, compare the emitted
request with [AnonRouter's compatibility contract](/compatibility).
## Privacy and key storage
This custom endpoint uses [compatibility mode](/compatibility), not the native
single-use ticket flow. The compatibility broker temporarily receives key
identity and plaintext request content together. Prompts and responses are not
intentionally retained, but the request is not unlinkable at that hop.
LibreChat may add chat history, memories, tool output, file excerpts, or other
enabled context to a request. Self-hosting LibreChat keeps its application data
under your control; it does not prevent the content included in a model request
from reaching AnonRouter and the selected provider.
## Troubleshooting
### AnonRouter does not appear in the selector
Check the three-file chain: `librechat.yaml` contains the endpoint, `.env`
contains every referenced variable, and Docker mounts the YAML into
`/app/librechat.yaml`. Then restart LibreChat and inspect `docker compose logs
api` for YAML or environment errors.
### 401 or 403 response
Verify the complete `ar_...` key and enable **OpenAI-compatible access** on that
inference key. With `user_provided`, confirm that the affected user saved the
correct key.
### No models appear
Keep `models.fetch: true` and include at least `/auto` under `models.default`.
Confirm the base URL ends in `/v1` and that the key can call `GET /v1/models`.
### A model returns 404
Replace stale ids with current exact values from the model catalog. Display
names are not API ids.
## LibreChat setup FAQ
### Can all users share one AnonRouter key?
Yes. Reference one `.env` variable in `apiKey`. That also shares its balance
and spend limit, so it is not the safest default for an untrusted public server.
### Can each user enter a separate key?
Yes. Set `apiKey: "user_provided"` for the custom endpoint.
### What base URL should I use?
Use `https://api.anonrouter.ai/v1`.
### Is an Anthropic model configured as an Anthropic endpoint?
No. Keep this as the default OpenAI-compatible custom endpoint. The wire format
depends on AnonRouter's endpoint, not the company that created the model.
## Sources and further reading
* [LibreChat: Custom Endpoints](https://www.librechat.ai/docs/quick_start/custom_endpoints)
* [LibreChat: `librechat.yaml` configuration](https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/custom_endpoint)
* [AnonRouter compatibility mode](/compatibility)
* [AnonRouter models](https://anonrouter.ai/models)
AnonRouter is an independent service and is not affiliated with LibreChat.
---
# Jan Desktop Setup with AnonRouter
Connect Jan Desktop to AnonRouter as a custom OpenAI-compatible provider, discover models, configure capabilities, and troubleshoot keys and endpoints.
Jan Desktop can connect to AnonRouter as a custom provider using the OpenAI
wire format. Jan fetches available models from AnonRouter and lets you configure
model capabilities that a generic endpoint cannot advertise automatically.
> **The short version**
>
> Open **Settings → Model Providers**, add an **OpenAI-compatible** provider,
> name it **AnonRouter**, use `https://api.anonrouter.ai/v1`, and paste a
> compatibility-enabled AnonRouter inference key.
## Jan custom-provider settings
| Jan setting | Value |
| ------------- | --------------------------------------------------------------------- |
| Provider name | `AnonRouter` |
| API format | **OpenAI-compatible** |
| Base URL | `https://api.anonrouter.ai/v1` |
| API key | A compatibility-enabled AnonRouter inference key beginning with `ar_` |
Do not choose **Anthropic-compatible**, including when you plan to use an
Anthropic-created model. Jan's API-format setting describes the endpoint's wire
protocol, and AnonRouter uses OpenAI Chat Completions here.
## Connect AnonRouter
1. Open Jan Desktop.
2. Go to **Settings → Model Providers**.
3. Select **Add Provider** beside the provider list.
4. Choose **OpenAI-compatible**.
5. Enter `AnonRouter` as the provider name.
6. Enter `https://api.anonrouter.ai/v1` as the base URL.
7. Paste your compatibility-enabled `ar_...` inference key.
8. Create the provider and wait for Jan to load its models.
9. Select an AnonRouter model in a new chat and send a short test message.
Jan tries `GET {base_url}/models` when the provider is saved. AnonRouter exposes
authenticated `GET /v1/models`, so automatic discovery should work. If needed,
open the provider's **Models** section, select **+**, and enter an exact model id
manually.
## Add `/auto` or a pinned model
Add `/auto` manually if you want AnonRouter to choose an eligible route for
each request. For a fixed route, copy its exact id from the
[AnonRouter model catalog](https://anonrouter.ai/models).
Jan does not infer tools, vision, or audio support for custom-provider models.
Configure only capabilities that both Jan and the selected AnonRouter route
support. AnonRouter's current compatibility endpoint accepts text message
content; do not mark a model as vision-capable for this integration.
## Multiple keys and fallback behavior
Jan supports several keys for one remote provider. In the AnonRouter provider,
open **API keys → Advanced**, add numbered keys, and use **Test keys**.
Jan tests each key against `{base_url}/models` and moves to a fallback only
after HTTP `401`, `403`, or `429`. It does not retry another key for every error.
If you add fallbacks, make each one compatibility-enabled and give it a clear
name and credit limit in the AnonRouter dashboard.
> **Fallback keys can multiply exposure**
>
> Jan may continue with another key after authentication, permission, or rate
> errors. A fallback should not be treated as a billing retry. Set a separate
> AnonRouter credit limit on every key and revoke keys you no longer use.
## Sampling parameters
Jan exposes many sampling controls for custom endpoints. AnonRouter accepts a
defined subset of OpenAI Chat Completions fields, and some selected providers
may ignore optional values.
Start with the defaults. Keep `n` at `1`, avoid `logprobs`, `top_logprobs`, and
`logit_bias`, and add supported controls one at a time. If Jan sends an
unsupported field, remove that sampler before diagnosing the model itself.
## Privacy and local-app expectations
Jan runs on your desktop, but this configuration calls a remote model service.
Messages, enabled history, tool results, and other context included in a request
leave the device.
The static key also means this setup uses [AnonRouter compatibility
mode](/compatibility). The broker temporarily sees key identity and plaintext
request together, so this is not the unlinkable native ticket flow. Prompts and
responses are not intentionally retained by AnonRouter.
## Troubleshooting
### Jan reports 401 or 403
Paste the full `ar_...` key and confirm **OpenAI-compatible access** is enabled.
Jan's **Test keys** action reports each key independently.
### Requests return 404
Use the base URL `https://api.anonrouter.ai/v1`. Do not omit `/v1` and do not
append `/chat/completions`; Jan builds the request path.
### No models are listed
Save the provider again and test the key. If discovery still fails, add `/auto`
or another current exact model id manually in the provider's **Models** section.
### Tools or attachments fail
Review the model capabilities you enabled in Jan. Tool calling requires a
tool-capable route; inline vision/audio content is not supported by
AnonRouter's current compatibility contract.
## Jan Desktop setup FAQ
### Which API format should I choose?
Choose **OpenAI-compatible**.
### What base URL should I use?
Use `https://api.anonrouter.ai/v1`. Jan strips trailing slashes automatically.
### Will Jan discover the models?
It should. Jan checks `{base_url}/models`, and AnonRouter exposes that endpoint
for authenticated inference keys.
### Does Jan know which models support tools or vision?
Not automatically for a custom provider. Configure model capabilities
manually, and only enable features supported by this integration.
## Sources and further reading
* [Jan: Custom Endpoints](https://www.jan.ai/docs/desktop/remote-models/custom-endpoint)
* [AnonRouter compatibility mode](/compatibility)
* [AnonRouter models](https://anonrouter.ai/models)
AnonRouter is an independent service and is not affiliated with Jan.
---
# Hermes Agent Setup with AnonRouter
Configure Hermes Agent for AnonRouter with a named custom provider, safe tool settings, Docker examples, model requirements, and spend controls.
Hermes Agent
Autonomous-agent integration guide
Hermes Agent can use AnonRouter through a named OpenAI Chat Completions
provider. Because an agent can read files, browse, call tools, and run for long
periods, use a dedicated key, a tool-capable model, and explicit safety limits.
> **Set a spending boundary first**
>
> Create a dedicated compatibility-enabled AnonRouter key for Hermes and give
> it a conservative **total credit limit**. AnonRouter does not currently offer
> self-service recurring daily or monthly limits per key, so treat the total
> cap as a circuit breaker and monitor unattended use.
## Recommended setup with `hermes model`
1. Run `hermes model`.
2. Select **Custom endpoint**.
3. Enter `https://api.anonrouter.ai/v1` as the API base URL.
4. Enter a compatibility-enabled AnonRouter `ar_...` inference key.
5. Enter an exact model id from the [AnonRouter catalog](https://anonrouter.ai/models).
6. Enter the model's advertised context length.
Hermes saves the provider and model in `~/.hermes/config.yaml`. The current
Hermes provider guide recommends this custom-endpoint flow over relying only on
`OPENAI_BASE_URL`.
## Ready-made configuration
For an explicit named provider, add this to `~/.hermes/config.yaml`:
```yaml
providers:
anonrouter:
api: https://api.anonrouter.ai/v1
key_env: ANONROUTER_API_KEY
transport: chat_completions
model:
provider: custom:anonrouter
default: meta-llama/llama-3.3-70b
```
Store the key separately in `~/.hermes/.env`:
```dotenv
ANONROUTER_API_KEY=ar_REPLACE_WITH_YOUR_API_KEY
```
Replace the example model after checking its current context window and tool
capabilities. Inside a running Hermes session, switch among already-configured
models with:
```text
/model custom:anonrouter:meta-llama/llama-3.3-70b
```
The `/model` command does not add credentials for a new provider; configure the
provider first with `hermes model` or `config.yaml`.
## Base URL and configuration precedence
Hermes treats `https://api.anonrouter.ai/v1` as the API root and calls:
```text
GET https://api.anonrouter.ai/v1/models
POST https://api.anonrouter.ai/v1/chat/completions
```
Do not put `/chat/completions` in the provider `api` value.
Hermes documents configuration precedence in this order:
1. Command-line arguments.
2. `~/.hermes/config.yaml`.
3. `~/.hermes/.env`.
4. Built-in defaults.
Keep non-secret provider configuration in YAML and the key in `.env`. The
Hermes environment-variable reference still mentions `OPENAI_BASE_URL`, while
the newer provider guide limits that variable to its `openai-api` provider.
The named custom provider above avoids that ambiguity.
## Choose an agent-capable model
Hermes documents a minimum context window of **64,000 tokens** for its full
agent and tool workflow. Its system prompt, tool schemas, and working state can
make smaller contexts unreliable, and current releases may refuse them during
startup.
Before selecting a route:
1. Open its live AnonRouter model page.
2. Confirm the context window is at least 64,000.
3. Confirm tool calling is supported.
4. Use the exact API model id, not the display name.
5. Set `model.context_length` if Hermes cannot detect the value correctly.
AnonRouter forwards tool definitions and tool-call responses; Hermes executes
the tools locally or in its configured execution backend. Do not enable vision
for this custom provider because AnonRouter's current compatibility contract
requires string message content.
> **Reasoning controls are version-sensitive**
>
> A current Hermes issue reports that `agent.reasoning_effort` can be dropped
> for generic custom providers. Verify the emitted request in your installed
> Hermes version before relying on that setting for an AnonRouter model.
## Guard unattended tool loops
Hermes' Docker guidance recommends hard stops for unattended gateways. Add:
```yaml
tool_loop_guardrails:
hard_stop_enabled: true
hard_stop_after:
exact_failure: 5
idempotent_no_progress: 5
```
This stops repeated failing or non-progressing tool loops. It is not a monetary
limit and does not replace the AnonRouter key cap.
For high-impact tools, also use the narrowest filesystem, credential, and
network permissions that let the agent complete its job. Never give an
unattended process a management key.
## Docker example
Hermes' official image keeps mutable state under `/opt/data`. Initialize the
host directory and run setup:
```bash
mkdir -p ~/.hermes
docker run -it --rm \
-v ~/.hermes:/opt/data \
nousresearch/hermes-agent setup
```
Place the named-provider configuration and `.env` file in the mounted Hermes
directory. Start an interactive session with:
```bash
docker run -it --rm \
-v ~/.hermes:/opt/data \
nousresearch/hermes-agent
```
For a deliberately configured unattended gateway:
```bash
docker run -d \
--name hermes \
--restart unless-stopped \
-v ~/.hermes:/opt/data \
nousresearch/hermes-agent gateway run
```
Your container platform can inject `ANONROUTER_API_KEY` directly instead of
keeping it in `.env`. Avoid placing a live key in shell history or a committed
Compose file.
## Current AnonRouter spending controls
AnonRouter supports an optional **lifetime credit limit** on each inference
key. Before dispatch, the billing path accounts for successful usage and
reserved or unsettled usage and rejects a request that would cross the cap.
You can set the limit while creating the key and update it later.
That is different from a recurring budget:
| Control | Self-service per inference key today |
| ------------------ | ------------------------------------ |
| Total credit limit | Yes |
| Daily reset | No |
| Monthly reset | No |
AnonRouter has account-level daily budgets for some usage plans, but these are
not user-configurable recurring limits for one Hermes key. Before broad
promotion to fully unattended agents, per-key daily or monthly ceilings would
provide a stronger operational boundary. This guide does not claim they already
exist.
## Privacy boundary
Hermes sends the prompt, tool schemas, selected file content, tool results, and
other active context to its model provider. Inspect configured tools before
using a mailbox, calendar, browser profile, credential store, or private files.
This setup uses [AnonRouter compatibility mode](/compatibility). The broker
temporarily sees the static key identity and plaintext request together. The
content is not intentionally retained, but the request is not unlinkable like
the native ticket flow.
## Troubleshooting
### 401 or 403 response
Verify the full `ar_...` key and enable **OpenAI-compatible access**. Confirm
that the `key_env` name exactly matches the variable in `.env` or Docker.
### 404 response
Use `https://api.anonrouter.ai/v1` as the provider API root. Do not append the
completion path.
### Hermes rejects the model at startup
Check the live context window. Choose a route with at least 64,000 tokens or
set the correct `model.context_length` when discovery is inaccurate.
### Tools never execute
Confirm the selected AnonRouter route supports tools. AnonRouter returns tool
calls, while Hermes is responsible for executing them and sending results back.
### The key hits its cap
Review usage before raising the limit. A loop guard protects against repeated
tool behavior; a credit cap protects billing. Diagnose the agent instead of
automatically rotating to an unlimited fallback key.
## Hermes Agent setup FAQ
### Should I use `OPENAI_BASE_URL`?
Use `hermes model` or the named custom-provider YAML shown above. Current Hermes
documentation is inconsistent about the generic scope of `OPENAI_BASE_URL`.
### Can I use `/auto`?
Hermes needs a tool-capable model with a sufficiently large context window.
For predictable agent behavior, pin a verified model rather than assuming every
route eligible for `/auto` meets Hermes' requirements.
### Are there per-key daily or monthly limits?
Not currently as a self-service feature. Use a dedicated key with a conservative
total credit limit and enable Hermes tool-loop guardrails.
## Sources and further reading
* [Hermes Agent: AI providers and custom endpoints](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/integrations/providers.md)
* [Hermes Agent: configuration and precedence](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/user-guide/configuration.md)
* [Hermes Agent: environment variables](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/reference/environment-variables.md)
* [Hermes Agent: Docker](https://hermes-agent.nousresearch.com/docs/user-guide/docker/)
* [Hermes Agent: custom-provider reasoning issue](https://github.com/NousResearch/hermes-agent/issues/55276)
* [AnonRouter compatibility mode](/compatibility)
AnonRouter is an independent service and is not affiliated with Nous Research
or Hermes Agent.
---
# API reference
The public HTTP surface for AnonRouter integrations.
The production base URL for OpenAI-compatible clients is:
```text
https://api.anonrouter.ai/v1
```
That is the only URL ordinary tools such as OpenCode, SillyTavern, and OpenAI
SDK clients need. The private ticket flow additionally uses:
```text
https://control.anonrouter.ai/v1
```
The control origin receives keys and content-free account or authorization
requests. The confidential API origin receives inference content and
attestation requests. The AnonRouter SDK keeps the two origins separate
automatically.
Requests authenticate one of three ways, depending on the endpoint:
* **Inference key** (`ar_`) in `Authorization: Bearer` to issue tickets and, in
compatibility mode, to call inference directly.
* **Management key** (`arm_`) in `Authorization: Bearer` to read balance and
usage.
* **Inference ticket** in the `x-anonrouter-ticket` header to run a single
ticketed request.
Some account and billing operations require an authenticated session and are
performed from the dashboard rather than with an API key.
## Inference
| Method | Endpoint | Origin | Credential | Description |
| ------ | --------------------- | ---------------------------------- | --------------------------- | --------------------------------------------------- |
| `GET` | `/models` | Control; API in compatibility mode | Inference or management key | List enabled models and capabilities |
| `GET` | `/catalog/models` | Control | None | Public, cacheable catalog with privacy metadata |
| `GET` | `/models/uptime` | Control | None | Passive success-rate and latency series |
| `POST` | `/inference/tickets` | Control | Inference key | Issue a single-use inference ticket |
| `POST` | `/chat/completions` | API | Ticket | Create a streaming or non-streaming chat completion |
| `POST` | `/embeddings` | API | Ticket | Create vector embeddings |
| `POST` | `/images/generations` | API | Ticket | Generate an image (when enabled) |
| `POST` | `/audio/speech` | API | Ticket | Synthesize speech (when enabled) |
| `GET` | `/capabilities` | Control | None | Report enabled media feature flags |
`/models/uptime` requires a `?model=` query parameter and answers `400
missing_model` without one.
In [compatibility mode](/compatibility), `/chat/completions` and
`/embeddings` accept an inference key directly instead of a ticket.
A chat completion can include the managed `anonrouter:web_search` tool. In the
ticket flow, issue the ticket with `web_search: { "max_uses": n }`. See
[web search](/web-search).
> **How the confidential origin decides which flow you are in**
>
> `api.anonrouter.ai` dispatches on the `Authorization` header. Send a ticket and
> no bearer token and you are in the private ticket flow; send an `ar_` bearer
> token and you are in compatibility mode, which is why `GET /v1/models` answers
> `404` there without a credential and an OpenAI-shaped `401 invalid_api_key`
> with one. Control-plane routes are not served on that origin in either flow.
## Attestation
| Method | Endpoint | Origin | Credential | Description |
| ------ | ----------------------------------- | ------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `GET` | `/gateway/attestation` | API | None | Attest AnonRouter's own confidential plane (hop 1); requires `?nonce=` of exactly 64 hex characters |
| `GET` | `/tee/attestation` | Control | Inference key | Fetch provider enclave evidence for a model and provider (hop 2) |
| `POST` | `/tee/attestation` | API | Attestation ticket | The same evidence without presenting a key to the content plane; send the ticket in `x-anonrouter-ticket` and the nonce in the JSON body |
| `GET` | `/tee/signature/:providerRequestId` | Control | Inference key | Retrieve a per-request enclave receipt where the provider exposes one |
| `POST` | `/inference/attestation-tickets` | Control | Inference key | Mint a model-bound ticket for the ticketed attestation call |
See [TEE verification](/tee-verification) for what each document proves.
The routing, account, workspace, billing, and key-management endpoints below
are control-plane operations and use `https://control.anonrouter.ai/v1` unless
the page says otherwise.
## Routing
| Method | Endpoint | Credential | Description |
| ------ | -------------------------- | ------------- | ------------------------------------------ |
| `GET` | `/routing/effective` | Inference key | List models the saved preferences allow |
| `GET` | `/routing/preferences` | Session | Read saved routing preferences |
| `PUT` | `/routing/preferences` | Session | Replace saved routing preferences |
| `POST` | `/routing/preview` | Session | Preview a routing decision |
| `POST` | `/routing/eligible-models` | None | List models eligible for draft preferences |
## Account
| Method | Endpoint | Credential | Description |
| ------ | ---------- | -------------- | ----------------------------------- |
| `GET` | `/balance` | Management key | Read available and reserved balance |
| `GET` | `/usage` | Management key | Read recent usage metadata |
## Workspaces
| Method | Endpoint | Credential | Description |
| -------- | ----------------- | ---------- | ------------------ |
| `GET` | `/workspaces` | Session | List workspaces |
| `POST` | `/workspaces` | Session | Create a workspace |
| `PATCH` | `/workspaces/:id` | Session | Edit a workspace |
| `DELETE` | `/workspaces/:id` | Session | Delete a workspace |
## Connected Apps
> **Not enabled on the production deployment yet**
>
> AnonRouter Connect is operator-gated and is currently switched off in
> production: the discovery document and every endpoint in this section answer
> `404` on the live deployment. The interface below is documented so an
> integration can be written against it, not because it can be called today.
Connected Apps uses a focused OAuth 2.1 and OpenID Connect provider. Begin with
the discovery document at
`https://control.anonrouter.ai/connect/.well-known/openid-configuration`
instead of hard-coding the authorization, token, UserInfo, revocation, or JWKS
paths. See [Connected Apps](/connected-apps) for the complete user and developer
flow.
The dashboard uses these session-authenticated management endpoints:
| Method | Endpoint | Description |
| -------- | --------------------------------- | ----------------------------------------------------------- |
| `GET` | `/connect/grants` | List the account's active and historical app grants |
| `DELETE` | `/connect/grants/:id` | Disconnect one grant |
| `GET` | `/connect/apps` | List enabled developer apps owned by the account |
| `POST` | `/connect/apps` | Register an app and return its client secret once |
| `PATCH` | `/connect/apps/:id` | Change app metadata; redirect changes revoke current grants |
| `POST` | `/connect/apps/:id/rotate-secret` | Rotate the secret and revoke current grants |
| `DELETE` | `/connect/apps/:id` | Disable the app and revoke current grants |
## Billing
| Method | Endpoint | Credential | Description |
| ------ | ---------------------------------- | ---------- | --------------------------------------------------------------------------------------------- |
| `GET` | `/billing/catalog` | None | Packages, limits, fee, payment terms, and whether card and crypto rails are currently enabled |
| `GET` | `/billing/orders` | Session | List purchase orders |
| `GET` | `/billing/orders/:orderId` | Session | Poll one order |
| `GET` | `/billing/receipts` | Session | List receipts |
| `GET` | `/billing/receipts/:receiptNumber` | Session | Fetch one receipt |
## Response headers
Successful inference responses include:
* `x-anonrouter-selected-model`: the chosen `provider/model`.
* `x-anonrouter-routing`: `auto` or `exact`.
* `x-anonrouter-provider`: the provider that served the request, as a public
provider id, the same value as the catalog's `provider` field.
* `x-anonrouter-provider-attempts`: how many providers were tried.
* `x-anonrouter-provider-fallback`: `true` when a fallback provider served the
request.
* `x-anonrouter-privacy-class`: the tier of the route that served the request,
`anonymous`, `private`, `tee`, or `e2ee`.
* `x-ratelimit-limit-requests`, `x-ratelimit-remaining-requests`,
`x-ratelimit-reset-requests`, and the matching `-tokens` variants.
See [provider routing and failover](/provider-routing) for how the provider is
chosen and when another one is tried.
## Errors
Errors use an OpenAI-compatible envelope with a machine-readable `type` and a
`request_id`:
```json
{
"error": {
"message": "Inference ticket is invalid or expired",
"type": "invalid_ticket",
"request_id": "req_..."
}
}
```
Common cases:
| Status | `type` | Meaning |
| ------ | --------------------------- | ------------------------------------------------- |
| `401` | `ticket_required` | A ticket is required and none was sent |
| `401` | `invalid_ticket` | The ticket is invalid, expired, or already used |
| `403` | `insufficient_scope` | The credential lacks the required scope |
| `402` | `insufficient_balance` | Not enough prepaid balance |
| `404` | `model_not_found` | The model is unknown or not enabled |
| `409` | `ticket_model_mismatch` | The request model differs from the ticket |
| `409` | `ticket_reasoning_mismatch` | The reasoning selection differs from the ticket |
| `429` | `rate_limited` | Rate limit exceeded; see `retry-after` |
| `503` | `media_disabled` | Image or speech is not enabled on this deployment |
A `429` response also sets a `retry-after` header. During streaming, a mid-stream
provider failure is delivered as an inline SSE error frame rather than an HTTP
status, because the response headers have already been sent.
### Provider routing errors
| Status | `type` | Meaning |
| ------ | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| `400` | `invalid_provider_policy` | The `provider` value is malformed, for example an invalid provider id |
| `400` | `provider_policy_conflict` | Contradictory `provider` fields, such as a provider in both `only` and `ignore` |
| `400` | `no_provider_route` | No eligible provider route is available for the model |
| `403` | `no_provider_route_meets_privacy` | No route meets the required privacy floor |
| `403` | `privacy_boundary_not_met` | No route of the model meets the workspace's privacy boundary; refused before any provider sees the request |
| `400` | `no_provider_route_meets_price` | No route is within `max_price` |
| `409` | `ticket_provider_policy_mismatch` | The body's `provider` differs from the policy the ticket was issued for |
| `503` | `provider_fallback_exhausted` | Every permitted provider attempt failed before output after a fallback, or a later attempt got a `400` |
| `503` | `provider_fallback_not_supported_for_e2ee` | An E2EE provider failed before output; E2EE cannot switch providers |
See [provider routing and failover](/provider-routing#errors).
## Router, free-model, and context errors
Router IDs use the same Chat Completions endpoint. See [choose a router](/routers)
for paid router selection and errors, [free models](/free-models#troubleshooting)
for account consent and capacity errors, and [context limits](/context-limits#errors-and-retries)
for context refusals and cooldowns. A `:free` request never falls back to a paid
route. A recognized context refusal is not automatically retried on another
provider.
---
# Web search
Let a model search the web inside one API request with the managed anonrouter:web_search tool.
Managed web search lets the model look things up while it answers. Add the
`anonrouter:web_search` tool to a chat completion. The model decides when to
search and what to search for, AnonRouter runs each search and hands the model
short excerpts from the results, and you get back one ordinary chat completion.
You do not implement a search loop.
Web search is an API feature. It works with an inference key, in the private
ticket flow and in [compatibility mode](/compatibility). It is not available in
the website chat.
> **Check that web search is on**
>
> Web search can be switched off. While it is off, a request that includes the
> tool, or a ticket request with `web_search`, fails with
> `503 managed_search_unavailable` and nothing is charged.
## Request
```json
{
"model": "creator/model",
"messages": [{ "role": "user", "content": "What changed in the news this week?" }],
"tools": [
{
"type": "anonrouter:web_search",
"parameters": { "max_uses": 3, "max_results": 5, "max_total_results": 15 }
}
]
}
```
`parameters` is optional. Every field has a default and a maximum:
| Field | Meaning | Default | Maximum |
| ------------------- | ------------------------------------------- | ------- | ------- |
| `max_uses` | Searches in this request | 3 | 3 |
| `max_results` | Results returned by each search | 5 | 8 |
| `max_total_results` | Results across all searches in this request | 15 | 15 |
Send at most one web search tool. Unknown fields or values outside these ranges
fail with `400 invalid_server_tools`.
Other limits on one request:
| Limit | Value |
| ------------------- | ------------------------ |
| Model calls | 5 |
| Search query length | 400 characters, 50 words |
| Whole request | 15 minutes |
Pick a model whose route supports tool calling, and name it explicitly. `/auto`
and the named routers are not supported with web search yet.
### Ticket flow
In the private ticket flow, ask for the search allowance when you issue the
ticket, then send the tool in the completion:
```json
{
"model": "creator/model",
"max_completion_tokens": 1024,
"web_search": { "max_uses": 3 }
}
```
`web_search.max_uses` is an integer from 1 to 3. The completion's `max_uses` may
not be higher than the ticket's, or the request fails with
`409 ticket_web_search_mismatch`. In compatibility mode AnonRouter issues the
ticket for you from the tool's `max_uses`.
### Your own tools
You can send your own function tools next to the web search tool. When the
model calls one of yours, the response ends with `finish_reason: "tool_calls"`
as usual. Run the tool and send its result in your next request. The function
name `anonrouter_web_search` is reserved.
AnonRouter never opens result pages. If you want the model to read a page, give
it your own tool that fetches one.
## Response
The response is a standard chat completion with two additions: `usage.cost`
covers every model call and every charged search, and a `web_search` object
summarizes the searches.
```json
{
"object": "chat.completion",
"model": "creator/model",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "..." },
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 3120,
"completion_tokens": 410,
"total_tokens": 3530,
"cost": 0.0151
},
"web_search": {
"searches": 2,
"model_calls": 3,
"results": [
{ "title": "...", "url": "https://example.com/...", "description": "..." }
],
"cost": 0.012,
"model_cost": 0.0031
}
}
```
| Field | Meaning |
| ------------------------ | -------------------------------------------------------------------------------- |
| `web_search.searches` | Searches run for this request |
| `web_search.model_calls` | Model calls made for this request |
| `web_search.results` | The result titles, URLs and descriptions the model was given, without duplicates |
| `web_search.cost` | What the searches cost, in USD |
| `web_search.model_cost` | What the model calls cost, in USD |
`usage` token counts are totals across all model calls. Values above are
illustrative.
### Streaming
With `stream: true` you receive the answer text as normal chunks. While a search
runs, AnonRouter also sends progress frames with the SSE event name
`anonrouter.web_search`:
```text
event: anonrouter.web_search
data: {"id":"chatcmpl-...","status":"started","index":1}
```
`status` is `started`, `succeeded` or `failed`, and `index` counts searches from
1. Progress frames never contain the query. They have no `choices`, so skip them
if your client only reads chunks.
The last chunk carries `finish_reason` and the `web_search` object. With
`stream_options.include_usage`, a usage chunk follows. `data: [DONE]` is sent
only after every model call and search has been billed. If something fails
after streaming has started, you get one error frame and the stream closes
without `[DONE]`.
## Pricing
Each successful search costs **$0.006**, on every account, including accounts
with a [subscription](/subscriptions). A search is charged only when the search
provider returned a successful response. A search that failed, timed out without an
answer, or was never sent is not charged.
The model calls in the request are billed like any other chat request, at the
model's listed price. A request can make up to five model calls, so its model
cost can be higher than a single completion.
A per-key credit limit and a daily usage budget count searches too. If a search cannot be
paid for, the model is told the search is unavailable and continues without it;
model calls already made are still billed.
For rate limits, each model call counts as one request.
## Privacy
A search sends the model's query to a third-party web search provider that
AnonRouter classifies as Private. Searches run on the confidential content
plane: the query goes only to the search provider, and the result excerpts go
only to the model provider, as part of the conversation. AnonRouter's account
side records that a search was authorized, whether it succeeded, and its price,
never the query or the results.
Because the query leaves for a Private provider, web search is refused for TEE
and E2EE routes and for encrypted requests. Search results are untrusted text
from the web. The model is told to treat them as information, not instructions.
## Usage records
`GET /v1/usage` with a management key includes a `web_search` field: up to 100
recent searches and the totals for the last 30 days.
```json
{
"web_search": {
"totals_30d": { "charged_searches": 42, "cost_usd": 0.252 },
"data": [
{
"id": "...",
"request_id": "req_...",
"status": "charged",
"outcome": "succeeded",
"unit_price_usd": 0.006,
"cost_usd": 0.006,
"pricing_version": "web-search-2026-10-26-v1",
"created_at": "2026-10-28T12:00:00.000Z"
}
]
}
}
```
`status` is `charged`, `not_charged` or `pending`. `request_id` is the API
request the search belonged to. Records never contain the query or the results.
## Not supported
Web search is refused before anything is charged for:
* `/auto`, the named routers, free models and the Free Models Router
* TEE and E2EE routes, and encrypted requests
* trial credit
* `response_format` (structured output)
* a `models` fallback list
* embeddings, images and speech
* website sessions and connected apps
## Errors
| Status | `type` | Meaning |
| ------ | ---------------------------------------------- | -------------------------------------------------------------------------------------- |
| `400` | `invalid_server_tools` | The web search tool is malformed, repeated, or outside its limits |
| `400` | `reserved_tool_name` | One of your functions uses the reserved name `anonrouter_web_search` |
| `400` | `managed_search_requires_explicit_model` | `/auto` or a named router was requested |
| `400` | `managed_search_free_unsupported` | A free model or the Free Models Router was requested |
| `400` | `managed_search_privacy_unsupported` | The route is TEE or E2EE |
| `400` | `managed_search_e2ee_unsupported` | The request is encrypted |
| `400` | `managed_search_trial_unsupported` | The request would be paid with trial credit |
| `400` | `managed_search_structured_output_unsupported` | The request sets `response_format` |
| `400` | `managed_search_models_unsupported` | The request sends a `models` fallback list |
| `400` | `managed_search_chat_only` | The ticket is not for chat completions |
| `403` | `managed_search_api_key_required` | The request does not use an inference key |
| `409` | `ticket_web_search_mismatch` | The ticket was issued without `web_search`, or with a lower `max_uses` |
| `502` | `mixed_server_client_tools_unsupported` | The model asked for a search and one of your tools in the same turn; retry the request |
| `503` | `managed_search_unavailable` | Web search is switched off |
| `504` | `managed_search_timeout` | The request ran longer than 15 minutes |
---
# Batch API Quickstart
Submit and retrieve asynchronous batches of inference requests
> **Design example**
> **Visual prototype, not a supported AnonRouter endpoint**
>
> This page is here to demonstrate the documentation experience, including
> tabbed examples, API payloads, tables, callouts, and long-form navigation.
> The OpenRouter Batch API shown below is not currently part of AnonRouter's
> supported API surface.
The Batch API lets you submit many inference requests together and retrieve the
results asynchronously. It is useful for work that does not need an immediate
response, and it uses a 24-hour completion window so you can process requests
without managing each call yourself.
The API supports several request shapes, including chat completions, Responses,
Anthropic Messages, and embeddings. Requests are submitted as an inline JSON
`requests` array.
> **Asynchronous results**
>
> A successful submission returns `202 Accepted` with `status: "validating"`.
> This means the request was persisted and queued for validation, not that
> every request has completed.
***
## Submit a batch
Submit a batch with:
```text title="Endpoint"
POST https://openrouter.ai/api/beta/batches
```
The request body has three required top-level fields:
| Field | Description |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `endpoint` | The API shape used by every request in the batch. Choose `/v1/chat/completions`, `/v1/responses`, `/v1/messages`, or `/v1/embeddings`. |
| `model` | An OpenRouter model slug, such as `openai/gpt-4o`. This batch-level model is applied to every request. |
| `requests` | A non-empty array of `{ custom_id, body }` items. `custom_id` must be unique within the batch, and `body` follows the selected endpoint. |
> **Field order matters**
>
> Serialize `endpoint` and `model` before `requests` in the JSON body. The API
> stream-parses the request so it can accept very large arrays, and returns a
> `400` if `requests` appears first. Every example below uses the required
> order.
The batch-level `model` applies to every request. An individual request body can
omit `model` to inherit it. If a request body specifies a model, it must match
the batch-level model.
```python
import json
import requests
response = requests.post(
url="https://openrouter.ai/api/beta/batches",
headers={
"Authorization": "Bearer ",
"Content-Type": "application/json",
},
data=json.dumps({
"endpoint": "/v1/chat/completions",
"model": "openai/gpt-4o",
"requests": [
{
"custom_id": "req-0001",
"body": {
"messages": [
{
"role": "user",
"content": "Summarize OpenRouter in one sentence."
}
]
}
}
]
})
)
print(response.json())
```
```typescript
const response = await fetch("https://openrouter.ai/api/beta/batches", {
method: "POST",
headers: {
Authorization: "Bearer ",
"Content-Type": "application/json",
},
body: JSON.stringify({
endpoint: "/v1/chat/completions",
model: "openai/gpt-4o",
requests: [
{
custom_id: "req-0001",
body: {
messages: [
{
role: "user",
content: "Summarize OpenRouter in one sentence.",
},
],
},
},
],
}),
});
console.log(await response.json());
```
```shell
curl https://openrouter.ai/api/beta/batches \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-d '{
"endpoint": "/v1/chat/completions",
"model": "openai/gpt-4o",
"requests": [
{
"custom_id": "req-0001",
"body": {
"messages": [
{
"role": "user",
"content": "Summarize OpenRouter in one sentence."
}
]
}
}
]
}'
```
The response is a batch object with an ID you can use to check progress:
```json title="202 Accepted"
{
"id": "batch_123",
"object": "batch",
"endpoint": "/v1/chat/completions",
"model": "openai/gpt-4o",
"completion_window": "24h",
"status": "validating",
"created_at": 1782097200,
"finalized_at": null,
"request_counts": {
"total": 1,
"completed": 0,
"failed": 0
},
"usage": null,
"results": null,
"error": null
}
```
The only supported completion window is `24h`.
***
## Poll for results
Use the batch ID to retrieve the current status:
```text title="Endpoint"
GET https://openrouter.ai/api/beta/batches/:id
```
For example:
```shell title="Shell"
curl https://openrouter.ai/api/beta/batches/batch_123 \
-H "Authorization: Bearer $OPENROUTER_API_KEY"
```
The status normally progresses through:
```text title="Status progression"
validating → in_progress → finalizing → completed
```
Other possible statuses are `failed`, `expired`, `cancelling`, and `cancelled`.
The terminal statuses are `completed`, `failed`, `expired`, and `cancelled`.
Poll until the batch reaches a terminal status.
`request_counts` contains the total number of requests and the number that
completed or failed:
```json title="request_counts"
{
"total": 100,
"completed": 98,
"failed": 2
}
```
While a batch is in progress, or has failed, expired, or been cancelled,
`results` is `null`. When it completes, results are returned inline in the same
response. Each result maps to an input using `custom_id`, and exactly one of
`response` or `error` is populated.
```json title="Result item"
{
"id": "batch_req_123",
"custom_id": "req-0001",
"response": {
"status_code": 200,
"request_id": "request_123",
"body": {
"id": "gen-batch-1782097200-a1b2c3d4e5f6a7b8c9d0",
"object": "chat.completion",
"created": 1782097200,
"model": "openai/gpt-4o",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "OpenRouter provides one API for many AI models."
},
"finish_reason": "stop"
}
]
}
},
"error": null
}
```
A completed batch response looks like this:
```json title="Completed batch"
{
"id": "batch_123",
"object": "batch",
"endpoint": "/v1/chat/completions",
"model": "openai/gpt-4o",
"completion_window": "24h",
"status": "completed",
"created_at": 1782097200,
"finalized_at": 1782100800,
"request_counts": {
"total": 1,
"completed": 1,
"failed": 0
},
"usage": {
"prompt_tokens": 20,
"completion_tokens": 40,
"total_tokens": 60,
"cost": 0.00045,
"is_byok": false
},
"results": [
{
"id": "batch_req_123",
"custom_id": "req-0001",
"response": {
"status_code": 200,
"request_id": "request_123",
"body": {
"id": "gen-batch-1782097200-a1b2c3d4e5f6a7b8c9d0",
"object": "chat.completion",
"model": "openai/gpt-4o",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "OpenRouter provides one API for many AI models."
},
"finish_reason": "stop"
}
]
}
},
"error": null
}
],
"error": null
}
```
***
## Reporting issues
Each completed result's `response.body.id` is that request's generation ID. To
flag a bad generation, copy that ID and submit it through the provider's
generation feedback flow.
> **Keep the generation ID**
>
> Generation-level feedback is available for the chat completions, Responses,
> and Messages request shapes.
***
## Use different API shapes
Set the top-level `endpoint` to choose the request shape used by every `body` in
the batch:
* Chat completions: `/v1/chat/completions`
* Responses: `/v1/responses`
* Anthropic Messages: `/v1/messages`
For example, an Anthropic Messages batch uses `/v1/messages` and puts the
Messages-shaped request in each item's `body`:
```json title="Anthropic Messages batch"
{
"endpoint": "/v1/messages",
"model": "anthropic/claude-3.5-sonnet",
"requests": [
{
"custom_id": "req-1",
"body": {
"max_tokens": 32,
"messages": [
{
"role": "user",
"content": "Say hello."
}
]
}
}
]
}
```
All requests in one batch use the same top-level `endpoint`. To mix API shapes,
submit separate batches.
***
## Embeddings
Embeddings are rolling out on providers that support them.
Set the top-level `endpoint` to `/v1/embeddings` and put the embeddings request
in each item's `body`. Each body takes an `input`, which may be a string, an
array of strings, a token array, or an array of token arrays.
```json title="Embeddings batch"
{
"endpoint": "/v1/embeddings",
"model": "openai/text-embedding-3-small",
"requests": [
{
"custom_id": "emb-0001",
"body": {
"input": [
"The quick brown fox jumped over the lazy dog.",
"Pack my box with five dozen liquor jugs."
]
}
},
{
"custom_id": "emb-0002",
"body": {
"input": "The quick brown fox jumped over the lazy dog."
}
}
]
}
```
Poll for results the same way as any other batch. Each result carries the
standard embeddings response in its `body`, with one result per `custom_id`.
```json title="Embeddings results"
[
{
"id": "batch_req_emb_1",
"custom_id": "emb-0001",
"response": {
"status_code": 200,
"request_id": "request_456",
"body": {
"object": "list",
"data": [
{
"object": "embedding",
"embedding": [0.0023064255, -0.009327292, 0.015797347],
"index": 0
},
{
"object": "embedding",
"embedding": [-0.012282, 0.0034567, -0.0089123],
"index": 1
}
],
"model": "openai/text-embedding-3-small",
"usage": {
"prompt_tokens": 18,
"total_tokens": 18
}
}
},
"error": null
}
]
```
> **Retention**
>
> Batch inputs and results are commonly retained as JSONL artifacts for a
> limited window. Download any results you need before the provider's retention
> window elapses.
---
# SDKs
The AnonRouter SDKs for verifying confidential routes, running E2EE inference, and generating images and speech across the two-origin privacy split. JavaScript packages are on npm; Python packages and checksummed release artifacts are on GitHub.
AnonRouter is OpenAI-compatible, so you can reach most of it with any HTTP client
or OpenAI SDK. These packages are for the parts a stock client cannot do:
* **check the provider evidence yourself**, rather than taking our word for a
confidentiality claim;
* **encrypt requests in your own process**, so AnonRouter only ever holds
ciphertext; and
* **run the two-origin ticket exchange** for images and speech, which needs two
hosts and two different credentials in one logical call.
> **SDK 0.1.3: npm for JavaScript, GitHub for Python**
>
> Install `@anonrouter/confidential@0.1.3` or `@anonrouter/client@0.1.3` from npm.
> Python is distributed as a wheel and sdist through the
> [`v0.1.3` release](https://github.com/anonrouter/anonrouter-sdk/releases/tag/v0.1.3),
> with checksums in `SHA256SUMS`; it is not published on PyPI. Source is public
> under Apache-2.0 in
> [`anonrouter/anonrouter-sdk`](https://github.com/anonrouter/anonrouter-sdk).
## Packages
| Package | Language | What it is |
| ----------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`@anonrouter/confidential`](/sdk/confidential) | JavaScript / TypeScript | Verify both hops, run E2EE inference, and generate [images and speech](/sdk/media). Installs the [`anonrouter-verify`](/sdk/verify-cli) command. |
| [`anonrouter-confidential`](/sdk/confidential) | Python | The twin of the above: same verification, same E2EE, same media surface, same shared test vectors. |
| `@anonrouter/client` | JavaScript / TypeScript | A thin, dependency-free client for the public chat and models API over the ticketed flow. |
## Install
```bash
npm install @anonrouter/confidential@0.1.3
# Or the lightweight ticketed chat client:
npm install @anonrouter/client@0.1.3
```
For Python, download `anonrouter_confidential-0.1.3-py3-none-any.whl` and
`SHA256SUMS` from the release, verify the wheel's SHA-256, then install it:
```bash
python -m pip install "./anonrouter_confidential-0.1.3-py3-none-any.whl[connection-binding]"
```
The extra installs the reviewed HTTPX/HTTPCore versions for retained-socket binding.
The native DCAP verifier is a separate installation; see
[verification setup](/sdk/confidential#reaching-hardware_verified).
Upgrade older confidential clients to 0.1.3 to verify the Intel TDX content
nodes behind `api.anonrouter.ai`. Earlier versions pin the previous deployment
and fail closed against it. Production verification requires native DCAP and
a retained TLS connection; see [configuration](/sdk/confidential#configuration-two-origins).
## Why an installed SDK is a stronger trust boundary
For browser-encrypted chat, AnonRouter serves the JavaScript that does the
encryption. That is convenient, but it means trusting code we deliver at request
time. A package you install yourself, pin to a version, and audit is a stronger
boundary: AnonRouter cannot swap it per request. If you are building a desktop
app, a backend service, or anything where verification must not depend on us, use
the SDK rather than the browser flow.
## What these SDKs claim, and what they do not
* **They re-verify raw evidence themselves.** The gateway's own verdict is
returned for comparison and is never trusted in its place.
* **They report an honest ceiling, per hop.** AnonRouter's own confidential plane
(hop 1) can reach `hardware_verified`, but only with a real DCAP engine that
actually chained the quote to Intel's roots. The provider hop (hop 2) caps at
`policy_matched` or `cryptographically_checked`: several provider routes run GPU
enclaves whose NVIDIA attestation chain is not available to verify, and chaining
only the CPU quote would claim more than was checked.
* **They keep the TEE / E2EE distinction explicit.** All content goes to the
confidential origin, where the relay runs in an attested TDX CVM and terminates
TLS in-enclave, so plaintext never reaches ordinary AnonRouter infrastructure.
What differs is the trust set: a **TEE** route has our attested relay handle
your plaintext inside the enclave, so you trust the reviewed build (and a
changed build fails your hop-1 check); an **E2EE** route encrypts to the
provider's key, so the relay holds ciphertext regardless of what it runs.
`verifyRoute()` reports which you got as `contentVisibleToAnonRouter`.
* **They fail closed.** Missing collateral, a debug-mode enclave, an unpinned
measurement, a nonce mismatch, or a route that attested something other than
what you asked for all produce a failing verdict, never a silent pass.
## New to the confidential model?
---
# Verification and E2EE
Verify both hops of a confidential route with one verdict you can gate on, and run end-to-end-encrypted inference, with @anonrouter/confidential (JS/TS) and anonrouter-confidential (Python).
`@anonrouter/confidential` (JavaScript / TypeScript) and `anonrouter-confidential`
(Python) are twin packages that let your own app do three things without trusting
AnonRouter: **verify** a route's evidence, **encrypt** inference requests end to
end, and [generate images and speech](/sdk/media) across the two-origin split.
The verifier is pure over its inputs, fails closed, and both languages pass the
same shared known-answer test vectors, so they agree bit-for-bit.
> **What TEE and E2EE each buy you**
>
> Your plaintext does not reach ordinary AnonRouter infrastructure. All content
> goes to `api.anonrouter.ai`, whose relay runs inside an attested Intel
> TDX CVM and terminates TLS **in-enclave**: `transport_terminates_in_tee` and
> `tls_certificate_bound_to_quote` are required checks in the shipped policy, so
> the attested TD provably holds the key for your connection.
>
> What still differs between the modalities is the **trust set**. On a **TEE**
> route the relay handles your plaintext inside that enclave, so you are trusting
> our reviewed build. A build changed to exfiltrate it would change the
> measurements and hop 1 would stop verifying, making cheating *detectable* rather
> than impossible. On an **E2EE** route (NEAR, Venice, Chutes) the request is
> encrypted to the *provider's* attested key, so the relay holds ciphertext
> whatever code it runs and our build is not in your trust set at all. On some
> E2EE routes the enclave holding that key is a verified provider gateway rather
> than the model host; the catalog's per-route `privacyNotes` say which.
>
> Every verdict reports which one you got as `contentVisibleToAnonRouter`, rather
> than leaving you to infer it from the provider name.
## Install
JavaScript is published on npm. Python is available from the checksummed
[`v0.1.3` release](https://github.com/anonrouter/anonrouter-sdk/releases/tag/v0.1.3),
not PyPI. Download its wheel and `SHA256SUMS`, verify the wheel's SHA-256, then
install the local file.
```bash
npm install @anonrouter/confidential@0.1.3
```
```bash
python -m pip install "./anonrouter_confidential-0.1.3-py3-none-any.whl[connection-binding]"
# Add Chutes ML-KEM support when needed:
python -m pip install "./anonrouter_confidential-0.1.3-py3-none-any.whl[connection-binding,mlkem]"
```
Node 22+ or Python 3.10+. You need an API key with the `inference` scope.
## Configuration: two origins
AnonRouter serves two browser-visible origins with different jobs, and the SDK
keeps them apart for you.
| Origin | Role | What it receives |
| ------------------------------- | ------------ | ------------------------------------------------------------------------------------ |
| `https://control.anonrouter.ai` | control | your API key, content-free ticket and catalog requests |
| `https://api.anonrouter.ai` | confidential | attestation evidence and request content, authenticated by a single-use ticket alone |
Both are the SDK's defaults. To verify the production content plane, use 0.1.3 and
configure required connection binding as below. This keeps the TLS certificate,
fresh attestation, and inference on the same retained connection. Install the
[native DCAP verifier](#reaching-hardware_verified) first. A separate successful
verification is not a substitute for binding the connection used for inference.
```ts
import { createClient, verifyContentNode } from "@anonrouter/confidential";
import { createAnonRouterDcapVerifier } from "@anonrouter/confidential/dcap";
// Free, credential-free diagnostic. Each inference operation verifies again.
const check = await verifyContentNode();
if (check.verdict.status !== "ok") throw new Error(check.verdict.reason);
const client = createClient({
apiKey: process.env.ANONROUTER_API_KEY!,
gatewayConnectionBinding: {
mode: "required",
verification: { chainVerifier: createAnonRouterDcapVerifier() },
},
});
```
```python
import os
from anonrouter_confidential import create_client, verify_content_node
from anonrouter_confidential.gateway.dcap import create_anonrouter_dcap_verifier
check = verify_content_node()
if check["verdict"].status != "ok":
raise RuntimeError(check["verdict"].reason)
client = create_client(
api_key=os.environ["ANONROUTER_API_KEY"],
gateway_connection_binding={
"mode": "required",
"verification": {"chain_verifier": create_anonrouter_dcap_verifier()},
},
)
```
> **Why the origin matters to verification**
>
> A gateway quote binds the exact origin the client connected to. Verifying one
> origin while sending content to another would prove nothing about the route that
> carried your prompt, so the SDK deliberately offers **no way** to do it: evidence
> and content always use the same confidential origin. The control origin only ever
> carries the API key and content-free metadata.
## The two hops
A request travels through two parties, and verifying one tells you nothing about
the other.
| | Question it answers | Endpoint |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| **Hop 1**: AnonRouter's own routing plane | Is the data plane I am connected to the exact reviewed build, running inside an Intel TDX confidential VM, bound to my nonce and my origin? | `GET /v1/gateway/attestation` |
| **Hop 2**: the downstream provider route | Did the model provider terminate my request inside a verified enclave running measurements I pinned? | `/v1/tee/attestation` |
A verified hop 2 says nothing about who routed the request. A verified hop 1 says
nothing about where inference actually ran. Only asking both establishes the whole
path.
## `verifyRoute()`: the stable contract
`verifyRoute` establishes both hops, **cross-binds** them to the route you asked
for, and reports ordered states rather than a boolean. This is the call to build
on.
```ts
import { atLeast } from "@anonrouter/confidential";
// Reuse the connection-bound client configured above.
const verdict = await client.verifyRoute({
model: "venice-uncensored",
provider: "venice",
gateway: true, // required binding keeps hop 1 in scope
});
verdict.overallState; // the weakest hop you ASKED about
verdict.gateway.requested; // whether hop 1 was in scope at all
verdict.gateway.failedChecks; // the exact required checks that failed
verdict.bindingMismatches; // route asked for vs route attested
verdict.contentVisibleToAnonRouter; // true on a tee route
if (!atLeast(verdict.overallState, "cryptographically_checked")) {
throw new Error(`route not established: ${verdict.reason}`);
}
```
```python
from anonrouter_confidential import at_least
# Reuse the connection-bound client configured above.
verdict = client.verify_route(
model="venice-uncensored",
provider="venice",
gateway=True, # required binding keeps hop 1 in scope
)
verdict.overall_state # the weakest hop you ASKED about
verdict.gateway.requested # whether hop 1 was in scope at all
verdict.gateway.failed_checks # the exact required checks that failed
verdict.binding_mismatches # route asked for vs route attested
verdict.content_visible_to_anonrouter # True on a tee route
if not at_least(verdict.overall_state, "cryptographically_checked"):
raise SystemExit(verdict.reason)
```
### Cross-binding is what this adds
Each hop's verifier only ever sees its own evidence, so neither can notice that
the gateway attested itself perfectly while serving a **different provider, model,
or privacy class** than you asked for. Any disagreement lands in
`bindingMismatches` and forces `untrusted`, however strong the individual hops
were. Two honestly-attested parties on the wrong route is still the wrong route.
### The five states
Strictly ordered. Gate with `atLeast()` / `at_least()` rather than comparing
strings, so a threshold keeps meaning the same thing if a state is later inserted
into the scale.
| State | What it establishes |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `hardware_verified` | Everything below, **plus** the quote's signature chained to the vendor's roots with an accepted TCB status. Required on the production gateway hop, with native DCAP and retained-socket binding. |
| `cryptographically_checked` | The evidence is internally consistent and bound to your fresh nonce, and the measurements match local pins. Nothing proved it came from real silicon. |
| `policy_matched` | The evidence's **claimed** identity matched local pins, established through a vendor SDK rather than by our own parse. |
| `untrusted` | We looked and it failed. |
| `unavailable` | We could not look. The deployment does not expose the endpoint. |
> **`untrusted` and `unavailable` are both failures, and they are different**
>
> "We looked and it failed" and "we could not look" call for different responses.
> Collapsing them into one boolean is how a missing endpoint gets mistaken for a
> passing check.
## Reaching `hardware_verified`
The npm and Python packages use a strict adapter to a separately installed
DCAP engine. The release provides a checksummed, reproducible `linux/amd64`
verifier and its source; other hosts must build the reviewed source. The engine
uses Intel-signed collateral supplied by the SDK and performs no network access
itself. Install `anonrouter-dcap-verifier`, put it on `PATH` or name it in
`ANONROUTER_DCAP_VERIFIER_BIN`, and hop 1 can reach `hardware_verified`.
```bash
# After installing the reviewed engine:
export ANONROUTER_DCAP_VERIFIER_BIN="/absolute/path/to/anonrouter-dcap-verifier"
anonrouter-verify doctor
anonrouter-verify gateway --origin https://api.anonrouter.ai --dcap --require hardware_verified
```
Use the connection-binding configuration above for SDK calls. Against the production content plane,
`verifyGateway()` and `verifyRoute({ gateway: true })` without required connection
binding refuse with `unsupported_request` before sending a request. Native DCAP
is mandatory; there is no lower-assurance fallback for this platform. Browsers
cannot read the TLS certificate or run the native verifier, so verify this hop
from Node or a Python backend you control.
The adapter fails closed on every path: a missing engine, a digest that does not
match the expected one, a timeout, a crash, non-JSON output, collateral it could
not acquire, or a verdict whose measured report disagrees with the quote the SDK
parsed. A missing engine cannot establish production gateway verification. It never silently
downgrades to a weaker state.
> **The pins do not come from the server being checked**
>
> A server that could hand you the list of builds you accept could always name
> itself. For the production content plane, the package carries the owner root public keys and verifies the
> independently signed release-v3 policy feed, with anti-rollback protection.
> Approved node images need no SDK update. Other supported platforms use
> reviewed packaged or explicitly supplied policies; an unknown origin fails
> rather than trusting its own claims.
## End-to-end-encrypted chat
For an E2EE provider the SDK encrypts your prompt **in your process**, relays only
ciphertext, and decrypts the reply locally. It verifies the enclave first and binds
the encryption key to that attestation, so it will not encrypt to an enclave it
could not verify.
```ts
const reply = await client.chat({
model: "venice-uncensored",
provider: "venice",
messages: [{ role: "user", content: "Draft a private message." }],
maxOutputTokens: 512,
// Re-establish hop 1 with a NEW nonce before a ticket is spent: a verdict from
// a minute ago is a fact about a minute ago.
requireGateway: true,
});
console.log(reply.content);
console.log(reply.usage);
```
```python
out = client.chat(
model="venice-uncensored",
provider="venice",
messages=[{"role": "user", "content": "Draft a private message."}],
max_output_tokens=512,
require_gateway=True,
)
print(out["content"])
```
`requireGateway` / `require_gateway` runs the check **before** the first
authenticated call, so a failure means no ticket was spent and no plaintext went
near the wire.
Conversations are **multi-turn** and **text-only**. Because the enclave is
stateless you re-send the conversation each turn and the SDK re-encrypts it fresh,
exactly like any stateless chat API. Tool calls, images, and attachments are not
part of the encrypted surface and fail closed.
## Providers and ceilings
| Provider | Modality | Hop 2 ceiling |
| --------- | ----------------------------------------------------- | --------------------------- |
| `venice` | E2EE (secp256k1 ECDH, AES-256-GCM) | `cryptographically_checked` |
| `chutes` | E2EE (ML-KEM-768, ChaCha20-Poly1305) | `cryptographically_checked` |
| `tinfoil` | TEE (our attested relay handles plaintext in-enclave) | `policy_matched` |
| `near-ai` | E2EE (Ed25519 / X25519, XChaCha20-Poly1305) | `cryptographically_checked` |
Chutes is the only **post-quantum** route. Tinfoil is TEE-only and reaches
`policy_matched` through its provider verification adapter. Missing or invalid provider
evidence fails closed.
Verifier support does not establish that a route is callable. Check the
authenticated catalog for currently enabled models and each route's privacy
class before selecting a provider.
> **A Venice E2EE route may decrypt at a gateway, not at the model host**
>
> `contentVisibleToAnonRouter` answers the question the SDK can answer: whether
> *our* build sees plaintext. It does not describe the provider's internal
> topology. On some Venice routes the enclave your client encrypts to is a
> verified gateway that decrypts and forwards onward inside Venice. The route's
> `privacyNotes` in the catalog state this per route.
Hop 2 does not reach `hardware_verified` on any provider: several of these routes
run GPU enclaves whose NVIDIA attestation chain is not available to verify, and
chaining only the CPU quote would claim more than was checked.
> **Chutes E2EE bills a flat, full-ceiling reservation**
>
> Chutes' protocol encrypts the whole request body, so the gateway cannot meter it
> token by token. It therefore reserves and bills the model's **full context window
> plus full output ceiling, flat**, regardless of how short your prompt or
> `maxOutputTokens` is. On a large-context model that is significant. NEAR and
> Venice are metered from ciphertext length and cost the usual per-token amount.
> For a cheap encrypted round-trip prefer a Venice route, and remember verification itself is always free.
## The security contract
* **Independent re-verification.** The SDK parses the raw TDX quote and provider
evidence itself and checks it against reviewed provider policies and the
independently signed gateway policy. It never
turns a provider's `verified: true`, a model suffix, or the gateway's verdict
into a proof.
* **Fresh nonce, key binding.** Every verification uses a fresh nonce and confirms
the enclave bound its key to that nonce, so evidence cannot be replayed.
* **Credential isolation.** Your API key authenticates only content-free control
requests. It is never sent to the origin that carries content.
* **No escape hatch.** There is no environment variable that weakens a verdict.
The suites set a dozen plausible spellings of one (`ANONROUTER_INSECURE`,
`ANONROUTER_SKIP_VERIFY`, `NODE_ENV=test`, `CI=true`, and so on) and require the
verdict for the same evidence to come out **identical**, not merely also-failing.
## Errors
Failures raise a typed `ConfidentialError` with a stable `code` (such as
`attestation_untrusted`, `nonce_mismatch`, or `inference_ticket_failed`) and a
content-free message. Error messages never carry prompt or key material by
construction. [Media errors](/sdk/media#errors) subclass the same type.
## Advanced use
Both packages export the building blocks, for callers who want to verify or
decrypt without the high-level client:
* **Pure verifiers** per provider (`NearTeeVerifier`, `VeniceTeeVerifier`,
`ChutesTeeVerifier`, `TinfoilTeeVerifier`) plus `verifyRawEvidence`.
* **TDX quote parsing**: `parseTdxQuote`, `matchMeasurementAllowlist`.
* **Reviewed pins**: `pinnedMeasurementPolicyFor`, `pinnedGatewayPolicyFor`.
* **Low-level provider crypto**: `providerCrypto["near-ai" | "venice" | "chutes"]`
(JS) or `anonrouter_confidential.crypto.{near,venice,chutes}` (Python),
exercised by the shared known-answer vectors.
## Release artifacts
[`v0.1.3`](https://github.com/anonrouter/anonrouter-sdk/releases/tag/v0.1.3)
publishes the JavaScript tarballs, Python wheel and sdist, verifier, source and
`SHA256SUMS`. Both JavaScript packages are also on npm; Python remains
GitHub-only. The SDK is Apache-2.0 and the DCAP engine source in
`native/dcap-verifier` is AGPL-3.0-only. The supplied reproducible verifier target
is `linux/amd64`.
Version 0.1.3 adds release-v3 policy verification in both languages. It
requires native DCAP, an accepted TCB, a fresh nonce and the TLS key observed on
the retained connection. The release documents advisory gaps for firmware CCEL
replay and optional cloud instance identity; `hardware_verified` does not imply
those advisory checks succeeded. Inspect individual checks as well as the state.
Older confidential SDK versions fail closed against the production gateway; upgrade
rather than copying measurements from the server into a policy.
---
# Images and speech
Generate images and synthesize speech across AnonRouter's two-origin privacy split with client.images.generate and client.audio.speech.create.
`client.images.generate(...)` and `client.audio.speech.create(...)` generate an
image or synthesize speech across AnonRouter's two-origin split, in both
JavaScript and Python. Parameter names follow the OpenAI API, so a working call
ports over mostly unchanged.
One SDK call is **two HTTP requests to two different hosts**, and that is the
entire point.
## What each host learns
```
YOU ──API key, model, size/voice, character COUNT──▶ control.anonrouter.ai
(control origin)
◀──────── single-use ticket, 30s ─────────────────┘
YOU ──ticket, prompt or text─────────────────────▶ api.anonrouter.ai
(confidential origin)
◀──────── image bytes / audio bytes ─────────────┘
```
| | control origin | confidential origin |
| ------------------------- | ------------------------------------------------------- | --------------------------------------- |
| Learns your account | **yes**: the API key authenticates here | no |
| Learns the prompt or text | **no** | **yes** |
| Learns the priced shape | model, image size/format, speech character count, voice | the same, re-derived from the body |
| Credential it accepts | `Authorization: Bearer ` | the single-use ticket, and nothing else |
Neither host holds both halves. The control plane can say *"this account paid for
one 1024×1024 image"* and cannot say what was in it. The confidential plane holds
the prompt and cannot say whose it was.
> **This is not end-to-end encryption**
>
> These methods live on the same client as `chat()`, so it is worth being exact.
> `chat()` on an E2EE provider encrypts in your process; AnonRouter receives
> ciphertext. Ticketed media does **not**: the prompt reaches the confidential
> origin as **plaintext**. What protects it is the origin split plus the Intel TDX
> enclave that origin runs in, which you can
> [verify yourself](/sdk/confidential#reaching-hardware_verified) before you send
> anything.
>
> That is a real property and a weaker one than E2EE chat. The difference is what
> it would take for AnonRouter to read a media prompt: shipping a different build
> into the CVM. Not undetectable (the measurements would change and
> `verifyGateway()` would stop passing), but possible, whereas on an E2EE route the
> relay holds ciphertext no matter what code it runs. If you need "cannot read it
> **even if they shipped code to try**" rather than "would be caught", media does
> not meet that today.
## Quickstart
The production origins are the defaults. To verify the content nodes on the
connection that carries media, install SDK 0.1.3 and the native DCAP verifier,
then configure required connection binding. Python also needs the
`connection-binding` extra; see [installation and verification](/sdk/confidential).
```ts
import { writeFile } from "node:fs/promises";
import { createClient } from "@anonrouter/confidential";
import { createAnonRouterDcapVerifier } from "@anonrouter/confidential/dcap";
const client = createClient({
apiKey: process.env.ANONROUTER_API_KEY!,
gatewayConnectionBinding: {
mode: "required",
verification: { chainVerifier: createAnonRouterDcapVerifier() },
},
});
const image = await client.images.generate({
model: "alibaba/z-image-turbo",
prompt: "a lighthouse in a storm, painted in oils",
size: "1024x1024",
});
await writeFile("out.png", image.data[0].bytes);
console.log(image.selected_model, image.data[0].mime_type);
const speech = await client.audio.speech.create({
model: "venice/kokoro-text-to-speech",
input: "The quick brown fox jumps over the lazy dog.",
voice: "af_sky",
});
await writeFile("out.mp3", speech.audio);
```
```python
import os
from anonrouter_confidential import create_client
from anonrouter_confidential.gateway.dcap import create_anonrouter_dcap_verifier
client = create_client(
api_key=os.environ["ANONROUTER_API_KEY"],
gateway_connection_binding={
"mode": "required",
"verification": {"chain_verifier": create_anonrouter_dcap_verifier()},
},
)
image = client.images.generate(
model="alibaba/z-image-turbo",
prompt="a lighthouse in a storm, painted in oils",
size="1024x1024",
)
with open("out.png", "wb") as f:
f.write(image.data[0].data)
print(image.selected_model, image.data[0].mime_type)
speech = client.audio.speech.create(
model="venice/kokoro-text-to-speech",
input="The quick brown fox jumps over the lazy dog.",
voice="af_sky",
)
speech.write_to("out.mp3")
```
> **The model IDs above are illustrative**
>
> Catalogue IDs change as routes are added and retired, so do not copy them
> blindly. List what your key can actually reach and filter on `model_type`:
> `image` for image generation, `tts` for speech:
>
> ```bash
> curl -s https://control.anonrouter.ai/v1/models \
> -H "Authorization: Bearer $ANONROUTER_API_KEY" \
> | jq -r '.data[] | select(.model_type=="image" or .model_type=="tts") | "\(.model_type)\t\(.id)"'
> ```
>
> Your key goes to the control origin here, which is where a content-free
> catalogue read belongs. `api.anonrouter.ai` also answers `/v1/models`, but only
> in compatibility mode and only for a key that has it enabled.
>
> Available voices are per-model and reported by the catalogue. You can also
> browse the [models page](/models).
## The official OpenAI SDK cannot do this
This is the reason these methods exist rather than a base-URL swap.
An OpenAI client is built around **one base URL and one credential**. Every
request it makes carries the API key to the host it is configured with. To run the
exchange above it would have to send a content-free request to host A with the
key, read a ticket out of the response, send the content to **host B** with the
ticket and *without* the key, and bind and re-check the ticket's facts between the
two. No configuration of `openai`, Python or Node, does that.
| If you point a stock OpenAI client at… | What happens |
| -------------------------------------- | --------------------------------------------------------------------------------------------- |
| `api.anonrouter.ai` | content answers **401 `ticket_required`** without a one-use ticket |
| `control.anonrouter.ai` | media answers **503 `media_disabled`**; the control plane serves no media content at all |
**Both failures are the design working.** The SDK methods on this page perform the
exchange automatically.
> **Compatibility mode is a different, lower-privacy option**
>
> AnonRouter also offers an [OpenAI compatibility mode](/compatibility) where one
> endpoint accepts an ordinary OpenAI-shaped request and performs the mint
> internally. It is a real option and it is not what these methods use. The
> difference is not cosmetic: in that mode a single AnonRouter-operated service
> receives your API key **and** your prompt in the same request, and does the split
> on your behalf on the far side of that boundary. In the two-origin exchange the
> split happens in *your* process, before anything leaves it. **The SDK never
> selects compatibility mode implicitly**. There is no fallback to it and no flag
> that enables it. If a call cannot be made privately it fails.
## Compatibility matrix
Unsupported OpenAI parameters are **refused, not dropped**. Silently ignoring
`n: 4` would hand back one image and charge for one while you believed you asked
for four.
### `images.generate`
| Parameter | Supported | Behaviour |
| -------------------------- | --------------- | ---------------------------------------------------------------------------------------------- |
| `model` | yes | required; bound into the ticket |
| `prompt` | yes | required; **sent only to the confidential origin**; max 10,000 characters |
| `size` | yes | `"WIDTHxHEIGHT"`, 128–2048 per side, default `1024x1024`; bound into the ticket |
| `response_format` | `b64_json` only | any other value is refused |
| `n` | `1` only | `n: 1` is accepted; anything else is refused, because one image per ticket is what gets priced |
| `quality`, `style`, `user` | no | refused as unknown keys |
Returns `created`, `model`, and `data[]` with `b64_json`, `mime_type`, and the
decoded bytes (`bytes` in JS, `data` in Python).
### `audio.speech.create`
| Parameter | Supported | Behaviour |
| ----------------- | ---------- | ---------------------------------------------------------------------------------------------- |
| `model` | yes | required; bound into the ticket |
| `input` | yes | required; **sent only to the confidential origin**; max 20,000 characters |
| `voice` | yes | optional; bound into the ticket. Omitting it binds "no voice", which is a distinct bound value |
| `response_format` | `mp3` only | any other value is refused |
| `speed` | `1` only | refused otherwise, because playback rate is not part of the authorized work |
Returns the complete audio buffer (`audio`) and its `content_type`. This endpoint
returns one response body rather than a stream, and the SDK does not pretend
otherwise. Python adds `result.write_to(path)`.
### Response metadata, on both
Kept because it is how you reconcile a generation against your bill.
| Field | Source |
| ------------------------------------------------ | ---------------------------------------------- |
| `selected_model` | the exact `provider/model` that ran |
| `routing` | how the model was chosen |
| `request_id` | the id that appears in your ledger |
| `rate_limit` | remaining requests and tokens after the call |
| `provider_blurred` *(image)* | the provider's safety layer blurred the result |
| `provider_content_violation` *(image)* | the provider flagged a policy violation |
## What the ticket binds
The confidential origin independently re-derives every bound fact from the body
you send and answers **409** on any drift. The SDK checks the mint's echo *first*,
so a mismatch fails on the content-free half of the exchange and **the prompt is
never sent at all**.
| Operation | Bound facts |
| --------- | ------------------------------------------------------------------------- |
| image | operation, model, width, height, `response_format` |
| speech | operation, model, exact input character count, `voice`, `response_format` |
### The speech character count is UTF-16 code units
Speech is priced per character, so the ticket binds the **exact** count and the
relay rejects a body that differs by one.
AnonRouter counts UTF-16 code units. Python's `len()` counts code points. They
disagree on every emoji and every astral-plane character: `"Hi 😀"` is **4** to
`len()` and **5** to the server.
The Python SDK therefore counts UTF-16 code units, exported as `utf16_length`. A
client that used `len()` would mint a ticket bound to the wrong number and get a
409 on exactly the inputs containing emoji. If you are building your own client
against the raw API, this is the detail that will bite you.
## Errors
Typed, with a machine-readable `code` and diagnostics that carry no prompt, key,
or ticket. The distinction that matters is whether money moved.
| Code | Meaning | Charged? |
| ------------------------- | -------------------------------------------------------------------- | ----------------------- |
| `unsupported_request` | refused locally, before any network call | no |
| `media_ticket_failed` | the control origin would not mint | no |
| `ticket_binding_mismatch` | the ticket bound facts you did not request; **content was not sent** | no |
| `ticket_rejected` | expired, already spent, or drifted | no |
| `relay_refused` | refused outright: no ticket, `media_disabled`, quota | no |
| `provider_failed` | reached a provider and failed there | **possibly** |
| `response_invalid` | the response was not well-formed media | the generation happened |
| `transport_failed` | the connection failed mid-request | **unknown** |
| `cancelled` / `timeout` | your abort signal, or a deadline | **unknown** |
Both `MediaError` types subclass the package's `ConfidentialError`, so an existing
`catch` / `except ConfidentialError` keeps catching everything.
```ts
import { MediaError } from "@anonrouter/confidential";
try {
await client.images.generate({ model, prompt });
} catch (error) {
if (error instanceof MediaError) {
// `provider_failed` is the only code that may correspond to work a provider
// actually attempted and charged for.
console.error(error.code, error.diagnostics.request_id);
// Header values are already redacted.
console.error(error.diagnostics.headers);
}
}
```
```python
from anonrouter_confidential import MediaError
try:
client.images.generate(model=model, prompt=prompt)
except MediaError as error:
# `provider_failed` is the only code that may correspond to work a provider
# actually attempted and charged for.
print(error.code, error.diagnostics.request_id)
# Header values are already redacted.
print(error.diagnostics.headers)
```
### No automatic retry
**A failed media POST is never retried.** A generation is billed on the provider
attempt, so a transparently retried POST is a second charge for a call you made
once. This holds for 500, 502, 503, 429, 408 and transport failures alike. Tickets
are single-use, so a retry would fail with `ticket_rejected` anyway, but the rule
is enforced regardless, and pinned by tests that count POSTs.
If you want to retry, do it deliberately in your own code, having decided that a
duplicate generation is acceptable.
### Nothing sensitive in an error
Messages and diagnostics never contain the prompt, the input text, the API key, or
the ticket. Diagnostics carry the origin, path, status, the machine-readable error
`type`, the request id, and the request headers **with every credential value
replaced by ``**: names preserved, values gone. An error body that
quotes your prompt back is discarded rather than passed through. `redactHeaders` /
`redact_headers` is exported so the same rule can apply to anything you log
yourself.
## Configuration
| Option (JS / Python) | Default |
| ------------------------------------------------------------------------- | ------------------------------- |
| `inferenceBaseUrl` / `inference_base_url` (alias: `baseUrl` / `base_url`) | `https://api.anonrouter.ai` |
| `controlBaseUrl` / `control_base_url` | `https://control.anonrouter.ai` |
Two rules, both fail-closed:
* **Media requires two distinct origins.** If the control and inference origins are
the same host, media is refused, because that host would receive the API key and the
prompt together, which is the exact linkage the ticket prevents. Verification and
E2EE chat are unaffected, because in E2EE chat the relay receives ciphertext, so
a single origin still never holds readable content.
* **Two origins that disagree are refused**, not resolved by precedence, and so is
an origin supplied as an empty string. The ambiguity is about where prompts go.
The one documented exception is a **loopback local-test override**: with
`allowInsecureHttp` / `allow_insecure_http` set *and* a loopback host, a single
origin is permitted, because a developer running both roles on one machine has no
privacy boundary to collapse. It cannot be reached for a remote host.
## Checking the contract yourself, for free
Every claim about how the two origins behave can be checked without a key and
without spending anything. These probes carry no credential and no ticket, so the
relay refuses them before any provider is dispatched:
```bash
# The confidential origin serves media and fails closed without a ticket.
curl -s -X POST https://api.anonrouter.ai/v1/images/generations \
-H 'content-type: application/json' -d '{}'
# {"error":{"message":"A single-use ticket is required","type":"ticket_required",...}}
# The control origin serves no media content at all.
curl -s -X POST https://control.anonrouter.ai/v1/audio/speech \
-H 'content-type: application/json' -d '{}'
# {"error":{"message":"media generation is disabled","type":"media_disabled"}}
```
The SDK ships these as live tests. They need no key and no DCAP engine, so anyone
can reproduce them:
```bash
ANONROUTER_LIVE_GATEWAY_ORIGIN=https://api.anonrouter.ai \
ANONROUTER_LIVE_PUBLIC_ORIGIN=https://control.anonrouter.ai \
npx vitest run test/live-media.test.ts # in js/confidential
```
> **Verify the plane before you send a prompt**
>
> Media content goes to the same origin the gateway attestation covers, so you can
> establish the plane first and only then generate. See
> [verification](/sdk/confidential#verifyroute--the-stable-contract).
---
# anonrouter-verify
The command both SDK packages install. It prints one JSON document and exits nonzero unless the assurance you asked for was established, so it can gate a deploy rather than only inform one.
> **Install SDK 0.1.3**
>
> Install `@anonrouter/confidential@0.1.3` from npm, or the Python wheel from
> [`v0.1.3`](https://github.com/anonrouter/anonrouter-sdk/releases/tag/v0.1.3)
> with the `connection-binding` extra. Python is not on PyPI. See
> [installation](/sdk/confidential#install). Install the native DCAP verifier
> separately to verify the production content nodes.
Installing either SDK package installs `anonrouter-verify`. It answers one
question from a terminal (*did this route establish the assurance I require?*)
and answers it with an **exit code**, so it can gate a deploy or a CI job rather
than only inform one.
Both languages ship the same command. A cross-language parity gate runs both real
executables over the same inputs and requires the JSON they print and the codes
they exit with to be identical, so it does not matter which one you have.
## Three subcommands
```bash
# Hop 1 only: is this data plane the reviewed build, in a TDX CVM, bound to a
# fresh nonce and this origin? Credential-free.
anonrouter-verify gateway --origin https://api.anonrouter.ai --dcap --require hardware_verified
# Both hops, cross-bound to a route. Needs an API key for hop 2.
anonrouter-verify route --origin https://api.anonrouter.ai \
--control-origin https://control.anonrouter.ai \
--provider venice --model venice-uncensored --dcap
# What can this machine establish at all? Contacts nothing unless --origin is given.
anonrouter-verify doctor --origin https://api.anonrouter.ai
```
> **`gateway` needs no credential**
>
> Hop 1 is credential-free by design: a client should be able to verify the plane
> *before* it trusts that endpoint with anything. You can run it against a
> deployment you have no account on.
## Exit codes
| Code | Meaning |
| ---- | -------------------------------------------------- |
| `0` | the requested assurance was established |
| `1` | it was not, **including "we could not look"** |
| `2` | the command or its inputs were wrong |
That third code exists so a typo cannot be mistaken for a verification failure,
and the second covers `unavailable` as well as `untrusted`: a deployment that does
not expose attestation has not passed anything.
```bash
anonrouter-verify gateway --origin https://api.anonrouter.ai --dcap \
--require hardware_verified
echo $? # 0 met, 1 not met, 2 the command itself was wrong
```
## Gating a deploy
`--require` sets the minimum state that exits `0`. The default is
`cryptographically_checked`.
```bash
#!/usr/bin/env bash
set -euo pipefail
anonrouter-verify gateway \
--origin https://api.anonrouter.ai \
--dcap --require hardware_verified --compact
echo "plane established; proceeding with deploy"
```
With `set -e`, a plane that stopped matching the reviewed pin stops the deploy.
See [the five states](/sdk/confidential#the-five-states) for what each level of
`--require` actually proves.
## Reaching `hardware_verified`
`--dcap` chains the quote's signature to Intel's roots using the reviewed engine.
It is mandatory for the content nodes behind `api.anonrouter.ai`. With
`--dcap`, 0.1.3 fetches evidence and observes the TLS key on one retained socket.
Without it the production gateway hop is `unavailable`, so verification exits nonzero.
Other platforms may have a `cryptographically_checked` ceiling without DCAP.
```bash
anonrouter-verify doctor # is an engine installed? which one? what digest?
```
`doctor` reports what this machine can establish (the engine, its digest, the
host target, and whether a pin ships for the origin) before you depend on it in
CI.
| Flag | What it is for |
| ----------------------- | --------------------------------------------------------- |
| `--dcap` | chain the quote to Intel's roots with the reviewed engine |
| `--dcap-binary ` | use this engine explicitly instead of discovering one |
| `--dcap-sha256 ` | refuse to run an engine whose SHA-256 differs |
| `--collateral ` | supply Intel collateral instead of fetching it |
| `--no-collateral-fetch` | refuse to fetch collateral (needs `--collateral`) |
## Pinning the policy yourself
For `api.anonrouter.ai`, the default policy is the owner-signed release-v3
feed, verified against the owner root packaged with SDK 0.1.3. Approved node
image changes therefore need no SDK update. The SDK persists the highest
accepted sequence and rejects rollback. If refresh fails, a previously verified
policy can be reused for at most one hour.
Use `--policy` only for an independently reviewed policy, such as your own
deployment. Never copy the server's claimed measurements into a policy to make
a failed verification pass.
```bash
anonrouter-verify gateway --origin https://your-cvm.example \
--policy ./reviewed-policy.json --dcap --require hardware_verified
```
`--allow-candidate` accepts a shipped pin whose status is `candidate`: a
reviewed but pre-release plane whose measurements are expected to move. It is off
by default, so a default invocation can never quietly pass against a plane the
operator has not released.
> **Upgrade older confidential clients**
>
> Versions before 0.1.3 pin the earlier deployment and cannot verify the current
> content nodes. Upgrade to 0.1.3. A verdict may report advisory gaps for
> firmware CCEL replay or optional cloud instance identity; inspect the individual
> checks instead of treating `hardware_verified` as proof that every advisory
> check passed. Browser code cannot perform native DCAP or observe the retained
> TLS key; run this check on a host you control.
## Credentials and output
* The API key for `route` is read **only from an environment variable**, never
from `argv`, where it would be visible in the process table. Change which
variable with `--api-key-env`.
* The command prints **one JSON document** on stdout (`--compact` for one line)
and diagnostics on stderr, so a caller can branch on the exit code and still
read why.
* It never prints a key, a ticket, request content, or the raw evidence body. The
gateway document alone is tens of kilobytes of internal build detail; the
command reports a verdict over it rather than dumping it.
```bash
anonrouter-verify gateway --origin https://api.anonrouter.ai --dcap --require hardware_verified --compact \
| jq '{schema, state: .gateway.state, reason: .gateway.reason}'
```
Every document carries a `schema` field (`anonrouter-verify/1`), so a consumer can
detect a format change rather than silently misparse one.
## Full options
```
anonrouter-verify --help
```
| Option | Default |
| ------------------------ | ----------------------------------------------------------- |
| `--origin ` | required for `gateway` / `route` |
| `--control-origin ` | `--origin`; production uses `https://control.anonrouter.ai` |
| `--require ` | `cryptographically_checked` |
| `--api-key-env ` | `ANONROUTER_API_KEY` |
| `--timeout ` | `30000` |
| `--no-tls-check` | off (TLS observation is on by default) |
| `--compact` | off |
> **`--control-origin` does not split verification from content**
>
> It only moves the content-free, API-key operations to the identity origin. Both
> attestation hops and any request content stay on `--origin`. The SDK deliberately
> offers no way to verify one inference origin while sending content to another.
> A verdict about a plane that did not carry your prompt proves nothing about the
> one that did.