# 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.
Your client the privacy split happens here IDENTITY AND CONTROL PLANE control.anonrouter.ai Knows your key, account, balance, and policy. Receives model and priced limits, never the prompt. Mints one-use opaque ticket bound to model · limits · policy INTEL TDX · CONFIDENTIAL VM api.anonrouter.ai TLS terminates inside the attested CVM Ticket relay prompt + ticket · no stable key or account Provider workers isolated credentials · model-bound payload Optional compatibility broker lives here too 1 · key + content-free limits 2 · opaque ticket 3 · ticket + prompt verify attestation before sending EXTERNAL MODEL PROVIDER Sees plaintext on ordinary routes. Provider E2EE routes keep it encrypted. 4 · provider call 5 · usage metadata only for settlement COMPATIBILITY MODE One URL, but key and prompt meet in the CVM. Host-blind, but not unlinkable like the ticket flow. No prompt or response is intentionally persisted by AnonRouter.
## 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.
OpenCode, the open source AI coding agent
> **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 logo 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.
Janitor AI dog mascot standing beneath a spiral-shaped cloud
--- # 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 logo 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 logo 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 logo 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.
Jan open-source AI platform banner
> **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 icon 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.
Hermes Agent wordmark
> **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.