API Reference¶
Continuum Router exposes an OpenAI-compatible API with additional administrative endpoints for monitoring and management. This document covers all available endpoints, request/response formats, and error handling.
Table of Contents¶
- Overview
- Authentication
- Core API Endpoints
- Health Check
- Readiness Check
- Version
- Caller Quota
- List Models
- List Models with Metadata
- Proxy a Backend Model List
- Force-Refresh Models
- Get Single Model
- Chat Completions
- Responses API
- Retrieve, Delete, or Cancel a Stored Response
- Responses Compact
- Batch API
- Image Generation
- Image Edit (Inpainting)
- Image Variations
- Audio Transcriptions & Translations
- Text Completions
- Embeddings
- Rerank
- Moderations
- Tokenize and Detokenize
- Decisions (System One)
- Sparse Embeddings
- Files API
- File Resolution in Chat Completions
- Anthropic Native API
- Admin API
- Admin REST API reference
- Error Handling
- Rate Limiting
- Streaming
- Examples
Overview¶
Base URL¶
Content Type¶
All requests and responses use application/json unless otherwise specified.
OpenAI Compatibility¶
Continuum Router provides OpenAI-compatible API surfaces for: - Chat completions with streaming - Text completions - Embeddings - Reranking (Cohere-compatible /v1/rerank) - Moderations (/v1/moderations, classified by the configured guardrails) - System One decisions (/v1/decisions, alias /v1/systemone) - Sparse embeddings (TEI/Jina-compatible /embed_sparse) - Image generation - Image editing/inpainting - Image variations - Files API (upload, list, retrieve, delete) - File resolution in chat completions (image_file references) - Model listing - Error response formats
Choosing a Backend with x-backend¶
Several backends can serve the same model id. /v1/models collapses them into one aggregated entry whose backends array lists every backend name serving that id, and by default the router load-balances across them.
Send the optional x-backend request header to name the backend you want:
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-H "x-backend: provider-openai-a1b2c3d4" \
-d '{"model": "gpt-5.6-terra", "messages": [{"role": "user", "content": "Hello"}]}'
Where it applies¶
The router honors x-backend across all model-routed inference endpoints (both streaming and non-streaming):
/v1/chat/completions,/v1/completions, and/v1/responses/anthropic/v1/messagesand/anthropic/v1/messages/count_tokens(and their root-level aliases/v1/messagesand/v1/messages/count_tokens)/v1/embeddings,/v1/rerank, and/embed_sparse/tokenizeand/detokenize(and their/v1aliases)/v1/images/generations,/v1/images/edits, and/v1/images/variations/v1/audio/transcriptions,/v1/audio/translations, and/v1/audio/speech/v1/realtimeWebSocket handshakes
The header is not read on these paths:
| Path | Reason |
|---|---|
| ACP prompts | The Agent Client Protocol carries JSON-RPC calls rather than HTTP requests, so no header reaches backend selection. |
/v1/batches and the Batch API (control-plane builds) | The batch backend is resolved from control_plane.batch.backend (or the first dispatchable OpenAI-style or Anthropic-style backend) rather than from the request's model. Status, cancel, and results must reach the backend that accepted the batch. |
/v1/fine_tuning/jobs* | Job CRUD is not a completion dispatch. Create selects a backend from the request's model, list selects one fine-tuning capable backend for the caller, and retrieve and cancel use the backend that created the job. |
/v1/vector_stores* | Store calls name no model. Create and list select one vector-store capable backend for the caller, and every store-scoped call uses the backend that created the store. |
How it behaves¶
x-backend is a routing preference, never a hard pin. The named backend is used on the first attempt when all four eligibility conditions hold:
- User-routable: The backend is enabled (
enabled: true) and not internal (internal: false). - Allow-listed: The backend is permitted by the API key's
allowed_backendslist, when one is configured. - In the candidate pool: The backend belongs to the candidate set the request would otherwise select from:
- Model-routed endpoints: Backends serving the requested model.
- Nano Banana (Gemini-native) image requests: Must name a
type: geminibackend (backend name and URL are not consulted), because the request is dispatched to the Gemini endpoint using the selected backend's credential; non-Gemini backends are rejected rather than borrowed. - OpenAI-compatible
/v1/images/editsand/v1/images/variations: Capability filtering is bypassed here because the request goes directly to the named backend's URL with its own credential. Naming a visible, allow-listed backend that lacks image-edit or variation support sends the request to that backend, where it fails rather than falling back to normal selection.
- Healthy and available: The backend is healthy and its circuit breaker admits the request.
If any check fails, the router logs a warning and falls back to normal selection. An absent, unknown, invisible, disallowed, or unhealthy backend name never fails a request; it behaves as if the header had been omitted.
- First attempt only: Retries and fallback-chain hops use normal selection, so a backend that fails mid-request still fails over.
/v1/realtimesessions: A realtime session selects its backend once during the WebSocket handshake and binds to that backend for the lifetime of the session. There is no second selection or mid-session failover.
Confirming which backend answered¶
When a request includes x-backend, the response includes x-served-backend naming the backend that actually produced the response (including after a retry or fallback-chain hop):
curl -sS -D - -o /dev/null http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-H "x-backend: provider-openai-a1b2c3d4" \
-d '{"model": "gpt-5.6-terra", "messages": [{"role": "user", "content": "Hello"}]}' \
| grep -i '^x-served-backend:'
- Value matches
x-backend: The preference was honored. - Value differs from
x-backend: The preference was not eligible (or a retry/fallback occurred) and another backend answered. - Header absent: Either the response did not come from an upstream backend (see table below) or the route does not read
x-backend. Never interpret an absent header as confirmation that the preference applied.
Errors a backend returned¶
The router attaches x-served-backend only when the request carried x-backend and the router holds an HTTP response authored by the upstream backend itself (for example, an upstream 429 quota error). Router-authored responses and transport failures never carry the header, regardless of HTTP status code.
x-served-backend Present | x-served-backend Omitted |
|---|---|
Upstream 2xx responses across all supported routes (except mid-stream relay and cache hits below). | Router-authored errors: Request validation (400), per-key allow-list (403), no healthy backend (503), rejected circuit-breaker admission, or an exhausted fallback chain. |
Upstream HTTP errors (such as 429 or 5xx) on routes using the shared retry/fallback funnel (/v1/chat/completions, /v1/completions, text /v1/embeddings, /v1/rerank, /embed_sparse, non-streaming /v1/images/generations) and upstream 4xx handshakes on streaming /v1/chat/completions. | Transport failures and timeouts: The backend returned no HTTP response. |
Guardrail Transform verdicts on non-streaming responses, and streaming responses where SSE headers were already committed before a mid-stream guardrail block. | Guardrail Block verdicts (non-streaming): The router discarded the backend response and authored the blocked reply (200 or 400). |
Final-turn responses of a successful web_search tool loop on /v1/chat/completions (names the backend that served the final turn). | web_search tool-loop failures: The failing turn is converted into a router error. |
Pre-stream native-protocol fallback hops, pre-stream-only streaming, and no-chain streaming on /v1/chat/completions. | Mid-stream relay streaming (fallback.enabled with a model chain and fallback.mid_stream_enabled: true): The relay can switch backends after SSE headers are sent. |
Exact backend /tokenize or /v1/messages/count_tokens responses. | Local or cached responses: Response-cache hits, prefix-cache hits, and /anthropic/v1/messages/count_tokens answered by the local character-based estimator. |
Errors on routes without error-attribution plumbing: /anthropic/v1/messages, /v1/responses, /v1/responses/compact, responses_only passthrough, /v1/images/edits, /v1/images/variations, multimodal /v1/embeddings, count_tokens, and mid-stream SSE error events. | |
Non-HTTP or unattributed handshakes: /v1/realtime 101 Switching Protocols upgrades, ACP prompts, and /v1/batches*. |
Not a metrics label
x-backend is client-supplied and is never used as a Prometheus label. Metrics always record the backend that actually served the request.
Detecting support (backend_preference_header_v1)¶
- Via Admin API:
GET /admin/capabilitiesadvertisesbackend_preference_header_v1in itscapabilitiesarray when the router supportsx-backend(see Admin REST API reference). - Via Inference API: Send
x-backendon a normal inference request and check forx-served-backendon a successful (200) non-cached response. A presentx-served-backendheader confirms support; comparing its value withx-backendshows whether the preference was honored.
Authentication¶
Continuum Router supports API key authentication with configurable enforcement modes.
Authentication Modes¶
The router supports two authentication modes for API endpoints:
| Mode | Behavior |
|---|---|
permissive (default) | Requests without API key are allowed. Requests with valid API keys are authenticated and can access user-specific features. |
blocking | Only authenticated requests are processed. Requests without valid API key receive 401 Unauthorized. |
Configuration¶
api_keys:
# Authentication mode: "permissive" (default) or "blocking"
mode: blocking
# API key definitions
api_keys:
- key: "${API_KEY_1}"
id: "key-production-1"
user_id: "user-admin"
organization_id: "org-main"
scopes: [read, write, files, admin]
Protected Endpoints (when mode is blocking)¶
API-key middleware protects all routes in the core API router: /v1/models*, /v1/quota, /v1/chat/completions, /v1/completions, /v1/embeddings, /v1/responses*, /v1/images/*, /v1/rerank, /v1/moderations, /v1/fine_tuning/jobs*, /v1/vector_stores*, /tokenize, /detokenize (and their /v1 aliases), /v1/decisions, /v1/systemone, /embed_sparse, /anthropic/v1/messages*, /v1/messages*, /anthropic/v1/models, and feature-gated /v1/batches* routes.
When a key has a non-empty scopes list, blocking mode also enforces the route's read or write class. write implies read, admin implies both, and files controls the separate Files API rather than satisfying /v1 read/write routes. Empty or absent scopes remains unrestricted for backward compatibility; permissive mode never rejects a request solely because an authenticated key has narrow scopes. See Security & Admin for the full route classification.
Note: Liveness, readiness, and version endpoints (/health, /health/ready, /version) are always accessible without authentication. Admin, Files, and Metrics endpoints have separate authentication mechanisms.
A key can also state prompt-capture consent with prompt_logging_consent (allowed or denied). See Where a key's consent comes from.
Making Authenticated Requests¶
Every protected route accepts the API key in either of two header forms: Authorization: Bearer <api_key> or the Anthropic-native x-api-key: <api_key>. Both forms work on every route, not only the /anthropic/* endpoints, so Anthropic SDKs and Claude Code (which set only ANTHROPIC_API_KEY and send it via x-api-key) authenticate the same way OpenAI-style clients do. When a request carries both headers, Authorization: Bearer takes precedence.
POST /v1/chat/completions HTTP/1.1
Authorization: Bearer sk-your-api-key
Content-Type: application/json
{
"model": "gpt-5.6-sol",
"messages": [{"role": "user", "content": "Hello"}]
}
POST /v1/chat/completions HTTP/1.1
x-api-key: sk-your-api-key
Content-Type: application/json
{
"model": "gpt-5.6-sol",
"messages": [{"role": "user", "content": "Hello"}]
}
Authentication Errors¶
When authentication fails, the API returns:
{
"error": {
"message": "Missing or invalid API key. Expected Authorization: Bearer <api_key> or x-api-key",
"type": "authentication_error",
"code": "invalid_api_key"
}
}
Status Codes:
401 Unauthorized: Missing or invalid API key
Core API Endpoints¶
Health Check¶
Check the health status of the router service.
Response:
Status Codes:
200: Service is healthy
Readiness Check¶
Check whether this replica has at least one active backend that is enabled and non-internal. Draining backends do not count. Upstream health and circuit-breaker state remain request-selection concerns and do not change this endpoint.
Ready response:
When the serving pool has no routable backend, the same body uses "status": "not_ready" and a count of 0.
Status Codes:
200: At least one enabled, non-internal backend is active503: No enabled, non-internal backend is active
Version¶
Return the running router version.
Response:
Status Codes:
200: Always returned; no authentication required
Caller Quota¶
GET /v1/quota returns the limits bound to the caller's authenticated identity and how much of each remains. It takes no parameters and no body, and it consumes no identity-bound quota, so a client can poll it without spending what it reads.
The response reports only values tied to the caller's identity. Shared fleet levels (global, per_client, per_model, per_backend), the batch dimensions, and every key id, name, user, and team are never included.
{
"object": "quota",
"degraded": false,
"dimensions": [
{"dimension": "request_rate", "unit": "requests", "limit": 20, "remaining": 17, "reset_seconds": 9, "window_seconds": null, "scope": "replica"},
{"dimension": "token_rate", "unit": "tokens", "limit": 20000, "remaining": 18400, "reset_seconds": 6, "window_seconds": null, "scope": "fleet"},
{"dimension": "token_budget", "unit": "tokens", "limit": 5000000, "remaining": 4100000, "reset_seconds": null, "window_seconds": 2592000, "scope": "replica"}
]
}
dimension | Present when | Reported values |
|---|---|---|
request_rate | The key has a per-key request rate (rate_limiting.limits.per_api_key or api_keys[].rate_limit) | Bucket capacity and remaining requests, matching x-ratelimit-limit and x-ratelimit-remaining |
token_rate | rate_limiting.limits.per_api_key.tokens_per_minute is set | Minute token capacity and remaining tokens, matching x-ratelimit-tokens-* |
user_request_rate | rate_limiting.limits.per_user is set and the key resolves a user | The user's shared request bucket |
team_request_rate | rate_limiting.limits.per_team is set and the key resolves a team | The team's shared request bucket |
inflight | max_in_flight_requests is set on per_api_key | The in-flight ceiling and the slots left |
token_budget | The key has max_tokens_budget | The budget, and the budget minus usage and in-flight reservations; window_seconds carries the rolling window |
hub_rpm, hub_input_tpm, hub_output_tpm, hub_monthly_tokens, hub_monthly_requests | The token matches a Hub-synced key whose tier sets that limit | The tier limit and remaining units, matching x-continuum-ratelimit-* |
scope states how far a value holds: fleet for a value read from a store every replica shares, replica otherwise. request_rate, user_request_rate, team_request_rate, inflight, and token_budget are per replica. degraded is true when a configured shared store was unreachable and the read fell back to this replica, and also when the Redis rate-limit store fell back according to rate_limiting.redis.fallback_to_memory.
Who sees what:
| Caller | Response |
|---|---|
| Blocking mode, missing or invalid token, revoked Hub key | The existing 401 |
Key whose scopes lack read | The existing 403 |
| Permissive mode, no token or unrecognized token | 200 with dimensions: [] |
| Recognized key with no configured limit | 200 with dimensions: [], identical to the row above |
Recognized key on bypass_keys or from a whitelisted IP | request_rate and token_rate omitted; token_budget and Hub rows still apply |
| Recognized key otherwise | Every row that applies |
An exhausted key gets 200 with remaining: 0 rather than a refusal. The shared global and per_client margins are still charged at the /v1/models cost of 0.1, so the route is metered before authentication like every other API path, but their levels are not reported. A quota response carries no x-ratelimit-* or x-continuum-ratelimit-* header.
Status Codes:
200: Quota read, possibly with an emptydimensionslist401: Missing or invalid key in blocking mode403: The key's scopes lackread429: The sharedglobalorper_clientmargin refused the read503:rate_limiting.redis.fallback_to_memory: falseand the Redis store is unreachable
See Rate Limiting for the read semantics and outage behavior.
List Models¶
Retrieve the aggregated model catalog from configured, enabled backends.
Response:
{
"object": "list",
"data": [
{
"id": "gpt-5.6-sol",
"object": "model",
"created": 1677610602,
"owned_by": "openai",
"context_length": 400000,
"max_output_tokens": 128000,
"pricing": {
"input_tokens": 1.25,
"output_tokens": 10.0
},
"capabilities": ["chat", "vision", "tool"],
"supported_parameters": ["temperature", "top_p", "max_tokens", "presence_penalty", "frequency_penalty", "logprobs", "top_logprobs"]
},
{
"id": "llama2:7b",
"object": "model",
"created": 1677610602,
"owned_by": "library",
"context_length": 4096,
"max_output_tokens": 4096,
"supported_parameters": ["temperature", "top_p", "max_tokens", "presence_penalty", "frequency_penalty", "top_k", "min_p", "logprobs", "top_logprobs"]
},
{
"id": "anthropic/claude-haiku-4.5",
"object": "model",
"created": 1760547638,
"owned_by": "anthropic",
"context_length": 200000,
"max_output_tokens": 64000,
"pricing": {
"input_tokens": 1.0,
"output_tokens": 5.0
},
"capabilities": ["chat", "vision", "tool"],
"supported_parameters": ["temperature", "top_p", "max_tokens", "presence_penalty", "frequency_penalty", "top_k", "min_p", "logprobs", "top_logprobs"]
}
]
}
Status Codes:
200: Models retrieved successfully503: All backends are unhealthy
Features:
- Model Aggregation: Combines models from configured, enabled backends
- Deduplication: Removes duplicate models across backends
- Caching: An internal 60-second hard TTL with stale-while-revalidate and request coalescing
- Outage Retention: When live discovery fails, the router keeps the last-known models for that backend in its catalog. Discovery results the router served from configuration or a fallback list (Bedrock, OAuth backends, the curated Anthropic list) never mark a backend healthy; only a live answer does.
- Routable Models Only: The list advertises a model only while the caller can request it, under these rules:
- At least one of its backends that the caller's key may use is not marked unhealthy (a backend not yet probed or still warming up counts as usable), or
- An enabled fallback chain is configured for the model name, so a degraded primary stays visible to clients that build model pickers from this endpoint and can still reach its fallback.
- Otherwise the model is withheld until a backend recovers.
GET /v1/models/{model}still answers200withavailable: falsefor it, andGET /admin/modelsshows it asdegraded./v1/models/extended,/v1/models/refresh, and/anthropic/v1/modelsapply the same rule.
- Backend Aliases: Each
aliasesentry in an enabled backend'smodel_configsis listed as its own entry, with the canonical model'sbackendsand metadata, while alias dispatch would resolve it:- The alias is listed only while a user-routable backend serves the canonical model, and it leaves the list on the refresh that drops the canonical model.
- A per-key
allowed_modelslist must name the alias itself; the backend allow-list, internal backends, and the routable-models rule apply through the canonical model's backends. - A name some backend serves literally keeps its own entry, and a name a model experiment answers for is left to the experiment.
- Aliases from
model-metadata.yamland themodel_aliasessection (haiku,sonnet,exact, and the others) are not listed.GET /admin/modelsreports each declared alias in the canonical entry'saliases.
- Vendor Attribution: When a backend sends no
owned_byfor avendor/modelid, as OpenRouter does for every entry,owned_byis the id's vendor namespace (anthropic/claude-haiku-4.5reportsanthropic). See Upstream vendor and metadata -
Capability Fields: Entries carry the following fields when the router knows their values, from the resolved model metadata (
model-metadata.yaml, a backend's own report, or a built-in catalog entry):context_length: context window in tokens.max_output_tokens: maximum output tokens.pricing: USD per 1M tokens.capabilities: tags such aschat,vision, andtool.supported_parameters: the chat request parameters the router carries to the backend, fromtemperature,top_p,max_tokens,presence_penalty,frequency_penalty,top_k,min_p,logprobs, andtop_logprobs. It is derived from each serving backend's conversion (a local engine or raw OpenAI-compatible backend carries all of them;api.openai.comand Gemini droptop_kandmin_p; Anthropic and Bedrock carrytemperature,top_p,max_tokens, andtop_k, or onlymax_tokensfor a Claude model that refuses sampling fields; Bedrock Converse dropstop_kas well), and a parameter is listed only when every backend serving the model carries it. A model whosecapabilitieslist neitherchatnor thetextoutput modality an upstream catalog reports (an image or embedding model, say) has nosupported_parameters. A model whose own request shape narrows the set further (for example a Claude request withreasoning, which dropstemperature) is not reflected.
When no
model-metadata.yamlentry setslimits,context_lengthcomes from the context windows the serving engines report (max_model_lenorcontext_lengthon their/v1/models), folded to the minimum across backends, since any of them may serve the request. A field with no known value is omitted rather than sent asnull; a model without metadata or engine reports carries onlyid,object,created,owned_by, andsupported_parameters.
List Models with Metadata¶
Retrieve the same aggregated catalog as GET /v1/models, with the router's resolved metadata and the backends serving each model.
Response:
{
"object": "list",
"data": [
{
"id": "qwen3-32b",
"object": "model",
"created": 1700000000,
"owned_by": "vllm",
"backends": ["vllm-large", "vllm-small"],
"metadata": {
"limits": {
"context_window": 32768,
"max_output": 32768
}
},
"max_model_len": 32768,
"root": "Qwen/Qwen3-32B"
}
]
}
Fields:
| Field | Type | Description |
|---|---|---|
id, object, created, owned_by | As on GET /v1/models | |
backends | array | Names of the backends serving the model, filtered by the caller's backend visibility |
metadata | object | The router's resolved metadata: configured model-metadata.yaml and model-metadata.d/ entries, backend model_configs, and values reported by the backends, combined as described under Get Single Model. Omitted when nothing is known |
tier, domains | Smart-routing tier information, present only when smart routing assigns it | |
| any other key | Upstream fields, see below |
Upstream fields: Every other top-level key is copied from one backend's own /v1/models entry for the model (with several backends, which one is not specified). These keys are a passthrough, not a contract: they appear only when a backend sent them and keep that backend's shape. Keys the router serializes itself (id, object, created, owned_by, backends, metadata, tier, domains) are never taken from upstream.
Which value is authoritative: metadata is the router's answer and the value it acts on. Where an upstream field restates something metadata also states, read metadata:
- The context window is the one exception the router reconciles.
max_model_len(vLLM-family engines) andcontext_length(aggregators such as OpenRouter), when present, are rewritten tometadata.limits.context_window, so the object never states two windows. That window is the operator's configuredlimitswhen metadata supplies them, otherwise the minimum that the serving backends report, since a request may reach any of them. Neither key is added when the backend did not send it, and neither is rewritten when the publishedcontext_windowis0(not applicable). - Every other upstream key stays verbatim. In particular an aggregator's
pricing(USD per token, as strings) andtop_provider.max_completion_tokensare left as the upstream sent them, whilemetadata.pricing(USD per 1M tokens) andmetadata.limits.max_outputcarry the router's values folded across backends. See Upstream vendor and metadata.
Status Codes:
200: Models retrieved successfully503: All backends are unhealthy
Query Models¶
Filter the aggregated catalog server side by capability and limit conditions. The endpoint returns the same model objects as GET /v1/models/extended, narrowed to the models the catalog proves satisfy every stated condition. A query no model satisfies is a successful response with an empty data array, not an error.
Request Body (all fields optional; an absent or empty body selects every visible model, so the endpoint then behaves like GET /v1/models/extended):
{
"capabilities": ["chat", "vision"],
"min_context_length": 64000,
"max_output": 4096,
"q": "gpt",
"limit": 20
}
Fields:
| Field | Type | Description |
|---|---|---|
capabilities | array | Required capabilities (AND): the model's metadata.capabilities must state every listed value. Models without capability metadata never match |
min_context_length | number | Minimum context window in tokens: metadata.limits.context_window must be greater than or equal to it. Models without limit metadata never match |
max_output | number | Minimum maximum-output size in tokens: metadata.limits.max_output must be greater than or equal to it. Models without limit metadata never match |
q | string | Case-insensitive (ASCII) substring match: the model id or metadata.display_name must contain it. A model without a display name is matched on id only |
limit | number | Maximum number of models to return. Applied after every filter, keeping the listing order. Omitted means no cap |
The filters apply to the exact listing GET /v1/models/extended returns for the same caller (alias and experiment names, the caller's backend and model allow-lists, internal backends, and the routability filter), so a query can only narrow that listing.
Validation:
capabilityis an accepted spelling ofcapabilitiesand also takes an array. A body with both spellings returns400(invalid_query).- Unknown fields are rejected with
400(invalid_query), so a misspelled filter cannot silently return every model. capabilitiesaccepts at most 32 entries of 1 to 64 bytes each. Matching is exact and case-sensitive.qmust be 1 to 256 bytes.limitmust be 1 to 1000.- Body size is capped at 16 KiB; a larger body returns
413. - Scope: the route requires the
readAPI-key scope, like the other model listings, because it changes nothing and dispatches no inference.
Example:
curl -s -X POST http://localhost:8080/v1/models/query \
-H 'Content-Type: application/json' \
-d '{"capabilities":["vision"],"min_context_length":64000}'
Status Codes:
200: Filtered model list returned (possibly empty)400: The request body is present but not a valid query (OpenAI-style error object, codeinvalid_query)413: The request body exceeds 16 KiB503: All backends are unhealthy
Proxy a Backend Model List¶
Return the raw /v1/models response from one selected eligible backend instead of aggregating all backends. This compatibility endpoint uses normal backend selection, retry, health, and caller backend-visibility rules.
Use GET /v1/models for the router's deduplicated aggregate catalog. Use this endpoint only when a client needs one backend's provider-native model-list payload.
Status Codes:
200: Selected backend response returned502: Backend connection or gateway failure503: No eligible healthy backend
Force-Refresh Models¶
Synchronously re-aggregate from all configured backends and replace the model-aggregation cache. If a backend cannot be discovered, its last-known models are retained while fresh results from successful backends remain authoritative. The returned list follows the same routable-models rule as GET /v1/models. The endpoint returns the same OpenAI-compatible model list shape as GET /v1/models so the caller can use the response immediately.
This endpoint is designed for interactive "Refresh" actions in desktop clients (e.g. the "Refresh models" button in backend.ai-go). It bypasses the stale-while-revalidate and soft-TTL windows, triggering an upstream fetch from every backend before responding.
Response:
{
"object": "list",
"data": [
{
"id": "gpt-5.6-sol",
"object": "model",
"created": 1677610602,
"owned_by": "openai-compatible"
}
]
}
Status Codes:
200: Refresh completed and the updated model list returned429 Too Many Requests: Force-refresh rate limit exceeded (3 requests per 5-second burst window, 12 per minute)
Rate Limiting:
The endpoint applies a tighter budget than the regular GET /v1/models because each call triggers an upstream fetch from every configured backend. The default limits are 3 requests per 5-second window and 12 per minute. Valid API keys receive separate buckets keyed by API key ID. Anonymous callers and requests with invalid bearer tokens share one global anonymous bucket, so spoofed Authorization, X-Forwarded-For, or X-Real-IP headers cannot bypass the force-refresh budget. Clients that exceed the limit receive 429 and should wait before retrying.
Cache policy:
The model-list cache currently uses internal fixed defaults: a 60-second hard TTL, an 80% soft TTL, stale-while-revalidate, request coalescing, and bounded empty-result backoff. These values and force-refresh availability are not fields in the public configuration schema.
Get Single Model¶
Retrieve information about a specific model, including its availability status and optional rich metadata.
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | Yes | Model identifier (e.g., "gpt-5.6-sol", "llama2:7b", "anthropic/claude-haiku-4.5"). A namespaced id containing / works either raw or percent-encoded: anthropic/claude-haiku-4.5 and anthropic%2Fclaude-haiku-4.5 return the same body. The exact raw segments extended, proxy, refresh, and query route to their own endpoints, not to this lookup. The reservation is matched on the raw path, so a percent-encoded spelling such as %65xtended is not reserved: it reaches this lookup as the id extended, under the same authorization and visibility checks as any other id. |
Response (Basic):
{
"id": "gpt-5.6-sol",
"object": "model",
"created": 1677610602,
"owned_by": "openai",
"available": true
}
Response (With Extended Metadata):
When model metadata is available in model-metadata.yaml, the response includes additional fields. For a model served by an OpenAI-compatible aggregator such as OpenRouter, limits (and therefore max_tokens) may be reported by the upstream catalog when configured metadata has none, and an upstream price replaces pricing from the shared model-metadata.yaml or built-in catalog unless the backend's own model_configs sets one; GET /v1/models/extended carries the same metadata plus the upstream's raw fields. See Upstream vendor and metadata:
{
"id": "gpt-5.6-sol",
"object": "model",
"created": 1704067200,
"owned_by": "openai",
"available": true,
"supported_methods": ["chat.completions"],
"features": ["chat", "vision", "code", "reasoning", "tool"],
"max_tokens": 128000,
"metadata": {
"display_name": "GPT-5.6 Sol",
"developer": "OpenAI",
"summary": "Frontier GPT-5.6 model for complex professional work.",
"knowledge_cutoff": "2026-02",
"relative_speed": 3,
"pricing": {
"input_tokens": 5.0,
"output_tokens": 30.0
},
"limits": {
"context_window": 1050000,
"max_output": 128000
}
}
}
Response Fields:
| Field | Type | Description |
|---|---|---|
id | string | Model identifier |
object | string | Object type (always "model") |
created | integer | Unix timestamp when model was created |
owned_by | string | Organization that owns/provides the model |
available | boolean | Whether the model can currently be used (true if at least one healthy backend provides it) |
supported_methods | array | (Optional) API methods this model supports (e.g., ["chat.completions"], ["images.generations"]) |
features | array | (Optional) Model capabilities (e.g., ["chat", "vision", "function_calling"]) |
max_tokens | integer | (Optional) Maximum output tokens for this model |
metadata | object | (Optional) Rich metadata object with detailed model information |
Metadata Object Fields:
| Field | Type | Description |
|---|---|---|
display_name | string | Human-readable display name for the model |
developer | string | Developer or organization that created the model |
summary | string | Brief summary describing the model's capabilities |
knowledge_cutoff | string | Knowledge cutoff date (e.g., "2025-01") |
relative_speed | integer | Relative speed indicator (1-5, where 1 is slowest and 5 is fastest) |
pricing | object | Pricing information in USD. See Pricing Object Fields |
limits | object | Model limits. context_window and max_output are in tokens, with 0 meaning "not applicable" (image models). Image models add optional keys: max_prompt_length (characters, not tokens), supported_sizes, max_n (max images per request), supported_qualities, supported_output_formats, supported_styles, and supports_streaming (boolean) |
artifact | object | (Optional) Operator-declared identity of the weights: digest (sha256: or blake3: plus 64 lowercase hex), registry_version, revision, source. See Artifact identity |
draft_model | object | (Optional) Operator-declared speculative-decoding draft: name and an optional artifact |
reasoning_efforts | array | (Optional) Accepted reasoning_effort levels in ascending order (subset of ["minimal", "low", "medium", "high", "xhigh", "max"]), from operator configuration, model metadata, or the built-in OpenAI catalog. Omitted when unknown |
rejects_reasoning_effort_none | boolean | (Optional) true when the model rejects reasoning_effort: "none". Omitted when false |
An image model's limits object carries the optional image keys alongside the token bounds. All of them are informational for API consumers: the router enforces image request limits per model family in code and does not read these values. dall-e-3 serves:
"limits": {
"context_window": 0,
"max_output": 0,
"max_prompt_length": 4000,
"supported_sizes": ["1024x1024", "1792x1024", "1024x1792"],
"max_n": 1,
"supported_qualities": ["standard", "hd"],
"supported_styles": ["vivid", "natural"]
}
Every optional limits key is omitted from the response when unset, so a token model's limits object serializes exactly as it did before these fields existed.
Artifact identity¶
artifact and draft_model appear only for a self-hosted model whose operator declared them in model_configs or the metadata layer, never for a model the built-in OpenAI catalog describes. Like every other key in metadata, they come from the first serving backend's entry, in configured backends order: a later backend's model_configs override is not visible here. GET /admin/models lists every serving backend's identity under artifacts, and each usage record carries the identity of the backend that served it. artifact.source is returned verbatim to every caller, so operators must declare a stable public locator there, never a presigned or token-bearing URL.
"metadata": {
"display_name": "Qwen 3.8 27B",
"artifact": {
"digest": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"registry_version": "v2.1-fp8",
"revision": "4c2e9a1",
"source": "hf://Qwen/Qwen3.8-27B-FP8"
},
"draft_model": {
"name": "qwen3.8-0.6b"
}
}
Pricing Object Fields¶
| Field | Type | Unit | Description |
|---|---|---|---|
input_tokens | number | USD per 1M tokens | Text input rate. Always present |
output_tokens | number | USD per 1M tokens | Output rate. Always present |
cached_input_discount | number | ratio 0.0-1.0 | (Optional) Discount applied to cached input tokens. A ratio, not a price |
image_input_tokens | number | USD per 1M tokens | (Optional) Image input rate, for models that bill image input separately from text input |
cached_image_input_tokens | number | USD per 1M tokens | (Optional) Cached image input rate. A price, unlike cached_input_discount |
per_image | object | USD per generated image | (Optional) Price of one generated image, keyed by size or by named tier |
long_context | object | USD per 1M tokens | (Optional) Whole-request input and output rates used when prompt tokens exceed threshold_tokens |
input_tokens and output_tokens are per 1,000,000 tokens, the unit vendors publish list prices in. This is not the smart_routing cost_per_1k_input_tokens unit, which is per 1000 tokens; divide by 1000 to convert.
long_context contains threshold_tokens, input_tokens, and output_tokens. A prompt equal to the threshold uses the base rates. A prompt above it uses both tier rates for the whole request. The field is metadata for API consumers; router-side smart-routing tier inference continues to use the base input rate.
"pricing": {
"input_tokens": 0.1,
"output_tokens": 0.5,
"long_context": {
"threshold_tokens": 100000,
"input_tokens": 0.5,
"output_tokens": 2.5
}
}
image_input_tokens and cached_image_input_tokens coexist with the text rates rather than replacing them. gpt-image-2 bills text input at input_tokens and image input at image_input_tokens:
"pricing": {
"input_tokens": 5.0,
"output_tokens": 30.0,
"cached_input_discount": 0.75,
"image_input_tokens": 8.0,
"cached_image_input_tokens": 2.0
}
per_image values are USD per generated image, a different unit from everything above. Each key is a size (1024x1024) or a named tier (ultra), and a key holds either a single price or a nested per-quality map. All three shapes are served:
"per_image": {"1024x1024": 0.02, "512x512": 0.018, "256x256": 0.016}
"per_image": {"fast": 0.02, "standard": 0.04, "ultra": 0.06}
"per_image": {
"1024x1024": {"standard": 0.04, "hd": 0.08},
"1792x1024": {"standard": 0.08, "hd": 0.12}
}
A model priced per image reports input_tokens: 0 and output_tokens: 0. Those zeros are literal catalog values, not missing data; the presence of per_image is the machine-readable signal that token pricing is not that model's axis. Clients should check for per_image before rendering a 0 token rate as a price.
Every optional field above is omitted from the response when unset, so a model that carries only token pricing serializes exactly as it did before these fields existed.
Supported Methods Mapping:
The supported_methods field is derived from model capabilities:
| Capability | API Method |
|---|---|
chat, vision, code, reasoning, audio, video, function_calling, tool | chat.completions |
embedding | embeddings |
image_generation | images.generations |
image_edit | images.edits |
image_variation | images.variations |
moderation | moderations |
Status Codes:
200: Model found and information returned404: Model does not exist in any configured backend
Features:
- OpenAI-Compatible: Response format matches OpenAI API with additional extension fields
- Health-Aware: The
availablefield reflects real-time backend health status - Privacy: Does not expose internal backend information
- Optional Metadata: Extended fields are present only when metadata is available
- Rich Metadata: Includes display name, developer, capabilities, and context-window limits to inform client-side model selection
Model Availability Algorithm:
The available field is determined by the following algorithm:
| Condition | Result |
|---|---|
| Health checker enabled + At least one backend providing the model is healthy | true |
| Health checker enabled + All backends are unhealthy | false |
| Health checker disabled + Model's backend list is non-empty | true |
| Health checker disabled + Model's backend list is empty | false |
The algorithm uses short-circuit evaluation: it returns true as soon as the first healthy backend is found, avoiding unnecessary health checks for remaining backends. This optimizes performance when backends are generally healthy.
Performance Optimization:
- Fast Path: If the model cache is valid, lookup is O(n) where n = number of models
- Slow Path: If cache is empty/expired, triggers one-time aggregation with singleflight protection to prevent cache stampede
- Stale-While-Revalidate: Cache serves stale data while refreshing in background
Example Request:
Example Response (Model Available with Full Metadata):
{
"id": "gpt-5.6-sol",
"object": "model",
"created": 1704067200,
"owned_by": "openai",
"available": true,
"supported_methods": ["chat.completions"],
"features": ["chat", "vision", "code", "reasoning", "tool"],
"max_tokens": 128000,
"metadata": {
"display_name": "GPT-5.6 Sol",
"developer": "OpenAI",
"summary": "Frontier GPT-5.6 model for complex professional work, with text and image input.",
"knowledge_cutoff": "2026-02",
"relative_speed": 3,
"pricing": {
"input_tokens": 5.0,
"output_tokens": 30.0
},
"limits": {
"context_window": 1050000,
"max_output": 128000
}
}
}
Example Response (Model Without Extended Metadata):
For models without metadata in model-metadata.yaml, only the basic fields are returned:
{
"id": "custom-model",
"object": "model",
"created": 1677610602,
"owned_by": "local",
"available": true
}
Example Response (Model Exists but Unavailable):
{
"id": "gpt-5.6-sol",
"object": "model",
"created": 1704067200,
"owned_by": "openai",
"available": false,
"supported_methods": ["chat.completions"],
"features": ["chat", "vision", "code", "reasoning", "tool"],
"max_tokens": 128000,
"metadata": {
"display_name": "GPT-5.6 Sol",
"developer": "OpenAI"
}
}
Chat Completions¶
Generate chat completions using the OpenAI Chat API format.
Request Body:
{
"model": "gpt-5.6-luna",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "Explain quantum computing in simple terms."
}
],
"temperature": 0.7,
"max_tokens": 150,
"top_p": 1.0,
"frequency_penalty": 0.0,
"presence_penalty": 0.0,
"stream": false,
"stop": null,
"logit_bias": {},
"user": "user123"
}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | Yes | Model identifier (must be available on at least one healthy backend). A missing, null, non-string, or blank value answers 400 missing_model (param: "model"); POST /v1/completions applies the same check |
messages | array | Yes | Array of message objects with role and content |
temperature | number | No | Sampling temperature (0.0 to 2.0, default: 1.0) |
max_tokens | integer | No | Maximum tokens to generate. This and max_completion_tokens must be non-negative integers or null; any other value answers 400 invalid_request_error naming the field (code invalid_type, or integer_below_min_value for a negative number), on POST /v1/completions as well. When omitted, the router forwards the request without one and the backend's default applies; set a request_params default or limit to give every request a router-side budget. A budget above the routed model's declared limits.max_output or limits.context_window answers 400 invalid_max_tokens before dispatch when every serving backend declares such a limit; a backend's model_configs limits take precedence over model-metadata.yaml |
top_p | number | No | Nucleus sampling parameter (0.0 to 1.0) |
frequency_penalty | number | No | Frequency penalty (-2.0 to 2.0) |
presence_penalty | number | No | Presence penalty (-2.0 to 2.0) |
stream | boolean | No | Enable streaming response (default: false) |
stop | string/array | No | Stop sequences |
logit_bias | object | No | Token logit bias |
logprobs / top_logprobs | boolean / integer | No | Token log probabilities. Forwarded unchanged on a pass-through backend; a conversion strategy (Anthropic Messages, Bedrock Converse, or the Chat-to-Responses bridge for a responses_only model) refuses the request with 400 unsupported_request_parameter, since it cannot carry logprobs |
user | string | No | User identifier for tracking |
disable_fallbacks | boolean | No | true skips every cross-model fallback hop for this request, before the first byte and mid-stream; see Per-request opt-out. Never forwarded to a backend |
models | array of strings | No | Per-request fallback list (OpenRouter form, at most 16 entries): when the primary fails, the router tries the next model in the list through the configured fallback machinery, and the response model names the model that answered. With model absent, the first entry is the primary. Applies only when fallback.enabled is on; every entry must be in the key's model allow-list, and disable_fallbacks: true takes precedence over the list. An unparseable entry is 400. Never forwarded to a backend |
mock_response | string/object | No | Testing and debugging parameter. On a non-streaming request the router answers from this value without contacting a backend or writing a usage record: a string becomes the assistant content of one choice with finish_reason: "stop", and an object is returned verbatim. Authentication, request validation, and the key's model allow-list still apply; any other type answers 400 invalid_type. A streaming request is dispatched normally. The field is never forwarded to a backend |
preset | string | No | Names a configured request_presets slug; see Request Presets. The preset fills a missing model and any sampling field the request left unset, while explicit request fields win. An unknown slug answers 400 unknown_request_preset, a non-string value answers 400 invalid_type, and null is ignored. The field is never forwarded to a backend |
provider | object | No | OpenRouter-style per-request routing preferences, interpreted only when proxy.client_provider_preferences is on; see Client Provider Preferences. With the switch off the object is forwarded unchanged |
reasoning_effort | string | No | Reasoning effort level for reasoning-capable models. Valid values vary by backend and model (see below). OpenAI values are validated exactly and are never silently downgraded. auto is reserved for adaptive thinking on Anthropic models. |
reasoning | object | No | Alternative nested format: {"effort": "high"}. Automatically normalized to reasoning_effort. |
Client Provider Preferences¶
With proxy.client_provider_preferences: true, a client may send OpenRouter's provider object in the request body to narrow and order the backends this request may use. The object is opt-in: with the switch off (the default) it is forwarded unchanged, exactly as before, so an aggregator backend behind a generic backend still receives it.
Supported fields:
| Field | Type | Behavior |
|---|---|---|
order | array of backend names | Soft preference: listed names rank first, in list order; every other candidate follows, ranked by sort when that is also stated |
only | array of backend names | Hard allow-list, intersected with the request's existing candidate set |
ignore | array of backend names | Hard block-list, subtracted from the candidate set after only |
sort | price, throughput, or latency | Ranks survivors by live telemetry: ascending estimated request cost, descending fresh engine throughput, or ascending recorded average response time |
max_price | object with optional prompt and completion | Hard ceiling in USD per 1M tokens, the same unit as model metadata pricing. Each absent key is unconstrained |
require_parameters | boolean | Only false is accepted |
data_collection | "allow" or "deny" | Only "allow" is accepted |
Security and precedence:
- The candidate set is built in this order: backend visibility (
internal,enabled), the key'sallowed_modelsandallowed_backends, Hub and policy narrowing, and finally the client'sproviderfilters. A preference can only narrow or reorder that set; it can never widen an allow-list or make an invisible or disabled backend reachable. only,ignore, andordernaming a backend outside the candidate set has no widening effect. A filter that empties a non-empty candidate set, such as anonlywhose intersection is empty, answers400 invalid_request_errornaming the field (provider.only,provider.ignore, orprovider.max_price) and dispatches nothing. A set that was already empty before the filters keeps the ordinary503.x-backendremains a first-attempt pin and wins overprovider. Retries after that first attempt apply the provider object again.- A request that carries a
providerobject never joins another request's in-flight dispatch. Deduplication keys on the dispatched body, and the router removesproviderfrom it, so two requests differing only in their preferences would otherwise share one dispatch and one could be answered from a backend it excluded. The preference therefore costs one dispatch of its own when it is present. - With the switch on, the router consumes and removes the object, so no backend receives it. An operator can still send a
providerobject to a backend that speaks it throughbackends[].request_extensions.body.overrides.provider, which is applied at dispatch after the client key is removed.request_extensions.body.defaults.providercannot restore the removed client object, because defaults only fill unset keys. Static operator configuration therefore wins over the client object, and the client object never reaches an upstream verbatim. - With the switch off, nothing changes: the object passes through as it does today.
Refused fields: An unknown field is a 400 naming provider.<field>, including OpenRouter fields this router does not implement (allow_fallbacks, quantizations, and similar). A silently ignored routing preference is worse than a rejected one. Two known OpenRouter fields are refused by name for the same reason: the router cannot prove that every request parameter survives every candidate's conversion, so it does not accept require_parameters: true, and it has no per-backend data-collection metadata, so it does not accept data_collection: "deny".
Validation limits: The serialized object is limited to 16 KiB. Each name list is limited to 16 entries, and each name is 1 to 128 bytes with no leading or trailing whitespace and no control characters. Duplicates are collapsed preserving first occurrence. provider must be a JSON object. Every refusal to parse the object answers 400 invalid_request_error with param: "provider". A filter the candidate set cannot satisfy answers the same status and type, with the field named in the message.
Ranking fallback: sort and order rank only on stated data. A candidate with no fresh engine snapshot or no recorded latency joins one trailing rank group, and a fleet with no telemetry at all produces a single group. In that case selection is exactly the configured selection_strategy: a preference never reorders on absent data. max_price is stricter: an unpriced candidate is excluded, because its price cannot be verified.
Candidates equal on every stated key form one rank group: equal on their order position (or absence from the list) and, when stated, on the sort metric. The configured selection_strategy chooses within that group, so equal prices or equal order ranks still rotate instead of pinning the first candidate.
The Anthropic Messages ingress refuses a top-level provider key with 400 while the switch is on, rather than silently dropping it, because that ingress does not apply client routing preferences.
Input context limit¶
The router rejects a chat input estimated to fill or exceed the routed model's context_window before backend dispatch, for buffered and streaming requests. The response is 400 invalid_request_error with code context_length_exceeded and a message containing the window and estimated input tokens. This also applies when no output-token budget is supplied.
By default the rejection does not trigger a larger-model fallback. With fallback.fallback_policy.trigger_conditions.context_window_exceeded: true, the router instead hops before dispatch to the first configured chain target whose own window fits the input, and answers the original rejection only when no target fits. See Context-window fallback.
The estimate includes messages and tool schemas, uses the same typical estimate as fallback admission, and runs after model routing and optional context compression. It is an approximation, not a model-specific tokenizer count. Missing or zero context limits allow the request through.
For each authorized serving backend, configured limits override cached engine reports. The smallest known positive window is enforced across these backends, as in fallback admission, even if another backend has a larger or unknown window. This is more conservative than output-budget validation. If metadata understates a backend's real window, correct the model metadata to restore requests that previously succeeded.
Valid reasoning_effort values by backend:
| Backend | Effort Levels | Token Conversion | Notes |
|---|---|---|---|
| OpenAI O-series (o1, o3, o4-mini) | low, medium, high | Native | Exact model validation |
| OpenAI GPT-5 (including mini/nano) | minimal, low, medium, high | Native | Original GPT-5 family only |
| OpenAI GPT-5.1 | none, low, medium, high | Native | minimal is not accepted |
| OpenAI GPT-5.2 through GPT-5.5 | none, low, medium, high, xhigh | Native | Exact model validation |
| OpenAI GPT-5.6 | none, low, medium, high, xhigh, max | Native | max is GPT-5.6-only |
| OpenAI GPT-5 Pro | high | Native via Responses bridge | Responses API only |
| OpenAI GPT-5.⅖.⅘.5 Pro | medium, high, xhigh | Native via Responses bridge | Responses API only |
| Anthropic Claude (Opus 4.7, Opus 4.6, Sonnet 4.6, etc.) | none, minimal, auto, low, medium, high, xhigh* | → budget_tokens or adaptive + output_config.effort | *xhigh maps to max on Opus 4.6/4.7 and downgrades to high elsewhere; Opus 4.7 drops temperature/top_p/top_k |
| Gemini (3.x, 2.5-pro, 2.x-flash) | none*, auto**, minimal, low, medium, high | Native | *none only for Flash; **auto downgraded to medium |
| Generic/llama.cpp | Any | Pass-through | Backend handles validation |
For detailed conversion tables and backend-specific behavior, see Reasoning Effort Architecture.
For OpenAI models, the router applies the same exact model-specific validation to streaming and non-streaming requests. Unsupported values return 400 invalid_request_error; the router never rewrites auto, xhigh, or max to a different effort. Other providers retain their provider-specific conversion behavior shown above.
Response (Non-streaming):
{
"id": "chatcmpl-123456789",
"object": "chat.completion",
"created": 1677652288,
"model": "gpt-5.6-luna",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Quantum computing uses quantum mechanical phenomena like superposition and entanglement to process information..."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 25,
"completion_tokens": 150,
"total_tokens": 175
}
}
Response (Streaming): When stream: true, the response uses Server-Sent Events (SSE):
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-5.6-luna","choices":[{"delta":{"role":"assistant","content":""},"index":0,"finish_reason":null}]}
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-5.6-luna","choices":[{"delta":{"content":"Quantum"},"index":0,"finish_reason":null}]}
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-5.6-luna","choices":[{"delta":{"content":" computing"},"index":0,"finish_reason":null}]}
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-5.6-luna","choices":[{"delta":{},"index":0,"finish_reason":"stop"}]}
data: [DONE]
Status Codes:
200: Completion generated successfully400: Invalid request format or parameters404: Model not found on any healthy backend502: Backend connection error504: Request timeout503: All backends unhealthy
Features:
- Model-Based Routing: Automatically routes to backends serving the requested model
- Load Balancing: Distributes load across healthy backends
- Streaming Support: Real-time response streaming via SSE
- Error Recovery: Automatic retry on transient failures
- Request Deduplication: Prevents duplicate processing of identical requests
- Reasoning Parameter Normalization: Automatically normalizes nested
reasoningformat to flatreasoning_effortformat; removes reasoning parameters for models that don't support them - Transparent Responses-API routing: Models marked
responses_only: true(such asgpt-5-pro,gpt-5.2-pro,gpt-5.4-pro, andgpt-5.5-pro) are transparently dispatched to the upstream/v1/responsesendpoint and translated back into a strict-modechat.completion(orchat.completion.chunkfor streaming) envelope:- Preserves
reasoning_content,refusal(finish_reason: "content_filter"),incomplete_detailstruncation (finish_reason: "length"or"content_filter"), and upstreamfailedorcancellederror envelopes. - Supported on OpenAI and Azure OpenAI backends only; pairing a
responses_onlymodel with another backend type returns400 invalid_request_errorbefore dispatch. See Responses-API-only Models.
- Preserves
- Gemini Native API Routing: When dispatching to a Gemini backend, requests using native-exclusive features (
tools: [{"google_search": {}}],tools: [{"code_execution": {}}],modalities: ["text", "image"],cachedContent, multi-turn assistant image history, orgemini_context_cache.prefer_native_api: true) automatically route to Gemini's nativegenerateContent/streamGenerateContentendpoint, transforming responses into OpenAI-compatible format withreasoning_content,code_execution,annotations,grounding_metadata, and inlineimages.
Gemini Native Exclusive Feature Examples (/v1/chat/completions)¶
1. Google Search Grounding & Code Execution:
{
"model": "gemini-2.5-flash",
"messages": [
{
"role": "user",
"content": "Search Google for the latest stable Rust release and use Python to compute the days since its release."
}
],
"tools": [
{ "type": "google_search" },
{ "type": "code_execution" }
]
}
The normalized chat.completion response includes annotations (type: "url_citation"), grounding_metadata, and structured code_execution items alongside the formatted content:
{
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "```python\nprint(42)\n```\n\n```output\n42\n```\nThe latest release is...",
"annotations": [
{
"type": "url_citation",
"url_citation": {
"url": "https://...",
"title": "rust-lang.org",
"start_index": 0,
"end_index": 24
}
}
],
"grounding_metadata": {
"webSearchQueries": ["latest stable Rust release"],
"groundingChunks": [],
"groundingSupports": []
},
"code_execution": [
{ "type": "executable_code", "language": "PYTHON", "code": "print(42)\n" },
{ "type": "code_execution_result", "outcome": "OUTCOME_OK", "output": "42\n" }
]
},
"finish_reason": "stop"
}
]
}
2. Multi-turn Conversational Image Generation & Editing:
Pass modalities: ["text", "image"] with a Gemini image model (gemini-2.5-flash-image, nano-banana, nano-banana-2, nano-banana-pro) and include the previous turn's assistant.images array to iteratively edit an image across turns:
{
"model": "gemini-2.5-flash-image",
"modalities": ["text", "image"],
"messages": [
{ "role": "user", "content": "Generate a flat icon of a blue circle." },
{
"role": "assistant",
"content": "",
"images": [
{
"type": "image_url",
"image_url": { "url": "data:image/png;base64,iVBORw0KGgo..." }
}
]
},
{ "role": "user", "content": "Change the circle color from blue to red and add a yellow star." }
]
}
Tool conversion warnings¶
Successful responses include x-continuum-dropped-tools when the router omits tool declarations while converting the request for the serving backend. This applies to Chat Completions, Responses, and Anthropic Messages bridges, including streaming responses. Requests retain their existing permissive behavior; inspect this header if your application requires every declared tool.
The value is a sorted, comma-separated list of distinct type names. Known names are web_search, web_search_preview, file_search, code_interpreter, computer_use_preview, image_generation, mcp, local_shell, and custom; all other types are grouped as other. The header is exposed through CORS. For example:
Only tools actually omitted are listed. Gemini search and code execution tools translated to native built-ins are not omissions. A failed upstream attempt contributes no warning to a successful fallback response. For streams, the header describes conversion completed during the serving handshake; subsequent OpenAI-wire relay hops preserve the tool declarations.
Router response caching is bypassed for requests containing non-function tools, including native tool declarations, because cached bodies do not preserve conversion warnings. This affects exact, prefix, and semantic response caches; requests containing only function tools remain eligible. Provider prompt/context caches are unchanged.
Responses API¶
Generate responses using OpenAI's Responses API format. This endpoint provides an alternative interface to Chat Completions, internally converting requests to the Chat Completions format for backend processing.
A streamed response that the client cancels, that times out, that a guardrail blocks, or that fails mid-stream is charged for the tokens generated up to the cut: one usage record with that exit's outcome, from the engine's usage when it arrived and the router's delta count otherwise. An Anthropic or Gemini upstream cut after reporting its input but before its output count (Anthropic's message_delta, Gemini's terminal candidatesTokenCount) is charged that input beside the router's delta count.
Request Body:
{
"model": "gpt-5.6-sol",
"input": "Explain quantum computing in simple terms.",
"instructions": "You are a helpful assistant that explains complex topics simply.",
"max_output_tokens": 1000,
"temperature": 0.7,
"reasoning": {
"effort": "high"
}
}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | Yes | Model identifier (must be available on at least one healthy backend) |
input | string/array | Yes | The input text or array of input items |
instructions | string | No | System instructions for the model (converted to system message) |
max_output_tokens | integer | No | Maximum tokens to generate |
temperature | number | No | Sampling temperature (0.0 to 2.0) |
top_p | number | No | Nucleus sampling parameter (0.0 to 1.0) |
stream | boolean | No | Enable streaming response (default: false) |
store | boolean | No | Whether to persist the response in session storage (default: true). Set to false for ephemeral requests that should not be stored (used by Codex) |
include_reasoning | boolean | No | Include reasoning content in the response |
reasoning | object | No | Reasoning configuration with nested format (see below) |
tools | array | No | List of tools available for the model (flat format) |
tool_choice | string/object | No | Controls tool usage |
previous_response_id | string | No | Reference to a previous response for multi-turn conversations |
conversation | string/object | No | Server-side conversation the response belongs to. Forwarded unchanged on the pass-through strategy; on a conversion strategy the request is refused with 400 unsupported_request_parameter, since the router cannot read the conversation's history |
Fields the router does not model (conversation, background, service_tier, parallel_tool_calls, and fields the API adds later) are forwarded to a pass-through backend exactly as sent. The conversion strategies build their request from the fields above and drop the rest, and a request carrying any such field is never served from the response cache. Input items of a type the router does not model (for example mcp_approval_response or computer_call_output) are likewise kept verbatim for a pass-through backend; a conversion strategy refuses them with 400 unsupported_request_parameter instead of dropping them.
Tool types beyond the built-in list keep the same rule, which is what lets Codex CLI reach a backend through this endpoint. A pass-through backend receives each tool the responses_tools policy allows, with the same JSON content the client sent; object key order is not preserved. A conversion strategy handles them as follows:
Tool type | Default policy | Conversion strategy |
|---|---|---|
custom (a freeform tool with a grammar format) | Always allowed | 400 unsupported_request_parameter naming tools[custom] |
local_shell | Always allowed | 400 unsupported_request_parameter naming tools[local_shell] |
namespace with only function members | Always allowed | Each member becomes a flat function tool named {namespace}__{name}, and a replayed function_call that carries namespace is renamed to match. A flattened name over 64 characters is sent as its first 55 bytes (cut back to a character boundary), _, and the first 8 hex characters of the SHA-256 of the full name |
namespace with any other member | Each member is judged on its own | 400 unsupported_request_parameter naming the member and the namespace |
Any other type (for example mcp or image_generation) | Refused unless listed in allow | 400 unsupported_request_parameter naming tools[<type>] |
The policy runs before backend selection on both flows, so a type it refuses gets 400 unsupported_request_parameter naming tools[<type>] on every backend, and no upstream request is made. tool_search with execution: "client" is always allowed, and with any other execution it follows the "any other type" row.
Two flattened names that collide are also refused with 400 unsupported_request_parameter. A tool of a known type with a missing required field, such as a function tool without name, fails with 422.
A conversion strategy also refuses a request whose tools flatten to more than 4096 entries, counting each namespace member as one entry, with 400 unsupported_request_parameter. Flattening cost grows with that count, and a request at the 10 MiB body limit costs far more CPU than one request may spend. A pass-through backend is unaffected: it sends the tools as declared and flattens nothing.
A converted reply, streaming or not, reports a call to a flattened tool under the original name with its namespace, so a client that dispatches on that pair can route it.
Modeled shapes also keep fields the router does not model. A pass-through backend receives them unchanged, and a conversion strategy ignores them:
- Hosted tools:
web_search(for exampleexternal_web_accessorfilters),file_search(vector_store_ids),code_interpreter(container),computer_use, andprogrammatic_tool_calling. - Replayed items:
message(id,status, and any other field, also on an item withroleandcontentand notype; with atype,phase,agent,caller, or a non-standardidorstatusmakes it a native-only item that a conversion strategy refuses),function_call(id,status,encrypted_content,encrypted_function_args) andfunction_call_output(id,status). These fields count toward the input size limit.
A backend may reject a replayed id it does not know, for example under store: false, exactly as it would if called directly.
tool_choice shapes:
tool_choice | Pass-through strategy | Conversion strategy |
|---|---|---|
auto, none, required | Forwarded | Mapped to the backend's equivalent |
Object with type: "function" and name, without namespace | Forwarded with all its fields | Sent under the same name if a top-level function has it, or under the flattened name if exactly one namespace declares it. A name no tool declares is sent unchanged. 400 unsupported_request_parameter when two or more namespaces declare it |
Object with type: "function", name, and namespace | Forwarded with all its fields | Sent under the flattened name if that namespace declares the function member; otherwise 400 unsupported_request_parameter naming tool_choice namespace member |
Object with a type other than function and no name (for example allowed_tools or web_search) | Forwarded with the same JSON content | 400 unsupported_request_parameter naming tool_choice[<type>] |
Object with a type other than function and a name (for example mcp) | Forwarded with all its fields | 400 unsupported_request_parameter naming tool_choice[<type>] |
Any other value, such as an unknown string, {"type": "function"} without name, or a named object whose namespace is not a string, fails with 422. On the Gemini conversion, tool_choice becomes tool_config only when the request declares a function tool. A request with only web_search or code_interpreter has no tool_config, so a forced choice has no effect there. A request that combines a function tool with web_search or code_interpreter needs a model with server-side tool circulation: gemini-2.5-flash rejects it with a 400 unless tool_choice is none. Tools Gemini ran itself come back as a reasoning item with an opaque encrypted_content, and a function_call can carry encrypted_content too. Later Gemini models answer a replayed call without it with a 400, so a client that manages its own history must send both fields back unchanged.
The reasoning item's encrypted_content is signed by the router instance that issued it; a value whose signature does not verify (after a restart, or on another instance) is dropped instead of replayed. Its server-side tool parts are inspected on output, and a masking verdict removes the item.
On /v1/chat/completions, a Gemini native request follows these tool rules:
- When
toolConfigis sent: only when function declarations are reachable, either inline or through acachedContentthe client manages. A request with only built-in tools (web_search_options,code_execution, and similar) sends none, so arequiredor named choice is validated but not enforced. Each such request logs a warning and incrementstool_choice_unenforced_total{reason="forced_without_functions"}. - Functions next to a built-in:
toolConfigcarriesinclude_server_side_tool_invocations: true, including when the client sends notool_choice. A model that does not support the mix still rejects it with a400, asgemini-2.5-flashdoes. tool_choice: "none": the router removes built-in tools from the body, including one derived fromweb_search_options, so the request runs with no tools. Each removal incrementstool_choice_unenforced_total{reason="none_with_built_ins"}.
A Gemini 4xx on either surface keeps Google's sanitized message in the client-facing error, for example Invalid tool. A 5xx body is never forwarded, and the client sees the status-derived message.
Tool Definition Format:
The Responses API uses a flat tool format where function properties are at the same level as the type field. This differs from the Chat Completions API which uses a nested function object.
{
"type": "function",
"name": "get_weather",
"description": "Get weather for a location",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string", "description": "City name"}
},
"required": ["location"]
},
"strict": true
}
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Must be "function" |
name | string | Yes | Name of the function |
description | string | No | Description of what the function does |
parameters | object | No | JSON Schema for function parameters |
strict | boolean | No | Enable strict parameter validation |
Other supported tool types include code_interpreter, file_search, web_search, and computer_use. When targeting a Gemini backend, web_search automatically maps to native google_search grounding (surfacing url_citation annotations and grounding_metadata), and code_interpreter maps to native code_execution (surfacing formatted code blocks and execution results).
On a Gemini backend, a turn that ends on a tool-related or malformed finish reason such as MALFORMED_FUNCTION_CALL or TOO_MANY_TOOL_CALLS reports status: failed with the lowercased reason as error.code, and a reply with no candidates and no prompt block reports failed with empty_response, in streaming and non-streaming responses alike. A failed reply is HTTP 200 but served no reply: it is returned as-is, never cached, never stored for a previous_response_id continuation, and never used for the usage-quality judgment, and the request records upstream_error while still charging the tokens the provider reported.
Multi-Modal Input Types:
When input is an array of items, each item can be a message containing multi-modal content parts. The following content part types are supported:
| Type | Description |
|---|---|
text | Plain text content |
input_text | Text content for user messages (alternative format for Responses API) |
output_text | Text content for assistant messages; differentiates assistant-generated content from user input (input_text) |
input_file | File content (PDF, images, or other files) |
input_image | Image content |
image_url | Image from URL or base64 data |
Message Roles:
Each message item carries one of five roles. The router accepts all five for /v1/responses and translates them per backend.
| Role | Purpose | Per-backend mapping |
|---|---|---|
developer | Application-developer instructions for OpenAI reasoning models (GPT-5.x, o-series). Highest priority in the chain of command and replaces system for those models. | OpenAI: passthrough as developer. Anthropic: merged into the system parameter (concatenated with any system text using \n\n). Gemini: merged into system_instruction. Ollama / vLLM / LocalAI / LM Studio: mapped to system. |
system | Application instructions. | OpenAI / Ollama / vLLM / LocalAI / LM Studio: passthrough as system. Anthropic: extracted into the system parameter. Gemini: extracted into system_instruction. |
user | End-user input. | Mapped to the target backend's user turn. |
assistant | Prior model output. | Mapped to the target backend's assistant (or Gemini model) turn. |
tool | Tool-call result returned to the model. | Mapped to the target backend's tool-result block or message. |
The router preserves arrival order when multiple developer and system messages are present. When the target backend (Anthropic, Gemini) only accepts a single instruction slot, the router concatenates them with a blank-line separator so no instruction text is silently dropped.
Item References:
An input item of the form {"type": "item_reference", "id": "item_..."} points at an output item from a previously stored response. This is how the OpenAI and Vercel AI SDKs submit multi-step tool round-trips by default: instead of echoing the full function-call item back, step 2 references the item produced in step 1.
- On the OpenAI / Azure OpenAI passthrough path, references are forwarded unchanged and the upstream resolves them.
- On the converted paths (Anthropic, Gemini, and chat-completions-backed routes), the router resolves each reference from its session store before dispatch, rewriting it to the inline
function_call,message, orfunction_call_outputitem it points to. References are de-duplicated bycall_id(first wins). - Streamed responses participate as well: the accumulated response is stored when the stream completes, so a follow-up request can reference items produced by a streaming step.
- An unresolvable reference returns HTTP 400 naming the id, and a request may carry at most 256 references.
Input File Format:
The input_file content part supports three input methods:
{
"type": "input_file",
"filename": "document.pdf",
"file_data": "data:application/pdf;base64,JVBERi0xLjQ..."
}
| Field | Type | Description |
|---|---|---|
filename | string | Optional filename for the file |
file_data | string | Base64 data URL (e.g., data:application/pdf;base64,...) |
file_url | string | External URL to the file (validated for SSRF) |
file_id | string | Reference to a file uploaded via Files API |
Input Image Format:
| Field | Type | Description |
|---|---|---|
image_url | string | Image URL or base64 data URL |
file_id | string | Reference to an image file uploaded via Files API |
detail | string | Image detail level: low, high, or auto (default) |
Exactly one of image_url or file_id must be present. A part with neither is rejected with 400. When file_id is provided, the file is resolved to an inline base64 data URL before the request reaches the backend; file ownership is verified and a 10MB size limit applies, matching the input_file + file_id path.
Multi-Modal Request Example:
curl -X POST http://localhost:8080/v1/responses \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.6-sol",
"input": [
{
"type": "message",
"role": "user",
"content": [
{"type": "input_text", "text": "What does this document say?"},
{"type": "input_file", "filename": "report.pdf", "file_data": "data:application/pdf;base64,..."}
]
}
]
}'
Security Notes:
- External URLs in
file_urlare validated to prevent SSRF attacks - Private IP addresses and localhost URLs are rejected
- Only HTTPS URLs are recommended for external files
- The
file_idfield references files uploaded via the Files API. Files are resolved and converted to base64 before sending to backends. File ownership is verified and a 10MB size limit applies for file injection
Reasoning Parameter:
The reasoning parameter controls the reasoning effort level for reasoning-capable models. It uses a nested format:
Valid effort values:
| Value | Description |
|---|---|
none | Disable reasoning on models that support it |
minimal | Minimal reasoning on the original GPT-5 family and supported non-OpenAI models |
auto | Adaptive thinking (Anthropic) |
low | Minimal reasoning effort, faster responses |
medium | Balanced reasoning effort |
high | Maximum standard reasoning effort |
xhigh | Extended reasoning on supported model families |
max | Maximum reasoning on GPT-5.6 |
The router automatically converts this nested format to the flat reasoning_effort format used by Chat Completions backends. Invalid effort levels are rejected with a 400 Bad Request error.
Example Request with Reasoning:
curl -X POST http://localhost:8080/v1/responses \
-H "Content-Type: application/json" \
-d '{
"model": "o1",
"input": "Solve this complex mathematical problem step by step.",
"reasoning": {
"effort": "high"
}
}'
Response:
{
"id": "resp_abc123",
"object": "response",
"created_at": 1699000000,
"model": "o1",
"output": [
{
"type": "message",
"id": "msg_001",
"role": "assistant",
"content": [
{
"type": "output_text",
"text": "Let me solve this step by step..."
}
]
}
],
"usage": {
"input_tokens": 25,
"output_tokens": 150
},
"status": "completed"
}
Status Codes:
200: Response generated successfully400: Invalid request format, parameters, or invalid reasoning effort value404: Model not found on any healthy backend502: Backend connection error504: Request timeout503: All backends unhealthy
Features:
- Smart Routing: Routes requests based on backend capabilities - native pass-through for OpenAI/Azure and for backends configured with
responses_api: native, automatic conversion for others - Native Pass-through: For OpenAI and Azure OpenAI backends, and any OpenAI-wire backend configured with
responses_api: native, requests are forwarded directly to the backend's/v1/responsesendpoint, preserving all native features - Automatic Conversion: For other backends (Anthropic, Gemini, vLLM, Ollama, etc.), converts Responses API format to their native format
- Reasoning Support: Full support for reasoning parameter with type-safe validation
- Multi-Backend Support: Works with OpenAI, Anthropic, Gemini, Ollama, vLLM, and other backends
- Streaming Support: Real-time response streaming via SSE when
stream: true - Session Management: Supports multi-turn conversations via
previous_response_id
Routing Strategy:
The router automatically determines the best strategy for each backend:
| Backend Type | Strategy | Description |
|---|---|---|
| OpenAI | Pass-through | Native Responses API support - requests forwarded directly |
| Azure OpenAI | Pass-through | Native Responses API support - requests forwarded directly |
| Anthropic | Native Convert | Converted to native Anthropic Messages API format with full PDF/image support |
| Gemini | Convert | Converted to Gemini generateContent API format |
| vLLM | Convert | Converted to Chat Completions format |
| SGLang | Convert | Converted to Chat Completions format |
| Ollama | Convert | Converted to Chat Completions format |
| LlamaCpp | Convert | Converted to Chat Completions format |
| Generic | Convert | Converted to Chat Completions format |
Any OpenAI-wire type with responses_api: native | Pass-through | Forwarded unchanged to the backend's own /v1/responses; see below |
The table is each type's default. A self-hosted server that implements the Responses API itself, such as an agent runtime that keeps its own conversation state or SGLang's /v1/responses, can be served unchanged by setting responses_api: native on its backend (see Backends: Native Responses API). The option is accepted on every type that speaks the OpenAI wire format (openai, azure, generic, continuumrouter, vllm, sglang, tensorrt-llm, ollama, llamacpp, mlxcel, lmstudio) and refused at load on anthropic, bedrock, and gemini. It changes only how /v1/responses (and /v1/responses/compact) reach the backend: model listing, owned_by, health checks, and every other endpoint keep the type's behavior.
On the pass-through strategy the backend receives the request as the client sent it, including previous_response_id, conversation, and input items the router does not model (for example mcp_approval_response), and its output reaches the client unchanged on both the streaming and the non-streaming path, including output items the router does not model (for example mcp_approval_request). The responses_tools.deny_resource_fields list can refuse the previous_response_id, conversation, and prompt fields, item_reference input items, and, through input_item_resource_ids, any input item that carries a resource ID such as a file_id or a container_id; these entries are off by default.
Three exceptions keep governance in force. background: true is forwarded only on the pass-through strategy (see Background responses); a conversion strategy drops it and answers synchronously. While the guardrail policy applies to the request's model and key, a reusable prompt is treated as uninspectable input (enforce mode refuses it, monitor mode records it), since its variables are not read by the input stage. While request_params or a hub tier limit governs top_k, min_p, or max_tokens, the unmodeled fields top_k, min_p, max_tokens, and max_completion_tokens are refused with 400 rather than forwarded past the bound.
Pass-through Benefits:
When using OpenAI or Azure OpenAI backends with pass-through mode:
- Native PDF file support (Chat Completions only supports images)
- Preserved reasoning state between turns for better performance
- Access to built-in tools (web_search, file_search, etc.)
- Better cache utilization (40-80% improvement per OpenAI documentation)
- Full compatibility with the latest OpenAI Responses API features
Anthropic Native Conversion Benefits:
When using Anthropic (Claude) backends with native conversion:
- Native PDF file support via Anthropic's document understanding
- Image file support with automatic format detection
- Extended thinking support for Claude 3+ models
- SSRF protection for external file URLs
- Media type whitelisting for security
Guardrails: When guardrails are configured, this endpoint runs the input stage on every request, streaming or not, before the response cache, backend selection, and any fallback attempt, and the output stage on every non-streaming reply before a store: true session is written. A blocked request is answered with a response object whose status is incomplete and whose incomplete_details.reason is content_filter (HTTP 200), or with the OpenAI content_filter error envelope (HTTP 400) under block_behavior: error, before any SSE event is sent. On the conversion strategies a previous_response_id or item_reference continuation is checked again over the rebuilt history, under the dispatched model's policy. A block is never cached and never triggers a fallback hop. See Guardrails: Coverage by route.
Retrieve, Delete, or Cancel a Stored Response¶
Responses created with store: true can be retrieved, deleted, or cancelled by ID:
- Storage and routing:
- Conversion strategies: Served from the router's in-memory session store (up to 10,000 sessions per router instance, 1-hour TTL). Multiple router instances embedded in the same process (such as via
serve_embedded) maintain separate stores. - Pass-through backends: Stored by the upstream backend itself; the router forwards the request to the backend that created the response (see Pass-through Responses).
- Conversion strategies: Served from the router's in-memory session store (up to 10,000 sessions per router instance, 1-hour TTL). Multiple router instances embedded in the same process (such as via
- Ownership and scoping: Sessions are scoped to the authenticated API-key principal (
api_keys[].user_id) that created the response.Authorization: Bearer <key>andX-API-Key: <key>share the same identity namespace. Missing, expired, unauthenticated, or cross-principal requests return404.
Migration from X-User-ID Scoping¶
X-User-ID is not an authorization boundary for the Responses API. The router ignores it for stored response access, previous_response_id context reconstruction, item-reference resolution, and local Files API ownership checks, and it is not forwarded to backends. Deployments that previously multiplexed multiple end users behind one shared router API key by setting X-User-ID must migrate to one router API key per isolated user or tenant, or place their own trusted service in front of the router and keep per-user state outside the router's stored Responses session and Files API ownership controls.
GET /v1/responses/{response_id}
DELETE /v1/responses/{response_id}
POST /v1/responses/{response_id}/cancel
A successful GET returns the stored response object. A successful DELETE returns:
Pass-through Responses¶
When a pass-through backend serves a store: true response, the router remembers which backend served its id, under the authenticated principal that created it. GET, DELETE, and POST .../cancel on that id are then forwarded to the same backend with its configured credential, and the backend's status, Content-Type, and body are returned (a GET query string such as ?stream=true is forwarded too). A successful DELETE also forgets the binding. A response the guardrail output stage blocked, masked, or cut is not bound, because the backend's stored copy is the unfiltered text; GET on it answers 404.
The router never sends an id it did not record to any backend, and re-checks the binding on every call against the live configuration: the backend must still be configured, enabled, not internal, and served through pass-through, and the caller's key must still be allowed to reach it (allowed_backends). A call that fails any check answers the same 404 as an unknown id, without contacting a backend. The bindings are held in memory per router instance, at most 10,000 (the oldest is forgotten first), and are lost on restart; a forgotten id also answers 404. cancel on a response the router produced itself answers 400, because such a response is already complete.
Background responses¶
A background: true request on a pass-through backend is forwarded as sent, and the backend answers queued (or in_progress) at once and finishes the response later. Admission (rate limits, local and hub budgets) runs at that POST as for any request.
- Metering. The
queuedanswer records nothing. The first time the router relays the terminal result (completed,incomplete,failed, orcancelled) carryingusage, throughGET /v1/responses/{id}orPOST .../cancel, it records the request and its usage once, through the same path a synchronous reply uses, attributed to the key that created the response, even when another key of the same principal fetches it. A repeat fetch records nothing. - Guardrails. While the guardrail policy applies to the request's model and the fetching key, every fetch of the result passes the output stage before it is relayed, and a block or mask applies as on a synchronous reply.
stream: truetogether withbackground: trueis refused with400 unsupported_request_parameterwhile a guardrail applies, since a client could resume that stream later throughGET ?stream=true, which the router cannot gate. - Streamed background without guardrails is forwarded and metered once: by the stream at its terminal event when the client stays connected, otherwise by the first non-streamed
GETorcancelof the terminal result, since the router stops reading the stream when the client disconnects while the backend keeps generating. - Replays.
GET ?stream=trueon a background result that is still unmetered (including a streamed one whose client disconnected before the terminal event), or while a guardrail applies, is refused with400 unsupported_request_parameter; fetch it withoutstream. - Limitation. A background result that is never fetched through the router (fetched from the backend directly, or not at all) is not metered: the router learns the usage only when it relays the result, and it does not poll backends on its own. The binding that holds the pending record is in memory per router instance and is lost on restart or eviction.
Responses Compact¶
Compact a conversation context by summarizing or compressing a sequence of input items. For OpenAI and Azure OpenAI backends, and backends configured with responses_api: native, the request is forwarded directly to the backend's native /v1/responses/compact endpoint. Other backends return HTTP 501 (Not Implemented), as context compaction is not natively supported.
When guardrails are configured, the input items and instructions go through the input stage before a backend is selected, and a block is always answered with the OpenAI content_filter error envelope (HTTP 400). The compaction output is opaque and is not gated. See Guardrails: Coverage by route.
Request Body:
{
"model": "gpt-5.6-sol",
"instructions": "Summarize the key points from this conversation.",
"input": [
{
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "What is the capital of France?"}]
},
{
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "The capital of France is Paris."}]
}
]
}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | Yes | Model identifier |
input | array | Yes | Array of input items representing the conversation to compact |
instructions | string | No | Optional instructions to guide the compaction process |
Response:
{
"id": "cmpt_abc123",
"object": "response.compaction",
"created_at": 1699000000,
"output": [
{
"type": "text",
"text": "Compacted conversation summary..."
}
],
"usage": {
"input_tokens": 120,
"output_tokens": 45
}
}
Status Codes:
200: Compaction successful400: Invalid request (empty model, empty input, instructions too long)501: Backend does not support native context compaction502: Backend connection error504: Request timeout
Batch API (control-plane builds)¶
The OpenAI-style Batch API is compiled only with the control-plane Cargo feature. It is absent from default source builds. Official release binaries compile the feature, but the routes remain inert unless control_plane.enabled, policy enforcement, and control_plane.batch.enabled are active and the Hub marks the presented key/tier as batch-eligible.
| Method | Path | Purpose |
|---|---|---|
POST | /v1/batches | Submit a provider batch |
GET | /v1/batches | List batches owned by the calling key |
GET | /v1/batches/{batch_id} | Retrieve owned batch status |
POST | /v1/batches/{batch_id}/cancel | Cancel an owned batch |
GET | /v1/batches/{batch_id}/results | Return completed results as JSONL |
A hub-synced API key is required. Ownership checks deliberately return the same 404 for unknown and other-key IDs. Results return 409 until available. The selected batch backend may be OpenAI-compatible or native Anthropic; the router presents one common surface. x-continuum-batch: off always opts a request out and cannot be overridden by policy.
For OpenAI-style batch creation, input_file_id may identify either a provider-owned file or a file in Continuum Router's local /v1/files storage. Provider-owned IDs are forwarded unchanged. For a router-local file, the router enforces the Files API ownership policy, requires purpose=batch, and refuses metadata larger than 200 MiB before reading content. After deferral and batch-pool admission, it streams the file once to the selected OpenAI-compatible backend with that backend's URL, credentials, and request extensions, then substitutes the returned provider file ID in the batch request. Native Anthropic batches use inline requests and do not accept a router-local input_file_id.
Before forwarding POST /v1/batches, the router checks the selected backend against the API key's allowed_backends. It also intersects the Hub tier's model_allowlist with the key's local allowed_models; an explicit empty Hub allow-list denies every model. Native Anthropic batches are checked from every requests[].params.model. For an OpenAI-style batch with a model restriction, the router scans a router-local file before upload, or fetches a provider-owned file with that backend's credentials, to check every JSONL body.model. An unrestricted OpenAI-style batch skips the model scan and may use the request's model hint only for usage attribution.
The input scan accepts at most 200 MiB in total and 16 MiB per JSONL record, and has a 60-second deadline. Invalid file IDs, malformed JSONL, missing models, fetch failures, and oversized inputs fail closed. These checks finish before deferral or batch-pool admission. A router-local scan reads storage as a stream and retains only the current bounded JSONL record. An unrestricted deferred request retains file metadata, not file content.
After admission, the router resolves the backend again. If its name, URL, credentials, provider type, or request extensions changed during a configuration reload, it releases the slot and returns batch_backend_changed without creating the provider batch.
The provider upload is a single attempt. A failed upload returns the provider error, or batch_input_upload_failed for a transport failure, without creating the batch. Retrying the client request can create another provider file if the first upload reached the provider but its response was lost. After a successful submission, the provider copy follows the provider's retention policy; deleting the router-local file does not cancel the batch or delete the provider copy.
| Error code | Status | Meaning |
|---|---|---|
backend_not_allowed | 403 | The API key may not use the selected batch backend. |
model_not_allowed | 403 | A numbered batch request names a model outside the effective allow-list. |
batch_input_file_access_denied | 403 | The caller may not read the router-local input file. |
batch_input_file_wrong_purpose | 400 | The router-local file does not have purpose=batch. |
batch_input_file_provider_unsupported | 400 | The selected provider uses inline batch requests and cannot accept the local file. |
invalid_input_file_id | 400 | The provider file ID is not 1 to 128 ASCII letters, digits, _, or -. |
batch_input_unverifiable | 400 | A numbered request is malformed or has no usable model. |
batch_input_too_large_to_verify | 400 | The total input or one JSONL record exceeds its scan bound. |
batch_input_fetch_failed | 502 | The provider file could not be fetched completely before the deadline. |
batch_input_read_failed | 500 | The router-local file metadata or content could not be read. |
batch_input_upload_failed | 502 | The provider upload failed before returning a response. |
batch_input_upload_invalid_response | 502 | The provider accepted the upload but did not return a valid file ID. |
batch_backend_changed | 503 | The selected backend changed while the request waited for admission. |
Fine-Tuning API¶
The OpenAI Fine Tuning job routes are forwarded to a selected OpenAI-wire backend. Create, retrieve, and cancel return the backend's answer unchanged; the list body keeps only the caller's own jobs. They are mounted on every build, unlike the feature-gated Batch API.
| Method | Path | Purpose |
|---|---|---|
POST | /v1/fine_tuning/jobs | Create a job on the backend selected from model |
GET | /v1/fine_tuning/jobs | List the caller's jobs from one selected fine-tuning capable backend |
GET | /v1/fine_tuning/jobs/{job_id} | Retrieve a job from the backend that created it |
POST | /v1/fine_tuning/jobs/{job_id}/cancel | Cancel a job on the backend that created it |
Only the openai and generic backend types on http or https are candidates. Azure OpenAI is excluded because its fine-tuning endpoint is a different path with a required api-version. A request that no candidate can serve returns 400 fine_tuning_not_supported, not a router-level 404.
The router records (principal, job id) to the backend that created the job. Retrieve and cancel re-check that binding against the live configuration and the caller's allowed_backends; an unknown or foreign id returns 404 fine_tuning_job_not_found without contacting the backend. The list keeps only the entries the router bound to the caller, so it never discloses another principal's job ids or training-file ids. The binding is held in process memory, so a job created before a router restart is not listed and answers not-found.
Job CRUD applies auth, rate limiting, and usage attribution but not request-parameter policy, guardrails, response caching, or the per-model generation budget. Each call is recorded as a non-completion request with zero tokens. A training_file or validation_file must already exist on the selected backend: a router-local /v1/files id is not uploaded to the provider.
| Error code | Status | Meaning |
|---|---|---|
fine_tuning_not_supported | 400 | No backend serving the model (or, for list, no configured backend) implements the Fine Tuning API. |
fine_tuning_job_not_found | 404 | The job id is unknown, belongs to another principal, or its backend is no longer usable. |
Vector Stores API¶
The OpenAI Vector Stores routes are forwarded to a selected OpenAI-wire backend. Store and file operations return the backend's answer unchanged; a list body keeps only the caller's own stores. They are mounted on every build.
| Method | Path | Purpose |
|---|---|---|
POST | /v1/vector_stores | Create a store on one selected vector-store capable backend |
GET | /v1/vector_stores | List the caller's stores from one selected vector-store capable backend |
GET | /v1/vector_stores/{store_id} | Retrieve a store from the backend that created it |
POST | /v1/vector_stores/{store_id} | Modify a store on the backend that created it |
DELETE | /v1/vector_stores/{store_id} | Delete a store on the backend that created it |
POST | /v1/vector_stores/{store_id}/files | Attach a provider file to the store |
GET | /v1/vector_stores/{store_id}/files | List the files attached to the store |
GET | /v1/vector_stores/{store_id}/files/{file_id} | Retrieve one attached file |
DELETE | /v1/vector_stores/{store_id}/files/{file_id} | Remove one attached file |
POST | /v1/vector_stores/{store_id}/search | Search the store |
Only the openai and generic backend types on http or https are candidates. Azure OpenAI is excluded because its Vector Stores endpoint is a different path with a required api-version. A request that no candidate can serve returns 400 vector_stores_not_supported, not a router-level 404.
A store call names no model, so the x-backend header is not consulted. Create and list select one capable backend with the configured selection strategy. The router then records (principal, store id) to the backend that created the store, and every store-scoped call re-checks that binding against the live configuration and the caller's allowed_backends. An unknown or foreign id returns 404 vector_store_not_found without contacting the backend. The list keeps only the entries the router bound to the caller, so it never discloses another principal's store ids. Its has_more, first_id, and last_id fields still describe the backend page, so a caller pages until has_more is false. The binding is held in process memory, so a store created before a router restart is not listed and answers not-found.
An attachment names a provider file id on the shared backend credential. The router does not record which principal uploaded a provider file, so a provider file id that leaks can be attached to another principal's store and read through search. Do not share one provider credential across tenants that must not see each other's files.
These routes apply auth, API-key scope, rate limiting, and usage attribution but not request-parameter policy, guardrails, response caching, or the per-model generation budget. Each call is recorded as a non-completion request with zero tokens. JSON request bodies are bounded at 1 MiB. An attachment names a provider file id: a router-local /v1/files id is not uploaded to the provider.
| Error code | Status | Meaning |
|---|---|---|
vector_stores_not_supported | 400 | No configured backend implements the OpenAI Vector Stores API. |
vector_store_not_found | 404 | The store id is unknown, belongs to another principal, or its backend is no longer usable. |
vector_store_file_not_found | 404 | The attached-file id is not a safe provider id. |
Image Generation¶
Known model capabilities are checked before image generation, editing, or variations reach a backend. If the requested operation is absent from a non-empty capability list, the router returns HTTP 400 with unsupported_model_capability and param: "model". Models with absent or empty capability lists remain eligible, including custom image editing models. Backend-specific capability overrides restrict selection to compatible backends.
Generate images using OpenAI's DALL-E, GPT Image models, or Google's Nano Banana (Gemini) models.
Request Body:
{
"model": "dall-e-3",
"prompt": "A serene Japanese garden with cherry blossoms",
"n": 1,
"size": "1024x1024",
"quality": "standard",
"response_format": "url"
}
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | No | Image model: gpt-image-2.5-sunburst, gpt-image-2.5-flare, gpt-image-2, gpt-image-1, gpt-image-1.5, gpt-image-1-mini, nano-banana, nano-banana-2, nano-banana-2-lite, or nano-banana-pro (default: gpt-image-2). dall-e-2 and dall-e-3 are still accepted for backends that serve them, but OpenAI shut both down on 2026-05-12. |
prompt | string | Yes | Description of the image to generate |
n | integer | No | Number of images (1-10, varies by model) |
size | string | No | Image size (varies by model, see below) |
quality | string | No | Image quality (varies by model, see below) |
style | string | No | Image style: vivid or natural (DALL-E 3 only) |
response_format | string | No | Response format: url or b64_json |
output_format | string | No | Output file format: png, jpeg, webp (GPT Image models only, default: png) |
output_compression | integer | No | Compression level 0-100 for jpeg/webp (GPT Image models only) |
background | string | No | Background: transparent, opaque, auto (GPT Image models only) |
stream | boolean | No | Enable streaming for partial images (GPT Image models only, default: false) |
partial_images | integer | No | Number of partial images 0-3 during streaming (GPT Image models only) |
google_search | boolean | No | Enable Google Search grounding (tools: [{"google_search": {}}]) for nano-banana-pro and nano-banana-2 (gemini-3-pro-image / gemini-3.1-flash-image) |
user | string | No | User identifier for tracking |
Model-specific constraints:
| Model | Sizes | n | Quality | Notes |
|---|---|---|---|---|
dall-e-2 | 256x256, 512x512, 1024x1024 | 1-10 | N/A | Classic DALL-E 2 |
dall-e-3 | 1024x1024, 1792x1024, 1024x1792 | 1 | standard, hd | High quality with prompt revision |
gpt-image-1 | 1024x1024, 1536x1024, 1024x1536, auto | 1-10 | low, medium, high, auto | Latest GPT Image model, supports streaming |
gpt-image-1.5 | 1024x1024, 1536x1024, 1024x1536, auto | 1-10 | low, medium, high, auto | 4x faster, better text rendering |
gpt-image-1-mini | 1024x1024, 1536x1024, 1024x1536, auto | 1-10 | low, medium, high, auto | Cost-effective option |
gpt-image-2 | presets 1024x1024, 1536x1024, 1024x1536, auto, or custom WIDTHxHEIGHT (edges multiples of 16, long edge <= 3840, ratio 1:3 to 3:1, 655,360 to 8,294,400 pixels) | 1-10 | low, medium, high, auto | GPT Image 2 |
gpt-image-2.5-sunburst, gpt-image-2.5-flare | presets 1024x1024, 1536x1024, 1024x1536, auto, or custom WIDTHxHEIGHT (edges multiples of 16, long edge <= 3840, ratio 1:3 to 3:1, 655,360 to 8,294,400 pixels) | 1-10 | low, medium, high, xhigh, max, auto | GPT Image 2.5 (Sunburst most capable, Flare fast); the only GPT Image models that take xhigh and max |
nano-banana, nano-banana-2 | 256x256 to 4096x4096 | 1-4 | N/A | Gemini 3.1 Flash Image (gemini-3.1-flash-image; fast, up to 4K, supports google_search) |
nano-banana-2-lite | 256x256 to 1024x1024 | 1-4 | N/A | Gemini 3.1 Flash Lite Image (gemini-3.1-flash-lite-image; cost-effective 1K generation) |
nano-banana-pro | 256x256 to 4096x4096 | 1-4 | N/A | Gemini 3 Pro Image (gemini-3-pro-image; advanced, up to 4K, supports google_search) |
Quality Parameter (GPT Image Models):
For GPT Image models, standard maps to medium and hd maps to high.
| Quality | Description |
|---|---|
low | Fast generation with lower quality |
medium | Balanced quality and speed (default) |
high | Best quality, slower generation |
auto | Model selects optimal quality |
Output Format Options (GPT Image Models):
| Format | Description | Supports Transparency |
|---|---|---|
png | Lossless format (default) | Yes |
jpeg | Lossy format, smaller file size | No |
webp | Modern format, good compression | Yes |
Note: Transparent background (
background: "transparent") requirespngorwebpformat.
Nano Banana (Gemini) Models:
Nano Banana provides access to Google's Gemini image generation capabilities through an OpenAI-compatible interface:
nano-banana/nano-banana-2: Maps to Gemini 3.1 Flash Image (gemini-3.1-flash-image) - fast, general-purpose image generation with up to 4K resolution and optional Google Search grounding (google_search: true).nano-bananamapped togemini-2.5-flash-imageuntil Google limited that model to projects that already used it on 2026-09-18; that id still passes through when named directly.nano-banana-2-lite: Maps to Gemini 3.1 Flash Lite Image (gemini-3.1-flash-lite-image) - lightweight 1K image generation.nano-banana-pro: Maps to Gemini 3 Pro Image (gemini-3-pro-image) - advanced model with high-resolution support (up to 4K) and optional Google Search grounding (google_search: true).
Nano Banana requests go to Google's Gemini API with the selected backend's key, so only a backend configured with type: gemini serves them. A backend of any other type is never selected, whatever its name or URL: a type: openai or generic backend pointed at Google's OpenAI-compatible endpoint (https://generativelanguage.googleapis.com/v1beta/openai) must be changed to type: gemini to serve Nano Banana.
Nano Banana Size Mapping:
The router automatically converts OpenAI-style size parameters to Gemini's aspectRatio and imageSize format:
| OpenAI Size | Gemini aspectRatio | Gemini imageSize | Notes |
|---|---|---|---|
256x256 | 1:1 | 1K | Falls back to Gemini minimum |
512x512 | 1:1 | 1K | Falls back to Gemini minimum |
1024x1024 | 1:1 | 1K | Default |
1536x1024 | 3:2 | 1K | Landscape |
1024x1536 | 2:3 | 1K | Portrait |
1024x1792 | 9:16 | 1K | Tall portrait |
1792x1024 | 16:9 | 1K | Wide landscape |
2048x2048 | 1:1 | 2K | nano-banana-pro and nano-banana-2 (gemini-3.1-flash-image) |
4096x4096 | 1:1 | 4K | nano-banana-pro and nano-banana-2 (gemini-3.1-flash-image) |
auto | 1:1 | 1K | Default fallback |
The conversion sends the following Gemini API structure:
{
"contents": [{"parts": [{"text": "Your prompt"}]}],
"generationConfig": {
"imageConfig": {
"aspectRatio": "3:2",
"imageSize": "1K"
}
}
}
Example Nano Banana Request:
{
"model": "nano-banana",
"prompt": "A white siamese cat with blue eyes, photorealistic",
"n": 1,
"size": "1024x1024",
"response_format": "b64_json"
}
Response:
{
"created": 1677652288,
"data": [
{
"url": "https://oaidalleapiprodscus.blob.core.windows.net/...",
"revised_prompt": "A tranquil Japanese garden featuring..."
}
]
}
Response (with b64_json):
{
"created": 1677652288,
"data": [
{
"b64_json": "/9j/4AAQSkZJRgABAQAA...",
"revised_prompt": "A tranquil Japanese garden featuring..."
}
]
}
Nano Banana Response Notes:
- When using
response_format: "url"with Nano Banana, the image is returned as a data URL (data:image/png;base64,...) since Gemini's native API returns inline base64 data - The
revised_promptfield contains any text response from Gemini describing the generated image - When
google_search: trueis used (ortools: [{"google_search": {}}]), anygroundingMetadatareturned by Gemini is preserved asgrounding_metadataat the top level of the image response - When using
/v1/chat/completionsor/v1/responseswith a Gemini backend, requests that includetools: [{"google_search": {}}],tools: [{"code_execution": {}}],modalities: ["text", "image"],cachedContent, orgemini_context_cache.prefer_native_api: trueautomatically dispatch to Gemini's nativegenerateContent/streamGenerateContentendpoint and surfaceannotations,grounding_metadata,code_execution, and inlineimagesin the OpenAI response format
Streaming Image Generation (GPT Image Models):
When stream: true is specified for GPT Image models, the response will be streamed as Server-Sent Events (SSE):
Example Streaming Request:
{
"model": "gpt-image-1",
"prompt": "A beautiful sunset over mountains",
"stream": true,
"partial_images": 2,
"response_format": "b64_json"
}
Streaming Response Format:
data: {"type":"image_generation.partial_image","partial_image_index":0,"b64_json":"...","created":1702345678}
data: {"type":"image_generation.partial_image","partial_image_index":1,"b64_json":"...","created":1702345679}
data: {"type":"image_generation.complete","b64_json":"...","created":1702345680}
data: {"type":"image_generation.usage","usage":{"input_tokens":25,"output_tokens":1024}}
data: {"type":"done"}
SSE Event Types:
| Event Type | Description |
|---|---|
image_generation.partial_image | Intermediate image during generation |
image_generation.complete | Final complete image |
image_generation.usage | Token usage information (for cost tracking) |
done | Stream completion marker |
Example GPT Image Request with New Options:
{
"model": "gpt-image-1.5",
"prompt": "A white cat with blue eyes, photorealistic",
"size": "auto",
"quality": "high",
"output_format": "webp",
"output_compression": 85,
"background": "transparent",
"response_format": "b64_json"
}
Status Codes:
200: Image(s) generated successfully400: Invalid request (e.g., invalid size for model, n > 1 for DALL-E 3)401: Invalid API key429: Rate limit exceeded500: Backend error503: Gemini backend unavailable (for Nano Banana models)
Timeout Configuration: Image generation requests use a configurable timeout (default: 3 minutes). See timeouts.request.image_generation in configuration.
Image Edit (Inpainting)¶
Edit existing images using OpenAI's inpainting capabilities or Gemini's natural-language editing. This endpoint allows you to modify or combine one or more input images based on a text prompt and optional mask. Supports GPT Image models, Nano Banana (Gemini) models, and DALL-E 2.
Request Parameters (multipart/form-data):
| Parameter | Type | Required | Description |
|---|---|---|---|
image / image[] | file | Yes | One or more source images to edit (< 4MB per image; see per-model format and count limits below) |
prompt | string | Yes | Description of the desired edit |
mask | file | No | Mask image indicating edit regions (PNG, same dimensions as the first source image; ignored by Nano Banana models) |
model | string | No | Model to use (default: gpt-image-2) |
n | integer | No | Number of images to generate (1-10 for OpenAI models, 1 for Nano Banana, default: 1) |
size | string | No | Output size (model-dependent, default: 1024x1024) |
response_format | string | No | Response format: url or b64_json (default: url) |
google_search | boolean | No | Enable Google Search grounding (tools: [{"google_search": {}}]) for nano-banana-pro and nano-banana-2 |
user | string | No | Unique user identifier for tracking |
Supported Models, Sizes, and Input Image Limits:
| Model | Sizes | Max Input Images | Supported Input Formats | Notes |
|---|---|---|---|---|
gpt-image-2, gpt-image-2.5-sunburst, gpt-image-2.5-flare | presets or custom WIDTHxHEIGHT, same rule as generation | 16 | PNG, JPEG, WebP | GPT Image 2 generation |
gpt-image-1 | 1024x1024, 1536x1024, 1024x1536, auto | 16 | PNG, JPEG, WebP | GPT Image 1 model |
gpt-image-1-mini | 1024x1024, 1536x1024, 1024x1536, auto | 16 | PNG, JPEG, WebP | Cost-optimized version |
gpt-image-1.5 | 1024x1024, 1536x1024, 1024x1536, auto | 16 | PNG, JPEG, WebP | Improved instruction following |
nano-banana, nano-banana-2 | 256x256, 512x512, 1024x1024, 1024x1792, 1792x1024, 1536x1024, 1024x1536, 2048x2048, 4096x4096, auto | 14 | PNG, JPEG, WebP, HEIC, HEIF | Gemini 3.1 Flash Image (mask ignored, n=1, up to 4K) |
nano-banana-2-lite | 256x256, 512x512, 1024x1024, 1024x1792, 1792x1024, 1536x1024, 1024x1536, auto | 14 | PNG, JPEG, WebP, HEIC, HEIF | Gemini 3.1 Flash Lite Image (mask ignored, n=1, 1K) |
nano-banana-pro | Same as nano-banana-2 (up to 4K) | 14 | PNG, JPEG, WebP, HEIC, HEIF | Gemini 3 Pro Image (mask ignored, n=1) |
dall-e-2 | 256x256, 512x512, 1024x1024 | 1 | PNG (square) | Legacy DALL-E 2 model |
Note: DALL-E 3 does NOT support image editing via this endpoint. Nano Banana (
nano-banana,nano-banana-2,nano-banana-2-lite,nano-banana-pro) and Gemini image models support natural-language editing and multi-image composition (up to 14 input images); when amaskfield is supplied for Nano Banana, it is ignored with a warning because Gemini determines edit regions from the prompt text.
Image Requirements:
- Field name:
imageorimage[](repeat the field to send multiple input images) - Size: Less than 4MB per image
- Count limit: Up to 16 images for
gpt-image-*, up to 14 images fornano-banana/nano-banana-2/nano-banana-2-lite/nano-banana-pro/ Gemini image models, and 1 image fordall-e-2 - Format and dimensions:
gpt-image-*: PNG, JPEG, or WebP (non-square dimensions allowed)nano-banana,nano-banana-2,nano-banana-2-lite,nano-banana-pro: PNG, JPEG, WebP, HEIC, or HEIF (non-square dimensions allowed)dall-e-2: PNG only, must be square (width equals height)
Mask Requirements:
- Format: PNG with alpha channel (RGBA)
- Dimensions: Must match the first source image (
images[0]) exactly - Transparent areas: Indicate regions to edit/generate
- Opaque areas: Indicate regions to preserve
Example Request:
curl -X POST http://localhost:8080/v1/images/edits \
-F "image=@source_image.png" \
-F "mask=@mask.png" \
-F "prompt=A sunlit indoor lounge area with a pool containing a flamingo" \
-F "n=1" \
-F "size=1024x1024" \
-F "response_format=url"
Example Request (multiple input images):
curl -X POST http://localhost:8080/v1/images/edits \
-F "model=gpt-image-2" \
-F "image[]=@subject.png" \
-F "image[]=@background.png" \
-F "prompt=Place the subject from the first image into the background of the second image" \
-F "n=1" \
-F "size=1024x1024"
Response:
{
"created": 1677652288,
"data": [
{
"url": "https://oaidalleapiprodscus.blob.core.windows.net/..."
}
]
}
Response (with b64_json):
Status Codes:
200: Image(s) edited successfully400: Invalid request (e.g., non-square image, invalid size, missing required field)401: Invalid API key503: OpenAI backend unavailable
Error Examples:
Non-square image:
{
"error": {
"message": "Image must be square (800x600 is not square)",
"type": "invalid_request_error",
"param": "image",
"code": "image_not_square"
}
}
Mask dimension mismatch:
{
"error": {
"message": "Mask dimensions (256x256) do not match image dimensions (512x512)",
"type": "invalid_request_error",
"param": "mask",
"code": "dimension_mismatch"
}
}
Unsupported model:
{
"error": {
"message": "Model 'dall-e-3' does not support image editing. Supported models: gpt-image-2.5-sunburst, gpt-image-2.5-flare, gpt-image-2, gpt-image-1, gpt-image-1-mini, gpt-image-1.5, dall-e-2, nano-banana, nano-banana-2, nano-banana-2-lite, nano-banana-pro. Note: dall-e-3 does NOT support image editing.",
"type": "invalid_request_error",
"param": "model",
"code": "unsupported_model"
}
}
Notes:
- Supported models:
gpt-image-2.5-sunburst,gpt-image-2.5-flare,gpt-image-2,gpt-image-1,gpt-image-1-mini,gpt-image-1.5,dall-e-2,nano-banana,nano-banana-2,nano-banana-2-lite,nano-banana-pro, plus configured metadata aliases that resolve to one of these models or a Gemini image model - DALL-E 3 does NOT support image editing via API
- Nano Banana (
nano-banana,nano-banana-2,nano-banana-2-lite,nano-banana-pro) routes to Gemini's nativegenerateContentAPI using natural-language editing (maskis ignored if supplied, andnmust be1) - Multiple input images can be uploaded by repeating
imageorimage[]up to the model's input limit (16 forgpt-image-*, 14 fornano-banana/nano-banana-2/nano-banana-2-lite/nano-banana-pro, 1 fordall-e-2) - When no mask is provided, the entire image may be modified
- Request timeout uses the image generation timeout configuration
Image Variations¶
Generate variations of an existing image using Nano Banana (Gemini) or OpenAI's DALL-E 2 model.
Form Fields:
| Parameter | Type | Required | Description |
|---|---|---|---|
image | file | Yes | Single source image for variations (< 4MB; only 1 input image is accepted) |
model | string | No | Model to use (default: nano-banana; OpenAI's only variations model, dall-e-2, was shut down on 2026-05-12) |
n | integer | No | Number of variations to generate (1-10 for DALL-E 2, 1 for Nano Banana, default: 1) |
size | string | No | Output size: 256x256, 512x512, 1024x1024 (default: 1024x1024) |
response_format | string | No | Response format: url or b64_json (default: url) |
user | string | No | User identifier for tracking |
Example Request:
curl -X POST http://localhost:8080/v1/images/variations \
-F "image=@source_image.png" \
-F "model=dall-e-2" \
-F "n=2" \
-F "size=512x512" \
-F "response_format=url"
Response:
{
"created": 1677652288,
"data": [
{
"url": "https://oaidalleapiprodscus.blob.core.windows.net/..."
},
{
"url": "https://oaidalleapiprodscus.blob.core.windows.net/..."
}
]
}
Response (with b64_json):
Model Support:
| Model | Variations Support | Notes |
|---|---|---|
nano-banana, nano-banana-2 | Yes (default) | Supported via Gemini generateContent (n=1, up to 4K; PNG, JPEG, WebP, HEIC, HEIF) |
nano-banana-2-lite | Yes | Supported via Gemini generateContent (n=1, 1K; PNG, JPEG, WebP, HEIC, HEIF) |
nano-banana-pro | Yes | Supported via Gemini generateContent (n=1; up to 4K) |
dall-e-2 | Yes (native) | Legacy OpenAI support, 1-10 variations (PNG, square) |
dall-e-3 | No | Not supported by OpenAI API |
gpt-image-1 | No | Not supported |
Image Requirements:
- Format: PNG only
- Size: Less than 4MB
- Dimensions: Must be square (width == height)
- Supported input sizes: Any square dimensions (will be processed by the model)
Error Scenarios:
| Error | Status | Description |
|---|---|---|
| Image not PNG | 400 | Only PNG format is supported |
| Image not square | 400 | Image dimensions must be equal |
| Image too large | 400 | Image exceeds 4MB size limit |
| Model not supported | 400 | Requested model doesn't support variations |
| Missing image | 400 | Image field is required |
| Invalid n value | 400 | n must be between 1 and 10 |
| Invalid size | 400 | Size must be one of the supported values |
Status Codes:
200: Variation(s) generated successfully400: Invalid request (invalid format, non-square image, unsupported model)401: Invalid API key429: Rate limit exceeded500: Backend error503: Backend unavailable
Audio Transcriptions & Translations¶
Transcribe audio into the input language (POST /v1/audio/transcriptions) or translate audio into English (POST /v1/audio/translations) using OpenAI-compatible multipart/form-data pass-through.
Form Fields:
| Parameter | Type | Required | Description |
|---|---|---|---|
file | file | Yes | Audio file to transcribe or translate (bounded by proxy.audio.max_body_size, default 25 MiB) |
model | string | Yes | ID of the audio model to use (e.g., whisper-1, gpt-4o-transcribe, gpt-4o-mini-transcribe) |
response_format | string | No | Output format: json, text, srt, vtt, verbose_json, or diarized_json (relayed unchanged with upstream status and Content-Type) |
language | string | No | Input audio language in ISO-639-1 format (transcriptions) |
prompt | string | No | Optional text to guide the model's style |
temperature | number | No | Sampling temperature between 0 and 1 |
timestamp_granularities[] | array | No | Timestamp granularities (word, segment) for verbose_json |
stream | boolean | No | stream=true is rejected with 400 Bad Request (streaming_not_supported) |
Routing & Limits:
- Model routing & policy: The router extracts
modelfrom the multipart form to enforce per-keyallowed_models, Hub tiermodel_allowlist,rate_limiting.limits.per_model, and backend selection (includingx-backendandx-served-backend). - Body limit & concurrency: Controlled by
proxy.audio.max_body_size(default26214400/ 25 MiB, max 100 MiB) andproxy.audio.max_concurrent_uploads(default8, max64). Uploads exceedingmax_body_sizereturn413 Payload Too Largewithout full buffering. - Timeouts & retry: Controlled by
timeouts.request.audio(defaultfirst_byte: "60s",total: "300s") andtimeouts.limits.max_audio_timeout(default"600s"). Upload bodies are dispatched single-shot without retry or fallback. - Usage metering: On control-plane builds, billed audio duration is recorded in whole seconds as
audio_secondson the usage record (preferringusage.secondswhenusage.type == "duration", or rounding up top-leveldurationonverbose_jsonresponses) and priced viaaudio_microusd_per_second, whileinput_tokens,output_tokens, andcached_input_tokensstay0so token-rate and token-budget windows are unaffected.
Audio Speech (Text-to-Speech)¶
Generate audio from text (POST /v1/audio/speech) using OpenAI-compatible JSON pass-through. The request body is forwarded as application/json to the selected backend's /v1/audio/speech. A successful audio response is streamed to the client as the backend sends it, with the backend's status and Content-Type (typically audio/mpeg), so playback can start before synthesis finishes.
Request Body:
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | Yes | ID of the TTS model to use (e.g., tts-1, tts-1-hd, gpt-4o-mini-tts) |
input | string | Yes | The text to synthesize. Must be non-empty |
voice | string | No | Voice to use (e.g., alloy). Validated by the backend |
Additional OpenAI fields (response_format, speed, instructions, ...) are passed through unchanged. Alias dispatch rewrites the model field to the canonical backend model id when a mapping is configured.
Routing & Limits:
- Model routing & policy: The router extracts
modelfrom the JSON body to enforce per-keyallowed_models, Hub tiermodel_allowlist, and backend selection (includingx-backendandx-served-backend). - Body limit: The JSON body is capped at 1 MiB, or at
proxy.audio.max_body_sizewhen that is lower. A larger body returns413 Payload Too Large, whether or not it declaresContent-Length; the router stops reading at the cap. - Rate limits and metering: per-model rate limits read the body's
model, the route requires thewriteAPI-key scope, and each served call records one usage row (usage_missingwith zero tokens for an audio body, or the parsedusagewhen the backend answers JSON). The row is recorded when the response head arrives. - Backend errors: a backend
4xxis answered with the sanitized backend message and a5xxwith a generic message, as on the transcription routes; the raw backend body is never relayed. - Timeouts & retry: Controlled by
timeouts.request.audio(defaultfirst_byte: "60s",total: "300s") andtimeouts.limits.max_audio_timeout(default"600s"). The request body is dispatched single-shot without retry or fallback.
Errors:
| Error | Status | Code |
|---|---|---|
Missing model field | 400 | missing_model |
Missing or empty input field | 400 | missing_input |
| Body is not a valid JSON object | 400 | invalid_json |
X-Model header mismatch | 400 | conflicting_model |
| Body too large | 413 | payload_too_large |
| Model has no backend | 404 | model_not_found |
Model or backend excluded by the key's allowed_models / allowed_backends | 403 | model_not_permitted |
| Backend timeout | 504 | audio_timeout |
| Backend connection error | 502 | audio_connection_error |
Text Completions¶
Generate text completions using the OpenAI Completions API format.
Request Body:
{
"model": "gpt-5.6-luna",
"prompt": "Once upon a time in a distant galaxy",
"max_tokens": 100,
"temperature": 0.7,
"top_p": 1.0,
"frequency_penalty": 0.0,
"presence_penalty": 0.0,
"stream": false,
"stop": null,
"logit_bias": {},
"user": "user123"
}
Response:
{
"id": "cmpl-123456789",
"object": "text_completion",
"created": 1677652288,
"model": "gpt-5.6-luna",
"choices": [
{
"text": ", there lived a young explorer named Zara who dreamed of discovering new worlds...",
"index": 0,
"finish_reason": "stop",
"logprobs": null
}
],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 90,
"total_tokens": 100
}
}
Streaming: With stream: true and guardrails disabled for the route, TCP and Unix-socket backends are relayed incrementally as text/event-stream, without Content-Length. The first text chunk can arrive before generation finishes. A mid-stream failure produces an SSE error and ends the stream; the router does not retry after accepting the stream.
The router always requests upstream usage for accounting. Set stream_options.include_usage: true to receive the trailing usage-only chunk. Disconnects and failures before that chunk are metered using the observed text and a prompt estimate. When guardrails are enabled for the route, the router buffers the entire output before releasing it, so blocked or masked text cannot leak.
Guardrails: When guardrails are configured, /v1/completions runs both input and output checks (see Guardrails: Coverage by route):
- Input stage: Each prompt segment (
promptstring, or each element of a string/nested-string array up to 128 prompts) andsuffix(checked once) are evaluated before dispatch. Requests exceeding 128 prompts, or using uninspectable token-ID /prompt_embedsinputs, are rejected inenforcemode (uninspectable_input) and recorded inmonitormode. Masking verdicts modify prompt segments in place. - Output stage: Evaluates
choices[].textacross all responses, including bufferedstream: truereplies. - Blocked responses: Return a
text_completionobject withfinish_reason: "content_filter"(HTTP 200), or an OpenAIcontent_filtererror envelope (HTTP 400) whenblock_behavior: erroris configured.
Status Codes: Same as Chat Completions
Embeddings¶
A known non-empty capability list must include embedding; otherwise the router returns HTTP 400 with unsupported_model_capability before sending the request upstream. Unknown and empty lists remain permissive. This also applies to multimodal embedding requests.
Generate embeddings for input text using the OpenAI Embeddings API format.
Request Body:
{
"model": "text-embedding-3-small",
"input": "The quick brown fox jumps over the lazy dog",
"encoding_format": "float",
"dimensions": 512,
"user": "user123"
}
Request Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
model | string | Yes | ID of the model to use (e.g., text-embedding-3-small, gemini-embedding-2) |
input | string, array, or content parts | Yes | Input to embed. Can be a single string, array of strings, array of token arrays, or an array of multimodal content parts (for Gemini Embedding 2) |
encoding_format | string | No | Format to return the embeddings: float or base64 (default: float) |
dimensions | integer | No | Number of dimensions for the output embeddings (supported for text-embedding-3-* and gemini-embedding-2* models) |
user | string | No | Unique identifier for the end-user |
Input Format Examples:
Single string:
Array of strings:
{
"model": "text-embedding-3-large",
"input": [
"First text to embed",
"Second text to embed",
"Third text to embed"
]
}
Token arrays:
Multimodal content parts (Gemini Embedding 2 only):
{
"model": "gemini-embedding-2",
"input": [
{"type": "text", "text": "A photo of a golden retriever"},
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQ..."}}
]
}
Response:
{
"object": "list",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [0.0023064255, -0.009327292, 0.0045318254, ...]
}
],
"model": "text-embedding-3-small",
"usage": {
"prompt_tokens": 8,
"total_tokens": 8
}
}
Response Fields:
| Field | Type | Description |
|---|---|---|
object | string | Always "list" |
data | array | Array of embedding objects |
data[].object | string | Always "embedding" |
data[].index | integer | Index of the embedding in the input array |
data[].embedding | array | Embedding vector (array of floats) |
model | string | Model used to generate the embedding |
usage | object | Token usage information |
usage.prompt_tokens | integer | Number of tokens in the input |
usage.total_tokens | integer | Total tokens used (same as prompt_tokens for embeddings) |
Supported Models:
| Backend | Model | Notes |
|---|---|---|
| OpenAI | text-embedding-3-small | 1536 dimensions by default, supports dimensions parameter |
| OpenAI | text-embedding-3-large | 3072 dimensions by default, supports dimensions parameter |
| OpenAI | text-embedding-ada-002 | 1536 dimensions, legacy model |
| Gemini | gemini-embedding-2 | 3072 dimensions, multimodal (text, image, audio, video, document) |
| Gemini | gemini-embedding-2-preview | Preview release of Gemini Embedding 2, multimodal |
| Gemini | text-embedding-004 | Text-only, via OpenAI-compatible endpoint |
| Self-hosted | bge-m3 | 1024 dimensions, 100+ languages, 8192 context. Supports dense, sparse, and ColBERT retrieval |
| Self-hosted | bge-large-en-v1.5 | 1024 dimensions, English-only, 512 context |
| Self-hosted | multilingual-e5-large | 1024 dimensions, 100+ languages, 514 context |
| vLLM | Deployment-specific | Depends on deployed model |
| llama.cpp | Deployment-specific | Native /v1/embeddings support |
| TEI | Deployment-specific | Hugging Face Text Embeddings Inference server |
| Ollama | Deployment-specific | Via Ollama embedding models |
Status Codes:
200: Embeddings generated successfully400: Invalid request (missing model/input, invalid dimensions)401: Invalid API key404: Model not found or doesn't support embeddings429: Rate limit exceeded500: Backend error503: Backend unavailable
Features:
- Multiple Input Formats: Supports single string, array of strings, token arrays, or multimodal content parts
- Multimodal Embeddings:
gemini-embedding-2andgemini-embedding-2-previewaccept mixed text, image, audio, video, and document parts in a single request - Dimension Control: For
text-embedding-3-*andgemini-embedding-2*models, specify custom dimensions for reduced vector size - Backend Agnostic: Routes to appropriate backend based on model
- Load Balancing: Applies configured load balancing strategy
- Error Handling: Returns
400with a clear message when multimodal input is sent to a text-only model
Multimodal Content Part Types (Gemini Embedding 2 only):
| Type | Fields | Constraints |
|---|---|---|
text | text (string) | Must not be empty |
image_url | image_url.url (data URI or file://file-* reference) | MIME: image/jpeg, image/png; max 20 MiB; up to 6 images per request |
audio | audio.data (base64), audio.format | Formats: mp3, wav; max 20 MiB |
video | video.data (base64), video.format | Formats: mp4, mov; max 20 MiB |
document | document.data (base64), document.format | Formats: pdf; max 20 MiB |
Example Request:
curl -X POST http://localhost:8080/v1/embeddings \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-key" \
-d '{
"model": "text-embedding-3-small",
"input": "The quick brown fox jumps over the lazy dog",
"encoding_format": "float"
}'
Example with Multiple Inputs:
curl -X POST http://localhost:8080/v1/embeddings \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-key" \
-d '{
"model": "text-embedding-3-large",
"input": [
"First document text",
"Second document text",
"Third document text"
]
}'
Example with Custom Dimensions:
curl -X POST http://localhost:8080/v1/embeddings \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-key" \
-d '{
"model": "text-embedding-3-small",
"input": "Sample text",
"dimensions": 512
}'
Example with Multimodal Input (Gemini Embedding 2):
curl -X POST http://localhost:8080/v1/embeddings \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-key" \
-d '{
"model": "gemini-embedding-2",
"input": [
{"type": "text", "text": "A photo of a sunset over the ocean"},
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQ..."}}
],
"dimensions": 768
}'
Rerank¶
Rerank documents based on their relevance to a query using the Cohere-compatible Rerank API. This is commonly used as a second-stage retrieval step after initial vector search to improve accuracy.
Request Body:
{
"model": "bge-reranker-v2-m3",
"query": "What is machine learning?",
"documents": ["Document 1 content", "Document 2 content", "Document 3 content"],
"top_n": 3,
"return_documents": false
}
Request Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
model | string | Yes | ID of the reranking model to use (e.g., bge-reranker-v2-m3, rerank-english-v3.0, jina-reranker-v2-base-multilingual) |
query | string | Yes | The search query to compare documents against |
documents | array | Yes | List of documents to rerank. Can be an array of strings or an array of objects with a text field |
top_n | integer | No | Number of top results to return. If not specified, returns all documents ranked |
return_documents | boolean | No | Whether to return the document text in the response (default: false) |
max_chunks_per_doc | integer | No | Maximum number of chunks to process per document for long document handling |
Document Format Options:
Simple string array:
{
"model": "bge-reranker-v2-m3",
"query": "What is deep learning?",
"documents": [
"Deep learning uses neural networks with multiple layers",
"Machine learning is a subset of artificial intelligence",
"Natural language processing deals with text understanding"
]
}
Structured documents with text field:
{
"model": "rerank-english-v3.0",
"query": "What is deep learning?",
"documents": [
{"text": "Deep learning uses neural networks with multiple layers"},
{"text": "Machine learning is a subset of artificial intelligence"}
]
}
Response:
{
"results": [
{
"index": 0,
"relevance_score": 0.95
},
{
"index": 2,
"relevance_score": 0.72
},
{
"index": 1,
"relevance_score": 0.45
}
],
"model": "bge-reranker-v2-m3",
"id": "rerank-abc123",
"usage": {
"prompt_tokens": 150,
"total_tokens": 150
}
}
Response with Documents (when return_documents: true):
{
"results": [
{
"index": 0,
"relevance_score": 0.95,
"document": {
"text": "Deep learning uses neural networks with multiple layers"
}
}
],
"model": "bge-reranker-v2-m3"
}
Response Fields:
| Field | Type | Description |
|---|---|---|
results | array | List of reranked results ordered by relevance score (highest first) |
results[].index | integer | The index of the document in the original input list |
results[].relevance_score | number | Relevance score (typically 0.0 to 1.0, higher is more relevant) |
results[].document | object | The document text (only present if return_documents was true) |
model | string | The model used for reranking |
id | string | Unique identifier for this request (optional) |
usage | object | Token usage information (optional) |
Supported Backends:
| Backend | Endpoint | Notes |
|---|---|---|
| vLLM | /v1/rerank | Cohere-compatible, supports BGE, Jina rerankers |
| llama.cpp | /v1/rerank | Requires --reranking flag at startup |
| Hugging Face TEI | /rerank | Text Embeddings Inference server |
| Cohere API | /v1/rerank | Native Cohere rerank endpoint |
| Jina AI | /v1/rerank | Native Jina rerank endpoint |
Status Codes:
200: Documents reranked successfully400: Invalid request (missing model/query/documents, empty documents array)401: Invalid API key404: Model not found or doesn't support reranking429: Rate limit exceeded500: Backend error503: Backend unavailable
Use Cases:
- Two-stage retrieval: Use vector search to retrieve candidates, then rerank for higher precision
- RAG systems: Improve context quality by reranking retrieved documents before LLM processing
- Search result refinement: Reorder search results based on semantic relevance
Example Request:
curl -X POST http://localhost:8080/v1/rerank \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-key" \
-d '{
"model": "bge-reranker-v2-m3",
"query": "What are the benefits of renewable energy?",
"documents": [
"Solar panels convert sunlight into electricity",
"Wind turbines generate power from wind",
"Coal is a fossil fuel used for electricity"
],
"top_n": 2
}'
Moderations¶
Classify text with the router's configured guardrails using the OpenAI Moderations API shape, so client.moderations.create() works against the router.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
input | string or array | Yes | One text, or an array of up to 32 items, each a string or a {"type": "text", "text": "..."} part. Image parts are refused with 400 invalid_input |
model | string | No | Selects the guardrail route for that model (guardrails.routes), the same route a chat request for it would use. It names no moderation model; when omitted, the global policy applies and the response echoes guardrails |
Response:
{
"id": "modr-3f6c0d1e9a2b4c5d6e7f8a9b",
"model": "gpt-5.6-terra",
"results": [
{
"flagged": true,
"categories": {"violence": true},
"category_scores": {"violence": 0.91}
},
{
"flagged": false,
"categories": {},
"category_scores": {}
}
]
}
- One result per input, in input order. Each input runs the input-stage evaluation a live request would: the
denyandallowmatch lists first, then every provider that participates in the input stage for the resolved route, with the same thresholds. The globalguardrails.modeand the bypass list are ignored, because a moderation request asks for a classification rather than a policy action. - Categories are the router's guardrail categories (
violence,hate_speech,sexual_content,self_harm,harassment,dangerous,jailbreak,pii,profanity, or a provider-specific label), not OpenAI's fixed category list. Only categories some entry flagged or blocked appear, andcategory_scorescarries the highest provider score for each. Adenymatch-list hit reports thedeny_listcategory with score1.0. - Provider failures never flag text: a provider's fail-policy verdict (for example
fail_closed) is left out of the result. When every evaluation that ran failed, the request answers503 moderation_unavailable. - Scope and cost: the route requires the
writescope, because every input may call each configured provider.
Errors:
| Error | Status | Code |
|---|---|---|
| Guardrails are not enabled on this router | 400 | moderation_unavailable |
input missing, empty, or not a string or text parts | 400 | invalid_input |
More than 32 input items | 400 | too_many_inputs |
model is not a string | 400 | invalid_type |
| Body is not valid JSON | 400 | invalid_json |
| Every guardrail provider failed | 503 | moderation_unavailable |
Tokenize and Detokenize¶
Turn text into the token ids of a model, or token ids back into text, using the tokenizer of the engine that serves the model. The router holds no tokenizer of its own for these routes: it forwards the request to one backend that serves exactly the requested model.
The four paths are interchangeable on ingress: both spellings reach the same handler whichever engine serves the model. The path the router calls on the engine is chosen by the backend type, not by the path the client used (see Engine support). All four need the read scope.
Tokenize request¶
The body is the vLLM and SGLang shape. The router translates it for llama.cpp and mlxcel, whose native bodies carry no model.
{
"model": "Qwen/Qwen3-32B",
"prompt": "Hello, world!",
"add_special_tokens": true,
"return_token_strs": true
}
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | Yes | Model whose tokenizer to use. A missing or empty value answers 400 missing_model |
prompt | string | One of prompt / messages | Raw text to tokenize. A list of prompts answers 400 unsupported_prompt_type |
messages | array | One of prompt / messages | A chat, rendered through the engine's chat template before tokenizing |
add_special_tokens | boolean | No | Add the tokenizer's special tokens (BOS and similar). See the note below for the default |
return_token_strs | boolean | No | Also return one string per token as token_strs (default false). Refused on sglang with 400 tokenize_option_unsupported, since SGLang ignores the field and returns no token_strs |
add_generation_prompt, continue_final_message, chat_template, chat_template_kwargs, tools | No | Chat-template options for messages. Forwarded unchanged to vllm and continuumrouter; see the option table under Engine support for llamacpp, mlxcel, and sglang |
Exactly one of prompt and messages must be present; both answers 400 conflicting_input and neither 400 missing_input.
add_special_tokens default: on a prompt request the effective default is true on every engine. vLLM and SGLang default to true themselves; llama.cpp and mlxcel default their native add_special to false, so the router always sends it explicitly, true unless the request says otherwise. On a messages request the default is false (vLLM defaults to false, because the chat template already adds special tokens; llama.cpp and mlxcel receive add_special: false unless the request sets add_special_tokens: true; SGLang strips add_special_tokens before rendering, so add_special_tokens: true on messages is refused on SGLang).
Tokenize response¶
{
"count": 4,
"tokens": [9707, 11, 1879, 0],
"max_model_len": 32768,
"token_strs": ["Hello", ",", " world", "!"]
}
| Field | Type | Description |
|---|---|---|
count | integer | Number of token ids |
tokens | array | The token ids |
max_model_len | integer | The model's context length. Present only when the engine reports it (vLLM and SGLang) |
token_strs | array | One string per token. Present only when return_token_strs was true and the engine returned them. On vLLM these are the Hugging Face tokenizer's token strings (for byte-level BPE tokenizers such as Qwen3, GPT-2 byte-level pieces like "Ġworld"). On llama.cpp and mlxcel it is lossy: a token that is not valid UTF-8 on its own is decoded alone, so a multi-byte character split across tokens (one Hangul syllable can be three single-byte tokens) becomes one U+FFFD per token and cannot be rebuilt by joining the strings. Use /detokenize on the ids to get the exact text |
Detokenize request and response¶
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | Yes | Model whose tokenizer to use |
tokens | array | Yes | A flat array of non-negative integers, at most 1,048,576 entries. A nested array (the SGLang batch form) answers 400 invalid_tokens_type; more entries answer 400 too_many_tokens before any backend is contacted |
skip_special_tokens | boolean | No | Forwarded to vLLM and SGLang (SGLang honors it and defaults to true; vLLM has no skip_special_tokens field on DetokenizeRequest and always renders special tokens). Ignored, without an error, on llama.cpp and mlxcel, which always render special tokens |
Both keys always carry the same string, so a client written against vLLM (prompt) and one written against SGLang (text) read the answer unchanged whichever engine served.
Engine support¶
| Backend type | Tokenize | Detokenize | What the router sends |
|---|---|---|---|
vllm | POST /tokenize | POST /detokenize | The client body with model rewritten to the served id |
sglang | POST /v1/tokenize | POST /v1/detokenize | The client body with model rewritten to the served id, after refusing options SGLang would ignore or misrender (see below) |
llamacpp | POST /tokenize | POST /detokenize | {"content", "add_special", "with_pieces"} and {"tokens"}. A messages request is rendered first with POST /apply-template, then the rendered prompt is tokenized (see below) |
mlxcel | POST /tokenize | POST /detokenize | Same as llamacpp, with fewer messages options |
continuumrouter | POST /tokenize | POST /detokenize | The client body with model rewritten to the served id. The downstream router answers in this same shape. A downstream 404 (a router older than these routes) or 501 answers 501 tokenization_not_supported. Any other downstream 4xx that carries this router's error object (string code and type, optional param) passes through with its status, code, type, and param, and its message capped at 256 bytes, so a chained deployment answers the error a direct router does (for example 404 model_not_found, or 400 tokenize_option_unsupported naming the option in param); a 401, 403, or 407 still answers 502, and any other downstream body answers tokenize_upstream_rejected. The call waits 35 seconds, 5 more than the downstream router gives its own engine, so the downstream's 504 arrives rather than a timeout here |
tensorrt-llm | Not supported | Not supported | trtllm-serve registers only an internal /_internal/tokenize route and no detokenize route (checked 2026-10-02 against tensorrt_llm/serve/openai_server.py on main) |
ollama | Not supported | Not supported | The Ollama API documents no tokenize or detokenize route (checked 2026-10-02) |
lmstudio | Not supported | Not supported | The LM Studio REST API documents no tokenize or detokenize route (checked 2026-10-02) |
generic, openai, azure, anthropic, gemini, bedrock | Not supported | Not supported |
The engine path is appended to backends[].url. A base URL that ends in /v1, such as http://host:8000/v1, is read as the API root: one trailing /v1 is dropped first, so vllm, llamacpp, and mlxcel are called at /tokenize and /detokenize and sglang at /v1/tokenize and /v1/detokenize, never at /v1/v1/tokenize. Only a trailing /v1 segment is dropped (/openai and other prefixes are kept), and /v1/messages/count_tokens builds its engine URL the same way.
A messages request to a llamacpp or mlxcel backend makes two calls to the same backend under one circuit admission: POST /apply-template with model (the served id), messages, and the options below, then POST /tokenize with the rendered prompt as content. add_special_tokens defaults to false here, as on vLLM's chat tokenize request, so a template that already emits BOS does not get a second one. Both calls share the 30-second budget, read through the 16 MiB cap, and follow the circuit rule below. On llamacpp, mlxcel, and sglang, an option the engine would ignore or reinterpret is refused with 400 tokenize_option_unsupported naming it, so a request never gets a silently different answer; when other backends serving the model can honor it, the request goes to one of them instead.
| Option | llamacpp | mlxcel | sglang |
|---|---|---|---|
return_token_strs: true (prompt or messages) | Honored (with_pieces: true) | Honored (with_pieces: true) | Refused: SGLang ignores the field and returns no token_strs |
chat_template_kwargs, tools | Passed to the render | Passed to the render | Forwarded to /v1/tokenize |
add_special_tokens on messages | Passed to /tokenize as add_special (default false) | Passed to /tokenize as add_special (default false) | Only false (the default): SGLang strips add_special_tokens before rendering |
add_generation_prompt | Passed to the render | Only true (the default): mlxcel ignores false and appends the generation prompt anyway | Only true (the default): SGLang ignores false and appends the generation prompt anyway |
continue_final_message | Passed to the render; needs add_generation_prompt: false and a final assistant message | Refused: mlxcel ignores it | Refused: SGLang ignores it after a user turn and continues a final assistant turn as a raw prefix that differs from vLLM's render |
| A final assistant message | Needs continue_final_message: true: llama.cpp continues it as a prefix where vLLM opens a new turn | Refused: mlxcel continues it as a prefix with no switch set, and that prefix differs from vLLM's continue_final_message render (with Qwen3 it drops the empty think block the template emits for that turn, which vLLM and llama.cpp keep) | Refused: without continue_final_message SGLang rewrites the final assistant message as a user turn, and with continue_final_message: true it continues it as the same raw prefix as mlxcel |
A content part other than text (image, audio, video) | Refused: the render emits a placeholder string for it, and tokenizing that string is not tokenizing the media | Refused, for the same reason | Refused, for the same reason (and on a multimodal model SGLang fetches the media URL) |
chat_template, and any other option | Refused: llama.cpp accepts a per-request template and ignores it | Refused: mlxcel accepts a per-request template and ignores it | Refused: SGLang accepts a per-request template and ignores it |
Verified engine behavior and fixture coverage:
vllm(verified againstvllm serve0.32.0, commit5df63a720, captured 2026-10-19 withQwen/Qwen3-0.6Binsrc/services/tokenize/fixtures/vllm_0_32_0.md):/tokenizereturns{"count", "max_model_len", "tokens", "token_strs"}(token_strsisnullunlessreturn_token_strs: true, which returns Hugging Face byte-level BPE token strings). Honorsmessageswithadd_generation_prompt,continue_final_message,chat_template_kwargs, andtools; setting both switches totrueor passing an untrusted per-requestchat_templatereturns400./detokenizereturns{"prompt": "..."}and always renders special tokens regardless ofskip_special_tokens. Out-of-vocabularyu32IDs ([999999999], alone or among valid IDs) are dropped by the Hugging Facetokenizersdecoder (200), whereas IDs aboveu32::MAX([5000000000]) return400.- Neither route has a
/v1alias (404). Inregister_vllm_serve_api_routers,attach_tokenize_routeris called unconditionally (onlyGET /tokenizer_inforequires--enable-tokenizer-info-endpoint).
sglang(verified againstsglang.launch_server0.5.22, commitbaa9befa3, captured 2026-10-19 withQwen/Qwen3-0.6Binsrc/services/tokenize/fixtures/sglang_0_5_22.md):/v1/tokenizereturns{"tokens", "count", "max_model_len"}and silently ignoresreturn_token_strs: trueon bothpromptandmessages. Onmessages,chat_template_kwargsandtoolsare honored (matching vLLM, llama.cpp, and mlxcel token IDs), whileadd_generation_prompt: false,add_special_tokens: true,chat_template, andcontinue_final_message: trueafter a user turn are ignored.- A final assistant message with
continue_final_message: trueis continued as a raw prefix (11 tokens on Qwen3, omitting the 4-token empty think block present in vLLM's 15-token render), and withoutcontinue_final_messageis rewritten by_handle_last_assistant_messageinto auserturn (16 tokens, identical to two user turns). /v1/detokenizereturns{"text": "..."}and honorsskip_special_tokens(defaulttrue). Out-of-vocabularyu32IDs are dropped (200), while IDs aboveu32::MAX([5000000000]) return500 InternalServerError(mapped by the router to502 tokenize_upstream_error).
llamacpp(verified againstllama-serverbuild 11344, commitec7630a64, captured 2026-10-02, and build 11424, commit6c59c4007, captured 2026-10-06 withQwen3-0.6B):/tokenizereturns{"tokens": [...]}or{"tokens": [{"id", "piece"}]}whenwith_piecesis set (add_specialdefaults tofalse)./detokenizereturns{"content": "..."}(the router also acceptspromptortext). Neither route has a/v1alias./apply-templateaccepts a chat-completions body and returns{"prompt": "..."}. Honorsadd_generation_prompt,continue_final_message,chat_template_kwargs, andtools; setting both switches totruereturns400, per-requestchat_templateis ignored, and a final assistant message is continued as a prefix even withoutcontinue_final_message.
mlxcel(verified againstmlxcel-server0.7.0, commit1e561f1e, captured 2026-10-06 withmlx-community/Qwen3-0.6B-bf16):/tokenize,/detokenize, and/apply-templateuse the llama.cpp wire shapes and produce byte-identical default,tools, andchat_template_kwargsrenders and token IDs. Incomplete UTF-8 pieces arrive as multi-byte arrays.add_generation_prompt: false,continue_final_message, and per-requestchat_templateare ignored, and a final assistant message is continued as a prefix without any switch set.- Out-of-vocabulary IDs on
/detokenizereturn500(type: server_error), which the router maps to502 tokenize_upstream_error(older mlxcel builds returned200with the ID dropped, e.g.,[999999999]->""). No/v1aliases exist.
continuumrouter: Uses this router's own contract. Downstream routers prior to the release that added these routes return404.tensorrt-llm,ollama, andlmstudio: Marked unsupported based on upstream source and API documentation as of 2026-10-02; the router returns501for these backend types.
Dispatch rules¶
Because token IDs are specific to the exact model that produced them, tokenization routes differ from inference routes:
- Exact model selection: Selects one backend serving the exact requested model with an engine tokenizer (metadata aliases resolve to the backend's canonical ID).
- Single-shot execution: Sends a single request with no retries, no
fallback.fallback_chainshops, no response caching, and no model-experiment resolution (hide_variant_modelsvariants return404). - Backend preference and attribution:
x-backendnarrows selection among capable backends.x-served-backendis returned on upstream200responses and attributed upstream errors (tokenize_upstream_rejected,tokenize_upstream_error, and passthrough downstream router errors/501), but omitted on router-authored refusals, connection failures, timeouts, or unparseable responses. - Access control and rate limits: Enforces
allowed_models,allowed_backends, and global/per-client/per-key rate limits (internaland disabled backends are excluded). Emits no usage records, runs no guardrails, and does not charge per-model token rate limits. - Circuit-breaker isolation (also applied to
/anthropic/v1/messages/count_tokenswith the same 16 MiB cap and 30-second timeout):- Open state: Rejects requests immediately, as on any other route.
- Closed state: Any HTTP response from the engine (
2xx,4xx, or5xx) is neutral; only connection failures and timeouts count as circuit-breaker failures. A2xxtokenizer response does not reset failure counters, and an input-triggered5xxcannot trip the circuit. - Half-open state: Admits requests without consuming a generation probe slot, and HTTP responses do not advance or close the half-open probe. The circuit stays half-open until a generation request succeeds (or reopens immediately on a tokenizer connection failure or timeout).
Status codes¶
| Status | Code | Meaning |
|---|---|---|
200 | The engine's answer in the router shape | |
400 | missing_model, missing_input, conflicting_input, unsupported_prompt_type, invalid_prompt_type, invalid_messages_type, invalid_parameter_type, missing_tokens, invalid_tokens_type, too_many_tokens | The body is not the documented shape. Nothing was sent upstream |
400 | tokenize_option_unsupported | /tokenize was sent with an option (return_token_strs: true on SGLang, or a messages option in the table above) that no capable backend serving the model can honor; param names it. Nothing was sent upstream, unless the refusal is a downstream router's passed through by a continuumrouter backend |
403 | The key's allowed_models or allowed_backends excludes the model | |
404 | No visible backend serves the model, or (model_not_found) the downstream router of a continuumrouter backend does not know it | |
4xx | tokenize_upstream_rejected | The engine rejected the request. The engine's status is kept, except 401, 403, and 407 (next rows); its body is logged, not returned. An engine 429 keeps the engine's Retry-After. A downstream router of a continuumrouter backend whose 4xx body is a router error object is the exception: its code, type, param, and capped message are returned instead |
429, 503 | Every capable backend is at its per-backend limit, or unavailable | |
501 | tokenization_not_supported | Backends serve the model, but none has an engine tokenizer, or the downstream router of a continuumrouter backend cannot tokenize (its 501, or a 404 from a router without these routes) |
502 | tokenize_upstream_error | The engine answered 5xx. On /detokenize the usual cause is a token id outside the model's vocabulary or integer range: llama.cpp and mlxcel (0.7.0 at 1e561f1e and newer) answer 500 for an out-of-vocabulary id, SGLang answers 500 for an id above u32::MAX (where vLLM answers 400, while both vLLM and SGLang answer 200 and drop an out-of-vocabulary u32 id, as older mlxcel builds did), and the router cannot tell that apart from an engine fault because it does not know the vocabulary size. On /tokenize to a vllm or sglang backend the client's chat_template, chat_template_kwargs, tools, and messages reach the engine, and a template that raises can do the same. Because the client's input can cause it, an engine 5xx on either route is never counted against the backend's circuit breaker, so a read key cannot take a healthy backend out of rotation (the circuit rule is under Dispatch rules above) |
502 | tokenize_upstream_error | The engine answered 401, 403, or 407: it refused the router's own backend credential (backends[].api_key), not the client's key, so the status is not passed on |
502 | tokenize_invalid_response | The engine answered 2xx with a body that is not the documented shape |
502 | tokenize_response_too_large | The engine's answer passed 16 MiB |
502 | tokenize_connection_error | The backend could not be reached |
504 | tokenize_timeout | The engine did not answer within 30 seconds |
Example¶
curl -X POST http://localhost:8080/tokenize \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $API_KEY" \
-d '{"model": "Qwen/Qwen3-32B", "prompt": "Hello, world!"}'
curl -X POST http://localhost:8080/detokenize \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $API_KEY" \
-d '{"model": "Qwen/Qwen3-32B", "tokens": [9707, 11, 1879, 0]}'
Decisions (System One)¶
Ask typed questions about a piece of evidence and get typed, probability-bearing answers in one call, using the System One decision protocol that TypeSafe's hosted Jev and the self-hosted Laya (laya-serve), Kev, and Nimble servers speak. A client written against any of those servers works against the router unchanged, under the router's API keys, scopes, rate limits, budgets, and fallback chains.
/v1/systemone is an alias with identical behavior, for clients that hard-code the protocol's own path.
The request's model selects how the decision is answered:
- Passthrough: a name listed under
decisions.targets[]is sent to that System One server (an inlineendpoint, or abackend:reference to a self-hostedbackends[]entry) and its answer is returned unchanged except forusage. - Emulation: any other name is treated as a chat model. The question map is rebuilt as one forced
emit_decisionfunction tool (boolean fornoul, an enum of option ids forchoice, an integer level range forscore), sent through the router's ordinary chat dispatch attemperature: 0without streaming, and the tool arguments are mapped back onto the answer shape. The name resolves as it does on/v1/chat/completions: a metadata alias is dispatched as the canonical id its backend serves, and a model experiment's client-facing name as its assigned variant. The smart-routing virtual model (for exampleauto) is refused with400 smart_routing_not_supported, because this endpoint does not classify requests.
Request Body:
{
"model": "jev",
"state": "Customer: I was charged twice, please refund the second charge.",
"questions": {
"refund": {"type": "noul", "instructions": "Is this a refund request?"},
"team": {
"type": "choice",
"instructions": "Which team should handle it?",
"criteria": {"billing": "Billing and payments", "tech": "Technical support"}
},
"urgency": {
"type": "score",
"instructions": "How urgent is it?",
"criteria": ["low", "normal", "high"]
}
}
}
Request Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
model | string | Yes | A decisions.targets[].name, or a chat model name to emulate on. |
state | string, object, array, or null | Yes | The evidence the questions are about. A string must not be blank; null is allowed only on protocol: llama_cpp targets with lfm2_d1, lfm2_d1_omni, or vllm_d3 families. |
questions | object | Yes | Question id to question. Default target limit 64, configurable 1–256 (max_questions). |
questions.*.type | string | Yes | noul (probability a statement is true), choice (one of a labelled set), score (a position on an ordered scale), or box (normalized bounding box {top, bottom, left, right}; requires protocol: typesafe, typesafe_images: true, and jev-1.14.0 / jev-preview). |
questions.*.instructions | string, object, array, or null | Profile-dependent | What to decide, at most 8 KiB. Explicit profiles preserve omitted/null/structured fields; generic retains required text. |
questions.*.criteria | object or array | For choice and score | choice: option id to description, 1 to 255 options (1 to 52 for openjev). score: level descriptions lowest first, 2 to 10 levels (1 to 10 on typesafe). noul: optional {"true": ..., "false": ...}. |
assets | object | No | Named image assets ({"type": "image", "format": "png", "data_base64": "..."}) referenced from state, instructions, or criteria. Requires protocol: typesafe and typesafe_images: true. |
images / files | array of strings | No | Inline data: URLs for image or audio inputs on protocol: llama_cpp targets (bounded by max_media_bytes, default 4 MiB, max 16 MiB). |
Provider profile details, experimental TypeSafe assets/Box and llama.cpp media are documented in Decision Models. These routes have a 2 MiB JSON body cap; multipart is explicitly unsupported (415), compressed input is not decompressed, unknown fields are rejected, and incompatible fallback hops cannot drop media. Null state requires an explicitly supported llama.cpp family.
The body is validated before any backend call. A model that is missing, blank, or carries leading or trailing whitespace answers 400 invalid_model: the name is never trimmed, because the per-model rate limit charges the exact string in the body and the dispatched name must be that string. A protocol violation (for example a choice with 256 options) answers 422 with code invalid_questions or invalid_state.
Response (passthrough):
{
"model": "jev-1.13.0",
"answers": {
"refund": {"type": "noul", "noul": 0.97},
"team": {
"type": "choice",
"choice": "billing",
"probabilities": {"billing": 0.94, "tech": 0.06},
"confidence": 0.88
},
"urgency": {
"type": "score",
"score": 1.4,
"probabilities": {"0": 0.1, "1": 0.4, "2": 0.5},
"confidence": 0.25
}
},
"usage": {"input_tokens": 62, "output_tokens": 0}
}
model is the id the server resolved (a moving alias such as jev-latest is reported as the pinned version it served). Both usage.input_tokens and usage.output_tokens are what the server reported. TypeSafe output tokens may be nonzero even when their price is zero; the Router does not erase usage. Missing optional confidence remains absent.
Response (emulation):
{
"model": "gpt-4o-mini-2024-07-18",
"answers": {
"refund": {"type": "noul", "noul": 1.0},
"team": {"type": "choice", "choice": "billing", "probabilities": {"billing": 1.0, "tech": 0.0}},
"urgency": {"type": "score", "score": 2.0, "probabilities": {"0": 0.0, "1": 0.0, "2": 1.0}}
},
"usage": {"input_tokens": 180, "output_tokens": 22},
"emulated": true
}
A chat model returns one answer, not a distribution, so an emulated answer carries one-hot probabilities, no confidence, and the response is marked emulated: true. Do not read an emulated 1.0 as calibrated certainty. usage is the chat model's own token usage.
Fallback: a target name is an ordinary model name to fallback.fallback_chains, so a chain can run from a failing real target to another target and end in a chat model the endpoint emulates on (for example jev: [laya, gpt-4o-mini]). The chain advances on the configured trigger conditions: a 429, a 529 (reported as backend unhealthy), a 5xx, a timeout, or a connection failure. A 422 from the target is never retried elsewhere. A response served by a chain hop carries the usual X-Fallback-Used, X-Original-Model, X-Fallback-Model, X-Fallback-Reason, and X-Fallback-Attempts headers.
Access control and accounting:
- Scope and rate limits: Requires the
writescope in blocking mode (403for read-only keys). Per-keyallowed_modelsgates the requestedmodel(including decision target names) before fallback, and per-key/per-model rate limits and control-plane budgets apply. - Backend allow-lists (
allowed_backends): Keys with a non-emptyallowed_backendslist can reach abackend:target only when that backend is in the list, and can never reach an inline-endpoint target (such as hosted Jev) so restricted keys never sendstateoutside their permitted backends. Direct requests to a disallowed target return403; disallowed hops in a fallback chain are skipped without sending a request. - Usage attribution: Recorded on the caller's key under the model that actually served the request: the resolved decision server ID (such as
jev-1.13.0forjev-latest) or the chat model that answered an emulation hop.
Guardrails and request parameters:
- Emulation hops: Evaluated as chat completions before dispatch under the hop model's policy:
- Request parameters: Governed by the composed local and Hub
request_paramspolicy (max_tokensclampsdecisions.emulation.max_tokens; refusals return400 invalid_request_parameter). Thex-continuum-request-paramsresponse header is not emitted on this route. - Input guardrails: Runs the input pre-call check (enforce-mode blocks return
400withx-guardrail-*headers; transforms rewrite the prompt). - Payload caps: Bounded by
decisions.emulation.max_state_bytesforstate(422 state_too_large) and a fixed 64 KiB cap for the serialized question set (422 questions_too_large).
- Request parameters: Governed by the composed local and Hub
- Passthrough targets: Forward
stateas sent without running content guardrails, since the reply is a typed decision object rather than generated text. Route privacy-sensitive traffic to self-hosted targets rather than external hosted targets such as Jev.
Status Codes:
200: Answered (passthrough or emulation)401: Missing or invalid API key (blocking mode)400:modelis missing, blank, or padded with whitespace (invalid_model); the smart-routing virtual model was named (smart_routing_not_supported); the request-parameter policy refused an emulation hop (invalid_request_parameter); or an input guardrail blocked it403: The key lacks thewritescope, or its allow-list excludes the model, the target's backend, or (with anyallowed_backendslist) an inline-endpoint target404: The endpoint is disabled (decisions_disabled), or the name is neither a target nor an emulable chat model415: Unsupported media or content type (for examplemultipart/form-data)422: The request failed validation, or the target rejected it (decision_validation_error), or the state or question set exceeds the emulation cap (state_too_large,questions_too_large)429: A rate limit was exceeded, or the target rate-limited the request (retry-afteris forwarded)502: The target failed or returned a malformed answer, or an emulated chat reply had no usableemit_decisioncall (decision_emulation_failed)503: The target is not available (decision_target_unavailable)504: The target did not answer within itstimeout_ms529: The target reported itself overloaded (retry-afteris forwarded)
Example Request:
curl -X POST http://localhost:8080/v1/decisions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-key" \
-d '{
"model": "jev",
"state": "The deploy failed with exit code 137 after the memory limit was lowered.",
"questions": {
"oom": {"type": "noul", "instructions": "Was the process killed for running out of memory?"}
}
}'
Configure targets and emulation under the decisions section (see the configuration reference).
Sparse Embeddings¶
Generate sparse embeddings for input text using the TEI/Jina-compatible Sparse Embedding API. Sparse embeddings (e.g., SPLADE) preserve lexical information through explicit term weights, complementing dense embeddings for hybrid search.
Request Body:
Request Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
model | string | Yes | ID of the sparse embedding model to use (e.g., naver/splade-v3, naver/splade-cocondenser-ensembledistil) |
input | string or array | Yes | Input text to embed. Can be a single string or an array of strings |
Input Format Examples:
Single string:
Array of strings:
{
"model": "naver/splade-v3",
"input": [
"First text to embed",
"Second text to embed",
"Third text to embed"
]
}
Response:
{
"data": [
{
"index": 0,
"sparse_embedding": {
"indices": [123, 456, 789, 1024, 2048],
"values": [0.5, 0.3, 0.1, 0.8, 0.2]
}
}
],
"model": "naver/splade-v3",
"usage": {
"prompt_tokens": 8,
"total_tokens": 8
}
}
Response Fields:
| Field | Type | Description |
|---|---|---|
data | array | Array of sparse embedding objects |
data[].index | integer | Index of the embedding in the input array |
data[].sparse_embedding | object | The sparse embedding vector |
data[].sparse_embedding.indices | array | Indices of non-zero elements in the vocabulary |
data[].sparse_embedding.values | array | Values at the corresponding indices |
model | string | Model used to generate the embedding (optional) |
usage | object | Token usage information (optional) |
Understanding Sparse Vectors:
A sparse vector only stores non-zero values along with their vocabulary indices. For example:
indices: [123, 456, 789]- positions in the vocabularyvalues: [0.5, 0.3, 0.1]- weights for those terms
This is memory-efficient for high-dimensional vectors with few non-zero elements (typically 100-500 non-zero values out of 30,000+ vocabulary size).
Supported Backends:
| Backend | Endpoint | Notes |
|---|---|---|
| vLLM | /embed_sparse | Supports SPLADE models via OpenAI-compatible server |
| Hugging Face TEI | /embed_sparse | Requires --pooling splade flag |
| Jina AI | Native | Native sparse embedding support |
Status Codes:
200: Sparse embeddings generated successfully400: Invalid request (missing model/input, empty input)401: Invalid API key404: Model not found or doesn't support sparse embeddings429: Rate limit exceeded500: Backend error503: Backend unavailable
Use Cases:
- Hybrid search: Combine dense (semantic) and sparse (lexical) retrieval for better results
- Keyword matching: Exact term matching with learned weights
- Domain-specific retrieval: Better handling of specialized terminology and rare words
- Cross-lingual retrieval: Some SPLADE models support multilingual sparse retrieval
Example Request:
curl -X POST http://localhost:8080/embed_sparse \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-key" \
-d '{
"model": "naver/splade-v3",
"input": "What are the benefits of sparse embeddings for search?"
}'
Example with Multiple Inputs:
curl -X POST http://localhost:8080/embed_sparse \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-api-key" \
-d '{
"model": "naver/splade-v3",
"input": [
"First query text",
"Second query text"
]
}'
Files API¶
The Files API allows you to upload, manage, and use files in chat completions. Uploaded files can be referenced in messages using the image_file content type, and the router automatically resolves these references by injecting the file content.
Upload File¶
Upload a file for use in chat completions.
Form Fields:
| Field | Type | Required | Description |
|---|---|---|---|
file | file | Yes | The file to upload |
purpose | string | Yes | Purpose of the file: vision, assistants, fine-tune, batch, user_data, evals |
Example:
Response:
{
"id": "file-abc123def456",
"object": "file",
"bytes": 12345,
"created_at": 1699061776,
"filename": "image.png",
"purpose": "vision"
}
Status Codes:
200: File uploaded successfully400: Invalid request (missing file, invalid purpose)413: File too large (exceeds configured max_file_size)
List Files¶
Retrieve a list of uploaded files.
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
purpose | string | No | Filter by purpose |
limit | integer | No | Maximum number of files on the page, 1 to 10000 (default 10000); a value outside that range is clamped to it |
order | string | No | desc (default, newest first) or asc, by created_at and then by file id |
after | string | No | Id of the last file of the previous page; the listing continues after it |
Response:
{
"object": "list",
"data": [
{
"id": "file-abc123def456",
"object": "file",
"bytes": 12345,
"created_at": 1699061776,
"filename": "image.png",
"purpose": "vision"
}
],
"has_more": false
}
When files.auth.enforce_ownership is on, a caller that is not an admin lists only its own files, and the owner filter is applied before limit, so a page holds up to limit of the caller's files however many newer files other owners uploaded. has_more is true when further files follow the page: request the next page with after set to the last id returned. An after id that is not in the caller's listing (a deleted file, another owner's file, or an unknown id) returns an empty page with has_more: false. GET /admin/files accepts the same limit, order, and after parameters across all owners.
Get File Metadata¶
Retrieve metadata for a specific file.
Response:
{
"id": "file-abc123def456",
"object": "file",
"bytes": 12345,
"created_at": 1699061776,
"filename": "image.png",
"purpose": "vision"
}
Status Codes:
200: File metadata retrieved404: File not found
Download File Content¶
Download the content of an uploaded file.
Response: Binary file content with appropriate Content-Type header.
Status Codes:
200: File content returned404: File not found
Delete File¶
Delete an uploaded file.
Response:
Status Codes:
200: File deleted successfully404: File not found
Per-Operation Rate Limiting¶
When files.rate_limiting.enabled is set to true, the router enforces separate per-caller token-bucket limits (in requests per minute) across three Files API operation categories:
| Category | Endpoints | Config Field | Default |
|---|---|---|---|
files_upload | POST /v1/files | files.rate_limiting.uploads_per_minute | 10 |
files_download | GET /v1/files/{file_id}/content | files.rate_limiting.downloads_per_minute | 100 |
files_operations | GET /v1/files, GET /v1/files/{file_id}, HEAD /v1/files/{file_id}, DELETE /v1/files/{file_id} | files.rate_limiting.operations_per_minute | 1000 |
Buckets are isolated per caller using the SHA-256 hash of the presented API key (Authorization: Bearer or x-api-key), the authenticated user ID, or the client IP. Every Files API response includes x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset, and their -requests variants (x-ratelimit-limit-requests, x-ratelimit-remaining-requests, x-ratelimit-reset-requests). When a caller exceeds a category limit, the router responds with 429 Too Many Requests, a Retry-After header, and an OpenAI error envelope whose error.details.limit_type names the exceeded category (files_upload, files_download, or files_operations).
Error Bodies¶
Every Files API error uses the OpenAI envelope: an error object with message, type, and code.
Client-caused failures name what the request got wrong, including the file_id the caller supplied:
{
"error": {
"message": "file not found: file-abc123def456",
"type": "invalid_request_error",
"code": "file_not_found"
}
}
Server-side failures return a fixed message instead of the router's internal diagnostics. The type and code still distinguish the cause, so a client that branches on them is unaffected:
{
"error": {
"message": "storage error: the file operation could not be completed",
"type": "server_error",
"code": "storage_error"
}
}
The full diagnostic (the absolute path under files.storage_path, the temporary upload filename, and the underlying operating-system error) is written to the router log at ERROR level and is never placed in a response body. It is deliberately not exposed through a configuration switch: an operator debugging a storage failure reads the log, and a caller has no use for the router's filesystem layout. The same applies to the /admin/files* endpoints and to the file_id resolution failures surfaced on /v1/chat/completions and /anthropic/v1/messages.
code | Status | Message |
|---|---|---|
storage_error | 500 | Fixed: storage error: the file operation could not be completed |
io_error | 500 | Fixed: io error: the file operation could not be completed |
service_unavailable | 503 | Fixed: files api is disabled |
| everything else | 4xx | Describes the request, including the file_id |
File Resolution in Chat Completions¶
The router automatically resolves file references in chat completion requests. When a message contains an image_file content block, the router:
- Validates the file ID format
- Loads the file content from storage
- Converts the file to a base64 data URL
- Replaces the
image_fileblock with animage_urlblock
Supported File Types¶
File resolution in chat completions supports the following file types:
| File Type | MIME Type | Support |
|---|---|---|
| PNG | image/png | All backends |
| JPEG | image/jpeg | All backends |
| GIF | image/gif | All backends |
| WebP | image/webp | All backends |
application/pdf | OpenAI, Anthropic | |
| Plain Text | text/plain | Anthropic |
Note: PDF and plain text support is available for Anthropic backends (and PDF for OpenAI). The file transformers automatically convert document files to the appropriate format for each backend (OpenAI uses
fileblocks for PDF, Anthropic usesdocumentblocks for both PDF and plain text). Non-image and non-document files will return a400 Bad Requesterror with a helpful message indicating the supported file types.
Request with File Reference:
{
"model": "gpt-5.6-sol",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "What's in this image?"},
{"type": "image_file", "image_file": {"file_id": "file-abc123def456"}}
]
}
]
}
Transformed Request (sent to backend):
{
"model": "gpt-5.6-sol",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "What's in this image?"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}
]
}
]
}
File Resolution Errors:
| Error | Status | Description |
|---|---|---|
| Invalid file ID format | 400 | File ID must start with file- |
| File not found | 404 | Referenced file does not exist |
| Too many file references | 400 | Request contains more than 20 file references |
| Resolution timeout | 504 | File resolution took longer than 30 seconds |
Supported MIME Types:
image/png- All backendsimage/jpeg- All backendsimage/gif- All backendsimage/webp- All backendsapplication/pdf- OpenAI, Anthropic (max 32MB, 100 pages)text/plain- Anthropic (max 32MB)
Anthropic Native API¶
Continuum Router provides native Anthropic API endpoints that allow clients to use Anthropic's API format directly while still benefiting from the router's load balancing, failover, and multi-backend routing capabilities.
A streamed message that the client cancels, that times out, that a guardrail blocks, or that fails mid-stream is charged for the tokens generated up to the cut: one usage record with that exit's outcome, from the engine's message_delta usage when it arrived and otherwise the message_start input beside the router's delta count. A client that leaves while the upstream then stalls or fails is recorded as client_cancelled, not as the timeout or the error.
Messages API¶
Send messages using Anthropic's native API format.
The same handler is also mounted at the root level as POST /v1/messages, so a client whose base URL is the server root (without the /anthropic prefix) reaches the Anthropic Messages ingress without configuration changes. Token counting has the same root-level alias: POST /v1/messages/count_tokens.
Headers:
| Header | Required | Description |
|---|---|---|
x-api-key or Authorization: Bearer | Yes* | API key for authentication (required in blocking mode); either header form is accepted, and Authorization: Bearer takes precedence when both are present |
anthropic-version | No | API version (e.g., 2023-06-01). Forwarded to native Anthropic backends |
anthropic-beta | No | Beta features (e.g., prompt-caching-2024-07-31). Forwarded to native Anthropic backends |
API version policy¶
Messages and token counting use the same anthropic-version policy, including their root-level aliases.
- Omitted: native Anthropic dispatch uses the backend's configured
anthropic_version, or2023-06-01when unset. This fallback is a router convenience. - Present: each comma-separated or repeated value must be a real
YYYY-MM-DDdate in 2020–2099. Surrounding whitespace is accepted, and client values are forwarded to native Anthropic backends. - Invalid: an empty value, malformed date, or invalid token returns HTTP 400
invalid_request_errorbefore backend dispatch, including when token counting uses a local estimate.
Request Body:
{
"model": "claude-sonnet-4-5-20250929",
"max_tokens": 1024,
"messages": [
{
"role": "user",
"content": "Hello, Claude!"
}
],
"stream": false
}
Request Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | Yes | Model identifier |
max_tokens | integer | Yes | Maximum tokens to generate |
messages | array | Yes | Array of message objects |
system | string/array | No | System prompt (string or array of content blocks) |
stream | boolean | No | Enable streaming (default: false) |
temperature | number | No | Sampling temperature (0-1) |
top_p | number | No | Nucleus sampling parameter |
top_k | integer | No | Top-k sampling parameter |
stop_sequences | array | No | Stop sequences |
metadata | object | No | Request metadata |
tools | array | No | Tool definitions for function calling |
tool_choice | object | No | Tool choice configuration |
thinking | object | No | Extended thinking configuration: a type of enabled, adaptive, or disabled, with an optional budget_tokens. See Extended thinking toward OpenAI-compatible backends |
output_config | object | No | Claude 4.6+ reasoning effort: an effort of low, medium, high, xhigh, or max. Takes precedence over thinking.budget_tokens; see Extended thinking toward OpenAI-compatible backends |
Response (non-streaming):
{
"id": "msg_01XFDUDYJgAACzvnptvVoYEL",
"type": "message",
"role": "assistant",
"content": [
{
"type": "text",
"text": "Hello! How can I help you today?"
}
],
"model": "claude-sonnet-4-5-20250929",
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": {
"input_tokens": 12,
"output_tokens": 15,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0
}
}
Backend Routing:
The router automatically transforms requests based on the target backend:
| Backend Type | Transformation |
|---|---|
| Anthropic | Pass-through with native API format |
| Gemini | Direct transformation to Gemini format |
| OpenAI/vLLM/Ollama | Transform to OpenAI format, then transform response back |
OpenAI / Azure OpenAI with responses_only model | Transform to Responses API format, dispatch to /v1/responses, transform response back to Anthropic Messages JSON or Anthropic SSE events |
Prompt caching¶
The router reports prompt-cache usage but never fabricates it. cache_read_input_tokens and cache_creation_input_tokens appear in usage only when the serving upstream reports cache use; an upstream that reports nothing yields input_tokens and output_tokens alone.
- Native Anthropic and Bedrock: the client's
cache_controlblocks are forwarded unchanged in the native body. - vLLM and SGLang: prefix caching runs automatically and needs no per-request breakpoint. Enable
--enable-prompt-tokens-detailson vLLM or--enable-cache-reporton SGLang so the engine reports cached tokens and the router maps them tocache_read_input_tokens. - Other OpenAI-wire backends: a converted request drops client
cache_controlbreakpoints. Setbackends[].forward_cache_control: trueon a backend that accepts the field, such as OpenRouter, LiteLLM, or a peer router fronting native Anthropic, to copy each breakpoint on system text, message text, image, and document text parts onto the OpenAI-wire part the conversion produces. The default istruefortype: continuum-routerandfalsefor every other type, because strict cloud endpoints and most local engines reject or ignore the field. Breakpoints ontool_use,tool_result,thinking, and tool definitions are not forwarded.
The router's own anthropic_auto_cache_control injection is a separate mechanism: it adds one breakpoint to a native Anthropic system prompt when the client sent none.
Extended thinking toward OpenAI-compatible backends¶
Anthropic and Bedrock backends receive thinking and output_config unchanged. Every OpenAI-compatible backend receives an OpenAI reasoning_effort value derived from them instead, and the Anthropic-native fields never reach the wire. A Gemini backend is a third case and the bands below do not apply to it: it receives budget_tokens as a generation_config.thinking_config.thinking_budget token count, and no output_config at all.
The derivation runs only when the request carries a thinking block. A thinking: {"type": "enabled", "budget_tokens": N} maps through these bands:
budget_tokens | reasoning_effort |
|---|---|
N <= 4096 | low |
4097 <= N <= 10240 | medium |
N > 10240 | high |
A thinking: {"type": "enabled"} with no budget_tokens means medium. With no output_config.effort to override it, a type of adaptive or disabled produces no reasoning_effort at all, so the backend applies its own default.
An output_config.effort takes precedence whenever the request carries one, whatever the thinking.type says: low, medium, high, and xhigh map level for level, max becomes xhigh, and any other string is logged and treated as high. A cross-provider fallback hop differs on two points: there a disabled thinking config suppresses the effort instead of yielding to it, and an unrecognized effort string falls through to the budget instead of becoming high.
thinking.budget_tokens validation on this endpoint¶
A thinking: {"type": "enabled"} block is validated against the Anthropic Messages contract before dispatch, since the router rewrites or drops the field on every backend arm and no upstream ever sees the raw value. These are rejected with 400 invalid_request_error:
budget_tokensmissing.budget_tokensbelow 1024.budget_tokensat or abovemax_tokens, unless theanthropic-betaheader carries aninterleaved-thinking-token, under which Anthropic allows the budget to exceedmax_tokens.
Thinking blocks in responses¶
Reasoning text from an OpenAI-compatible backend (reasoning_content or reasoning) or a Responses API reasoning item becomes a thinking block, or thinking_delta events when streaming, only when the request set thinking.type to enabled or adaptive. With no thinking parameter or with disabled, the reasoning text is dropped and the response starts with its text or tool-use block, as on the Anthropic API. Backends such as DeepSeek and newer vLLM report reasoning on every completion, so this rule applies whatever the backend returns.
Token accounting is independent of thinking-block visibility. For an OpenAI-compatible upstream, Anthropic usage.output_tokens retains the inclusive completion_tokens count: a completion of 20 tokens with 20 reasoning-detail tokens is still 20 output tokens, not 40. Upstream finish_reason: "length" becomes stop_reason: "max_tokens" even when reasoning consumed the entire completion and visible text is empty, in buffered and streaming responses.
A Gemini backend signs its reasoning with an opaque thought signature, and Gemini 3 rejects the next turn of a function-calling loop when the replayed call lacks it. The router returns that signature as the signature of a thinking block (a signature_delta event when streaming) placed right before the tool_use it signs, with empty thinking text when Gemini reported no reasoning text. This block is emitted whether or not the request enabled thinking. When the client replays the block, the router writes the signature back onto the matching function call; no other backend receives it.
Note: A router-produced
thinkingblock is not signed by Anthropic. A thinking block from a backend that supplies no signature has nosignaturefield, and a Gemini signature is verifiable only by the Gemini backend that issued it. Replay such a transcript through the router, not directly to the Anthropic API.
Both transforms on this endpoint apply the same bands, the Chat Completions one and the Responses API one used for responses_only models, and so does a cross-provider fallback hop onto an OpenAI-compatible backend. The bands are the inverse of the reasoning_effort to budget_tokens mapping the router applies in the other direction. See The thinking config becomes reasoning_effort for the hop side and for how the level is fitted to what a specific target accepts.
Transparent Responses-API routing¶
When the requested model is marked responses_only: true in model-metadata.yaml or the built-in OpenAI registry (e.g., gpt-5-pro, gpt-5.2-pro, gpt-5.4-pro, gpt-5.5-pro), the router transparently routes the Anthropic-formatted request to the upstream /v1/responses endpoint instead of /v1/chat/completions:
- The request is converted from Anthropic Messages format into the Responses API shape before dispatch.
- The upstream response is converted back into Anthropic Messages JSON (non-streaming) or the Anthropic SSE event sequence (streaming).
- Tool-call round-trips, web-search emulation, and Unix-socket transports all honor the flag.
The dispatch is transparent: clients keep targeting /anthropic/v1/messages and continue to receive Anthropic-shaped responses. Only OpenAI and Azure OpenAI backends serve these models; pairing a responses_only model with another backend type produces a 400 invalid_request_error (Anthropic-shaped on this surface) before any upstream call. See Responses-API-only Models for the full list, configuration syntax, and how to mark new models.
File references and resolution limits¶
Image and document blocks may point at a file uploaded through the Files API instead of carrying inline base64:
Every such reference is loaded and base64-encoded into the request before dispatch, including references nested inside a tool_result content array. Each reference gets its own copy of the encoded data, so the cost of a request grows with the number of references rather than with the number of distinct files. The following per-request limits apply:
| Limit | Value | On violation |
|---|---|---|
| References per request | 100 | 400 invalid_request_error, evaluated before any file is read |
| Aggregate referenced bytes | 32 MiB | 400 invalid_request_error; counted once per reference, so referencing one 10 MiB file four times costs 40 MiB |
| File loads in flight | 8 | No error; additional loads wait for a slot |
| Total resolution time | 30 seconds | 504 |
A single referenced file is additionally capped at 10 MiB.
The reference cap is intentionally higher than the 20-reference limit on /v1/chat/completions. The Messages API replays the whole conversation transcript on every turn, so a long multimodal session legitimately accumulates references that a single chat-completions call never would. Base64 expands the raw bytes by roughly 4/3, so the 32 MiB budget corresponds to an emitted ceiling of about 43 MiB.
Token Counting¶
Count the number of tokens in a message request payload.
Request Body:
{
"model": "claude-sonnet-4-5-20250929",
"messages": [
{
"role": "user",
"content": "Hello, how are you?"
}
],
"system": "You are a helpful assistant."
}
Response:
Tiered Token Counting¶
Token counting uses different strategies based on the backend type:
| Backend Type | Strategy | Accuracy |
|---|---|---|
| Anthropic | Native /v1/messages/count_tokens API proxy | Exact |
| llama.cpp, mlxcel | Backend /tokenize endpoint proxy | Exact |
| Continuum Router (chained) | Downstream router's /tokenize | Whatever the downstream answers; this router's character-based estimate when the downstream cannot tokenize (for example its 501, or a 404 from a release before these routes) |
| vLLM | Backend /tokenize endpoint proxy | Exact |
| SGLang | Backend /v1/tokenize endpoint proxy (not the bare /tokenize path) | Exact |
| Others | Character-based estimation (~4 chars/token) | Approximate |
An engine /tokenize call that fails (a non-2xx status, an unreadable answer, an answer past 16 MiB, a connection failure, or a timeout after 30 seconds) is answered with the character-based estimate below. The call follows the circuit rule of the /tokenize route: an engine answer of any status is neutral for the backend's circuit and only a connection failure or timeout counts.
Note: For backends that don't support native tokenization, the router uses a character-based estimation of approximately 4 characters per token. This provides a reasonable approximation for planning purposes but may not match the exact token count used by the model.
Models List¶
List available models in Anthropic API format.
Response:
{
"data": [
{
"id": "claude-sonnet-4-5-20250929",
"type": "model",
"display_name": "Claude Sonnet 4",
"created_at": "2025-05-14T00:00:00Z"
},
{
"id": "claude-opus-4-5-20251101",
"type": "model",
"display_name": "Claude Opus 4",
"created_at": "2025-05-14T00:00:00Z"
}
],
"has_more": false,
"first_id": "claude-sonnet-4-5-20250929",
"last_id": "claude-opus-4-5-20251101"
}
Claude Code Compatibility¶
The Anthropic Native API includes full compatibility with Claude Code and other Anthropic API clients that require advanced features.
Prompt Caching¶
Prompt caching is fully supported through the cache_control field on content blocks:
{
"model": "claude-sonnet-4-5-20250929",
"max_tokens": 1024,
"system": [
{
"type": "text",
"text": "You are a helpful coding assistant with extensive knowledge...",
"cache_control": {"type": "ephemeral"}
}
],
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "Previous context...",
"cache_control": {"type": "ephemeral"}
},
{
"type": "text",
"text": "Current question"
}
]
}
]
}
Supported cache_control locations:
- System prompt text blocks
- User message text blocks
- User message image blocks
- Tool definitions
- Tool use blocks
- Tool result blocks
Beta Features Header¶
The anthropic-beta header is forwarded to native Anthropic backends, enabling beta features:
curl -X POST http://localhost:8080/anthropic/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-H "anthropic-beta: prompt-caching-2024-07-31,interleaved-thinking-2025-05-14" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-4-5-20250929",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Hello"}]
}'
Cache Usage in Streaming¶
When streaming with native Anthropic backends, cache usage information is included in the message_start event:
{
"type": "message_start",
"message": {
"id": "msg_01XFDUDYJgAACzvnptvVoYEL",
"type": "message",
"role": "assistant",
"content": [],
"model": "claude-sonnet-4-5-20250929",
"usage": {
"input_tokens": 2159,
"cache_creation_input_tokens": 2048,
"cache_read_input_tokens": 0
}
}
}
Interleaved Thinking¶
Extended thinking with interleaved output is supported in streaming mode. When the model produces thinking and text content alternately, the streaming events properly represent this interleaved structure with appropriate content_block_start, content_block_delta, and content_block_stop events for each block.
Admin API¶
Administrative routes are mounted under /admin when the admin feature is compiled. Without admin.auth, they fail closed with 401 Unauthorized; an auth block without method defaults to api_key. Only an explicit admin.auth.method: none enables unauthenticated development access.
Common read operations:
GET /admin/capabilities
GET /admin/health
GET /admin/backends
GET /admin/models
GET /admin/config/full
GET /admin/config/hot-reload-status
GET /admin/stats
Mutation, schema, import/export, API-key, prompt, cache, smart-routing, guardrail, Files, ACP, and feature-gated control-plane routes are documented in the Admin REST API reference. That reference also records the current reload boundaries and the configuration endpoints that must not be used as if they applied a pending change.
Error Handling¶
Error Response Format¶
All errors follow a consistent JSON structure:
{
"error": {
"message": "Human-readable error description",
"type": "error_type_identifier",
"param": null,
"code": "error_code_identifier",
"details": {
"additional": "context information"
}
}
}
code is a string or null, never the HTTP status; the status stays on the HTTP status line. param is a string or null and is present even when empty. See Error handling for the full list of router-authored codes.
Error Types¶
| Type | HTTP Code | Description |
|---|---|---|
bad_request | 400 | Invalid request format or parameters |
unauthorized | 401 | Authentication required (blocking auth mode) |
forbidden | 403 | Access denied (insufficient scope or ownership) |
model_not_found | 404 | Requested model not available |
rate_limit_exceeded | 429 | Rate limit exceeded |
internal_error | 500 | Router internal error |
bad_gateway | 502 | Backend connection/response error |
service_unavailable | 503 | All backends unhealthy |
gateway_timeout | 504 | Backend request timeout |
Example Error Responses¶
Model Not Found:
{
"error": {
"message": "Model 'invalid-model' not found on any healthy backend",
"type": "model_not_found",
"param": null,
"code": "model_not_found",
"details": {
"requested_model": "invalid-model",
"available_models": ["gpt-5.6-sol", "gpt-5.6-luna", "llama2"]
}
}
}
Backend Error:
{
"error": {
"message": "Failed to connect to backend 'local-ollama'",
"type": "bad_gateway",
"param": null,
"code": "upstream_error",
"details": {
"backend": "local-ollama",
"backend_error": "Connection refused"
}
}
}
Service Unavailable:
{
"error": {
"message": "All backends are currently unhealthy",
"type": "service_unavailable",
"param": null,
"code": "service_unavailable",
"details": {
"healthy_backends": 0,
"total_backends": 3
}
}
}
Rate Limiting¶
When rate_limiting is enabled in the configuration, the router enforces token-bucket limits across several dimensions:
- Per-client (IP address) limits
- Per-API-key limits, including per-key overrides
- Per-user and per-team limits
- Per-backend and global limits
- Model-specific limits
- Token budgets (
tokens_per_minuteandtokens_per_day)
Requests over a limit receive 429 Too Many Requests with a Retry-After header; responses within limits carry X-RateLimit-* headers that report only the caller's own per_api_key bucket. GET /v1/quota is the pull channel for the same identity-bound limits and can be polled without consuming them. It reports the per-key, per-user, per-team, in-flight, long-window budget, and Hub tier values that apply, withholds the shared global, per_client, per_model, and per_backend levels, and carries no rate-limit headers of its own. See Caller Quota and Rate Limiting for configuration, client identification, bypass options, and storage backends.
Streaming¶
Server-Sent Events (SSE)¶
When stream: true is specified, responses are sent as Server-Sent Events with:
Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive
SSE Format¶
data: {"id":"chatcmpl-123","object":"chat.completion.chunk",...}
data: {"id":"chatcmpl-123","object":"chat.completion.chunk",...}
data: [DONE]
SSE Compatibility¶
The router supports multiple SSE formats for maximum compatibility:
- Standard Format:
data: {...} - Spaced Format:
data: {...} - Mixed Line Endings: Handles
\r\n,\n, and\r - Empty Lines: Properly processes chunk separators
Streaming Tool Calls¶
When a model calls tools during a streamed completion, each call arrives as a tool_calls array inside choices[0].delta. The opening chunk for a call carries id, type, and function.name; the chunks that follow carry only function.arguments fragments, which the client concatenates.
tool_calls[].index is the 0-based ordinal of the tool call within the response. The first tool call of a response is always 0, the second 1, and so on. It is a slot in the client's tool_calls array and is unrelated to whatever position the call occupied in the upstream provider's own content structure: Anthropic numbers content blocks, so a tool_use block preceded by a thinking block and a text block is block 2, and Gemini numbers parts within a single stream event. The router re-numbers both, so neither upstream number reaches the client. This matters because OpenAI-compatible clients such as the Vercel AI SDK use the field as a literal array slot: an upstream block index would leave a hole in the array and the stream would fail to assemble at the end of the turn.
Every argument chunk repeats the index its opening chunk used, which is what lets parallel tool calls interleave safely:
data: {"choices":[{"delta":{"content":"Checking the weather."},"index":0,"finish_reason":null}]}
data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"toolu_01","type":"function","function":{"name":"get_weather","arguments":""}}]},"index":0,"finish_reason":null}]}
data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"city\":\"Seoul\"}"}}]},"index":0,"finish_reason":null}]}
data: {"choices":[{"delta":{"tool_calls":[{"index":1,"id":"toolu_02","type":"function","function":{"name":"get_weather","arguments":""}}]},"index":0,"finish_reason":null}]}
data: {"choices":[{"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\"city\":\"Busan\"}"}}]},"index":0,"finish_reason":null}]}
data: {"choices":[{"delta":{},"index":0,"finish_reason":"tool_calls"}]}
data: [DONE]
Two details are easy to confuse:
choices[].indexandtool_calls[].indexare different numbers. The first is the choice ordinal, which is0for a single-choice completion; the second is the tool call ordinal.- Non-streaming responses carry no
indexontool_callsat all. There, the position in the array is the order of the calls, and nothing needs to be reassembled.
Connection Management¶
- Keep-Alive: Connections are kept open during streaming
- Timeouts: 5-minute timeout for long-running requests
- Error Handling: Partial responses include error information
- Client Disconnection: Gracefully handles client disconnects
Examples¶
Basic Chat Completion¶
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.6-luna",
"messages": [
{"role": "user", "content": "Hello, how are you?"}
]
}'
Streaming Chat Completion¶
curl -X POST http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.6-luna",
"messages": [
{"role": "user", "content": "Write a short story"}
],
"stream": true,
"max_tokens": 200
}'
Text Completion with Parameters¶
curl -X POST http://localhost:8080/v1/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-5.6-luna",
"prompt": "The future of AI is",
"max_tokens": 50,
"temperature": 0.8,
"top_p": 0.9
}'
Check Backend Status¶
Monitor Service Health¶
List Available Models¶
Python Client Example¶
import requests
import json
# Configure the client
BASE_URL = "http://localhost:8080"
def chat_completion(messages, model="gpt-5.6-luna", stream=False):
"""Send a chat completion request"""
response = requests.post(
f"{BASE_URL}/v1/chat/completions",
headers={"Content-Type": "application/json"},
json={
"model": model,
"messages": messages,
"stream": stream,
"temperature": 0.7
},
stream=stream
)
if stream:
# Handle streaming response
for line in response.iter_lines():
if line:
line = line.decode('utf-8')
if line.startswith('data: '):
data = line[6:] # Remove 'data: ' prefix
if data == '[DONE]':
break
try:
chunk = json.loads(data)
content = chunk['choices'][0]['delta'].get('content', '')
if content:
print(content, end='', flush=True)
except json.JSONDecodeError:
continue
print() # New line after streaming
else:
# Handle non-streaming response
result = response.json()
return result['choices'][0]['message']['content']
# Example usage
messages = [
{"role": "user", "content": "Explain machine learning in simple terms"}
]
print("Streaming response:")
chat_completion(messages, stream=True)
print("\nNon-streaming response:")
response = chat_completion(messages, stream=False)
print(response)
JavaScript/Node.js Client Example¶
const fetch = require('node-fetch');
const BASE_URL = 'http://localhost:8080';
async function chatCompletion(messages, options = {}) {
const response = await fetch(`${BASE_URL}/v1/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: options.model || 'gpt-5.6-luna',
messages: messages,
stream: options.stream || false,
temperature: options.temperature || 0.7,
...options
})
});
if (options.stream) {
// Handle streaming response
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') return;
try {
const parsed = JSON.parse(data);
const content = parsed.choices[0]?.delta?.content;
if (content) {
process.stdout.write(content);
}
} catch (e) {
// Ignore JSON parse errors
}
}
}
}
console.log(); // New line
} else {
const result = await response.json();
return result.choices[0].message.content;
}
}
// Example usage
const messages = [
{ role: 'user', content: 'What is the meaning of life?' }
];
// Streaming
console.log('Streaming response:');
await chatCompletion(messages, { stream: true });
// Non-streaming
console.log('\nNon-streaming response:');
const response = await chatCompletion(messages);
console.log(response);
Agent-to-Agent (A2A) Protocol¶
When a2a.enabled is true in config.yaml, Continuum Router exposes the standard A2A Agent Card discovery endpoint and JSON-RPC 2.0 task execution endpoint. When the a2a section is absent or a2a.enabled is false, both endpoints return 404 Not Found.
a2a:
enabled: true
name: "Continuum Router"
description: "High-performance LLM API router with A2A protocol support"
default_model: "gpt-5.6-luna"
max_tasks: 1000
task_ttl: "1h"
Agent Card Discovery (GET /.well-known/agent.json)¶
Returns the A2A Agent Card JSON describing the router's display name, /a2a endpoint URL, crate version (CARGO_PKG_VERSION), capabilities (streaming: true, stateTransitionHistory: true), supported authentication schemes (Bearer, ApiKey), default input/output modes, and advertised skills.
JSON-RPC 2.0 Task Endpoint (POST /a2a)¶
Accepts JSON-RPC 2.0 requests and enforces the router's API key / Bearer authentication and per-key allowed_models and allowed_backends policies (401 Unauthorized when credentials are missing or invalid in blocking mode; 403 Forbidden when the requested model or serving backend is outside the key's allow-list).
| Method | Description |
|---|---|
message/send | Execute a task synchronously through the router's chat completion proxy pipeline (or register in submitted state when configuration.blocking is false) and return the Task object (submitted, working, completed, failed, or canceled). |
message/stream | Execute a task with Server-Sent Events (text/event-stream), emitting JSON-RPC 2.0 status-update and incremental artifact-update events until final completion or cancellation. |
tasks/get | Retrieve the current status, generated artifacts, and message history for an existing task ID. |
tasks/cancel | Cancel an active (submitted or working) task and transition its status to canceled. |
For deployment configuration and admin endpoint usage, see the Admin REST API Reference. For runtime monitoring, see the Metrics and Error Handling guides.