Skip to content

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

Base URL

http://localhost:8080

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/messages and /anthropic/v1/messages/count_tokens (and their root-level aliases /v1/messages and /v1/messages/count_tokens)
  • /v1/embeddings, /v1/rerank, and /embed_sparse
  • /tokenize and /detokenize (and their /v1 aliases)
  • /v1/images/generations, /v1/images/edits, and /v1/images/variations
  • /v1/audio/transcriptions, /v1/audio/translations, and /v1/audio/speech
  • /v1/realtime WebSocket 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:

  1. User-routable: The backend is enabled (enabled: true) and not internal (internal: false).
  2. Allow-listed: The backend is permitted by the API key's allowed_backends list, when one is configured.
  3. 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: gemini backend (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/edits and /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.
  4. 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/realtime sessions: 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/capabilities advertises backend_preference_header_v1 in its capabilities array when the router supports x-backend (see Admin REST API reference).
  • Via Inference API: Send x-backend on a normal inference request and check for x-served-backend on a successful (200) non-cached response. A present x-served-backend header confirms support; comparing its value with x-backend shows 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.

GET /health

Response:

{
  "status": "ok",
  "service": "continuum-router",
  "version": "2.0.0"
}

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.

GET /health/ready

Ready response:

{
  "status": "ready",
  "service": "continuum-router",
  "routable_backend_count": 2
}

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 active
  • 503: No enabled, non-internal backend is active

Version

Return the running router version.

GET /version

Response:

{
  "version": "2.0.0"
}

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 empty dimensions list
  • 401: Missing or invalid key in blocking mode
  • 403: The key's scopes lack read
  • 429: The shared global or per_client margin refused the read
  • 503: rate_limiting.redis.fallback_to_memory: false and 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.

GET /v1/models

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 successfully
  • 503: 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 answers 200 with available: false for it, and GET /admin/models shows it as degraded. /v1/models/extended, /v1/models/refresh, and /anthropic/v1/models apply the same rule.
  • Backend Aliases: Each aliases entry in an enabled backend's model_configs is listed as its own entry, with the canonical model's backends and 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_models list 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.yaml and the model_aliases section (haiku, sonnet, exact, and the others) are not listed. GET /admin/models reports each declared alias in the canonical entry's aliases.
  • Vendor Attribution: When a backend sends no owned_by for a vendor/model id, as OpenRouter does for every entry, owned_by is the id's vendor namespace (anthropic/claude-haiku-4.5 reports anthropic). 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 as chat, vision, and tool.
    • supported_parameters: the chat request parameters the router carries to the backend, from temperature, top_p, max_tokens, presence_penalty, frequency_penalty, top_k, min_p, logprobs, and top_logprobs. It is derived from each serving backend's conversion (a local engine or raw OpenAI-compatible backend carries all of them; api.openai.com and Gemini drop top_k and min_p; Anthropic and Bedrock carry temperature, top_p, max_tokens, and top_k, or only max_tokens for a Claude model that refuses sampling fields; Bedrock Converse drops top_k as well), and a parameter is listed only when every backend serving the model carries it. A model whose capabilities list neither chat nor the text output modality an upstream catalog reports (an image or embedding model, say) has no supported_parameters. A model whose own request shape narrows the set further (for example a Claude request with reasoning, which drops temperature) is not reflected.

    When no model-metadata.yaml entry sets limits, context_length comes from the context windows the serving engines report (max_model_len or context_length on 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 as null; a model without metadata or engine reports carries only id, object, created, owned_by, and supported_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.

GET /v1/models/extended

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) and context_length (aggregators such as OpenRouter), when present, are rewritten to metadata.limits.context_window, so the object never states two windows. That window is the operator's configured limits when 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 published context_window is 0 (not applicable).
  • Every other upstream key stays verbatim. In particular an aggregator's pricing (USD per token, as strings) and top_provider.max_completion_tokens are left as the upstream sent them, while metadata.pricing (USD per 1M tokens) and metadata.limits.max_output carry the router's values folded across backends. See Upstream vendor and metadata.

Status Codes:

  • 200: Models retrieved successfully
  • 503: 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.

POST /v1/models/query

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:

  • capability is an accepted spelling of capabilities and also takes an array. A body with both spellings returns 400 (invalid_query).
  • Unknown fields are rejected with 400 (invalid_query), so a misspelled filter cannot silently return every model.
  • capabilities accepts at most 32 entries of 1 to 64 bytes each. Matching is exact and case-sensitive.
  • q must be 1 to 256 bytes.
  • limit must be 1 to 1000.
  • Body size is capped at 16 KiB; a larger body returns 413.
  • Scope: the route requires the read API-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, code invalid_query)
  • 413: The request body exceeds 16 KiB
  • 503: 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.

GET /v1/models/proxy

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 returned
  • 502: Backend connection or gateway failure
  • 503: 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.

POST /v1/models/refresh

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 returned
  • 429 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.

GET /v1/models/{model}

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 returned
  • 404: Model does not exist in any configured backend

Features:

  • OpenAI-Compatible: Response format matches OpenAI API with additional extension fields
  • Health-Aware: The available field 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:

curl http://localhost:8080/v1/models/gpt-5.6-sol

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.

POST /v1/chat/completions

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's allowed_models and allowed_backends, Hub and policy narrowing, and finally the client's provider filters. 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, and order naming a backend outside the candidate set has no widening effect. A filter that empties a non-empty candidate set, such as an only whose intersection is empty, answers 400 invalid_request_error naming the field (provider.only, provider.ignore, or provider.max_price) and dispatches nothing. A set that was already empty before the filters keeps the ordinary 503.
  • x-backend remains a first-attempt pin and wins over provider. Retries after that first attempt apply the provider object again.
  • A request that carries a provider object never joins another request's in-flight dispatch. Deduplication keys on the dispatched body, and the router removes provider from 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 provider object to a backend that speaks it through backends[].request_extensions.body.overrides.provider, which is applied at dispatch after the client key is removed. request_extensions.body.defaults.provider cannot 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 successfully
  • 400: Invalid request format or parameters
  • 404: Model not found on any healthy backend
  • 502: Backend connection error
  • 504: Request timeout
  • 503: 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 reasoning format to flat reasoning_effort format; removes reasoning parameters for models that don't support them
  • Transparent Responses-API routing: Models marked responses_only: true (such as gpt-5-pro, gpt-5.2-pro, gpt-5.4-pro, and gpt-5.5-pro) are transparently dispatched to the upstream /v1/responses endpoint and translated back into a strict-mode chat.completion (or chat.completion.chunk for streaming) envelope:
    • Preserves reasoning_content, refusal (finish_reason: "content_filter"), incomplete_details truncation (finish_reason: "length" or "content_filter"), and upstream failed or cancelled error envelopes.
    • Supported on OpenAI and Azure OpenAI backends only; pairing a responses_only model with another backend type returns 400 invalid_request_error before dispatch. See Responses-API-only Models.
  • 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, or gemini_context_cache.prefer_native_api: true) automatically route to Gemini's native generateContent / streamGenerateContent endpoint, transforming responses into OpenAI-compatible format with reasoning_content, code_execution, annotations, grounding_metadata, and inline images.

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:

x-continuum-dropped-tools: file_search, other

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.

POST /v1/responses

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 example external_web_access or filters), file_search (vector_store_ids), code_interpreter (container), computer_use, and programmatic_tool_calling.
  • Replayed items: message (id, status, and any other field, also on an item with role and content and no type; with a type, phase, agent, caller, or a non-standard id or status makes it a native-only item that a conversion strategy refuses), function_call (id, status, encrypted_content, encrypted_function_args) and function_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 toolConfig is sent: only when function declarations are reachable, either inline or through a cachedContent the client manages. A request with only built-in tools (web_search_options, code_execution, and similar) sends none, so a required or named choice is validated but not enforced. Each such request logs a warning and increments tool_choice_unenforced_total{reason="forced_without_functions"}.
  • Functions next to a built-in: toolConfig carries include_server_side_tool_invocations: true, including when the client sends no tool_choice. A model that does not support the mix still rejects it with a 400, as gemini-2.5-flash does.
  • tool_choice: "none": the router removes built-in tools from the body, including one derived from web_search_options, so the request runs with no tools. Each removal increments tool_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, or function_call_output item it points to. References are de-duplicated by call_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:

{
  "type": "input_image",
  "image_url": "data:image/png;base64,iVBORw0KGgo...",
  "detail": "high"
}
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_url are validated to prevent SSRF attacks
  • Private IP addresses and localhost URLs are rejected
  • Only HTTPS URLs are recommended for external files
  • The file_id field 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:

{
  "reasoning": {
    "effort": "high"
  }
}

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 successfully
  • 400: Invalid request format, parameters, or invalid reasoning effort value
  • 404: Model not found on any healthy backend
  • 502: Backend connection error
  • 504: Request timeout
  • 503: 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/responses endpoint, 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).
  • Ownership and scoping: Sessions are scoped to the authenticated API-key principal (api_keys[].user_id) that created the response. Authorization: Bearer <key> and X-API-Key: <key> share the same identity namespace. Missing, expired, unauthenticated, or cross-principal requests return 404.

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:

{
  "id": "resp_abc123",
  "object": "response.deleted",
  "deleted": true
}

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 queued answer records nothing. The first time the router relays the terminal result (completed, incomplete, failed, or cancelled) carrying usage, through GET /v1/responses/{id} or POST .../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: true together with background: true is refused with 400 unsupported_request_parameter while a guardrail applies, since a client could resume that stream later through GET ?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 GET or cancel of the terminal result, since the router stops reading the stream when the client disconnects while the backend keeps generating.
  • Replays. GET ?stream=true on 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 with 400 unsupported_request_parameter; fetch it without stream.
  • 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.

POST /v1/responses/compact

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 successful
  • 400: Invalid request (empty model, empty input, instructions too long)
  • 501: Backend does not support native context compaction
  • 502: Backend connection error
  • 504: 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.

POST /v1/images/generations

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") requires png or webp format.

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-banana mapped to gemini-2.5-flash-image until 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_prompt field contains any text response from Gemini describing the generated image
  • When google_search: true is used (or tools: [{"google_search": {}}]), any groundingMetadata returned by Gemini is preserved as grounding_metadata at the top level of the image response
  • When using /v1/chat/completions or /v1/responses with a Gemini backend, requests that include tools: [{"google_search": {}}], tools: [{"code_execution": {}}], modalities: ["text", "image"], cachedContent, or gemini_context_cache.prefer_native_api: true automatically dispatch to Gemini's native generateContent / streamGenerateContent endpoint and surface annotations, grounding_metadata, code_execution, and inline images in 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 successfully
  • 400: Invalid request (e.g., invalid size for model, n > 1 for DALL-E 3)
  • 401: Invalid API key
  • 429: Rate limit exceeded
  • 500: Backend error
  • 503: 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.

POST /v1/images/edits
Content-Type: multipart/form-data

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 a mask field is supplied for Nano Banana, it is ignored with a warning because Gemini determines edit regions from the prompt text.

Image Requirements:

  • Field name: image or image[] (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 for nano-banana / nano-banana-2 / nano-banana-2-lite / nano-banana-pro / Gemini image models, and 1 image for dall-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):

{
  "created": 1677652288,
  "data": [
    {
      "b64_json": "/9j/4AAQSkZJRgABAQAA..."
    }
  ]
}

Status Codes:

  • 200: Image(s) edited successfully
  • 400: Invalid request (e.g., non-square image, invalid size, missing required field)
  • 401: Invalid API key
  • 503: 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 native generateContent API using natural-language editing (mask is ignored if supplied, and n must be 1)
  • Multiple input images can be uploaded by repeating image or image[] up to the model's input limit (16 for gpt-image-*, 14 for nano-banana / nano-banana-2 / nano-banana-2-lite / nano-banana-pro, 1 for dall-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.

POST /v1/images/variations
Content-Type: multipart/form-data

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):

{
  "created": 1677652288,
  "data": [
    {
      "b64_json": "/9j/4AAQSkZJRgABAQAA..."
    }
  ]
}

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 successfully
  • 400: Invalid request (invalid format, non-square image, unsupported model)
  • 401: Invalid API key
  • 429: Rate limit exceeded
  • 500: Backend error
  • 503: 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.

POST /v1/audio/transcriptions
POST /v1/audio/translations
Content-Type: multipart/form-data

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 model from the multipart form to enforce per-key allowed_models, Hub tier model_allowlist, rate_limiting.limits.per_model, and backend selection (including x-backend and x-served-backend).
  • Body limit & concurrency: Controlled by proxy.audio.max_body_size (default 26214400 / 25 MiB, max 100 MiB) and proxy.audio.max_concurrent_uploads (default 8, max 64). Uploads exceeding max_body_size return 413 Payload Too Large without full buffering.
  • Timeouts & retry: Controlled by timeouts.request.audio (default first_byte: "60s", total: "300s") and timeouts.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_seconds on the usage record (preferring usage.seconds when usage.type == "duration", or rounding up top-level duration on verbose_json responses) and priced via audio_microusd_per_second, while input_tokens, output_tokens, and cached_input_tokens stay 0 so 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.

POST /v1/audio/speech
Content-Type: application/json

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 model from the JSON body to enforce per-key allowed_models, Hub tier model_allowlist, and backend selection (including x-backend and x-served-backend).
  • Body limit: The JSON body is capped at 1 MiB, or at proxy.audio.max_body_size when that is lower. A larger body returns 413 Payload Too Large, whether or not it declares Content-Length; the router stops reading at the cap.
  • Rate limits and metering: per-model rate limits read the body's model, the route requires the write API-key scope, and each served call records one usage row (usage_missing with zero tokens for an audio body, or the parsed usage when the backend answers JSON). The row is recorded when the response head arrives.
  • Backend errors: a backend 4xx is answered with the sanitized backend message and a 5xx with a generic message, as on the transcription routes; the raw backend body is never relayed.
  • Timeouts & retry: Controlled by timeouts.request.audio (default first_byte: "60s", total: "300s") and timeouts.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.

POST /v1/completions

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 (prompt string, or each element of a string/nested-string array up to 128 prompts) and suffix (checked once) are evaluated before dispatch. Requests exceeding 128 prompts, or using uninspectable token-ID / prompt_embeds inputs, are rejected in enforce mode (uninspectable_input) and recorded in monitor mode. Masking verdicts modify prompt segments in place.
  • Output stage: Evaluates choices[].text across all responses, including buffered stream: true replies.
  • Blocked responses: Return a text_completion object with finish_reason: "content_filter" (HTTP 200), or an OpenAI content_filter error envelope (HTTP 400) when block_behavior: error is 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.

POST /v1/embeddings

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:

{
  "model": "text-embedding-3-small",
  "input": "Hello, world!"
}

Array of strings:

{
  "model": "text-embedding-3-large",
  "input": [
    "First text to embed",
    "Second text to embed",
    "Third text to embed"
  ]
}

Token arrays:

{
  "model": "text-embedding-ada-002",
  "input": [[1234, 5678, 9012], [3456, 7890, 1234]]
}

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 successfully
  • 400: Invalid request (missing model/input, invalid dimensions)
  • 401: Invalid API key
  • 404: Model not found or doesn't support embeddings
  • 429: Rate limit exceeded
  • 500: Backend error
  • 503: Backend unavailable

Features:

  • Multiple Input Formats: Supports single string, array of strings, token arrays, or multimodal content parts
  • Multimodal Embeddings: gemini-embedding-2 and gemini-embedding-2-preview accept mixed text, image, audio, video, and document parts in a single request
  • Dimension Control: For text-embedding-3-* and gemini-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 400 with 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.

POST /v1/rerank

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 successfully
  • 400: Invalid request (missing model/query/documents, empty documents array)
  • 401: Invalid API key
  • 404: Model not found or doesn't support reranking
  • 429: Rate limit exceeded
  • 500: Backend error
  • 503: 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.

POST /v1/moderations

Request Body:

{
  "model": "gpt-5.6-terra",
  "input": ["first text to classify", "second text to classify"]
}
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 deny and allow match lists first, then every provider that participates in the input stage for the resolved route, with the same thresholds. The global guardrails.mode and 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, and category_scores carries the highest provider score for each. A deny match-list hit reports the deny_list category with score 1.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 answers 503 moderation_unavailable.
  • Scope and cost: the route requires the write scope, 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.

POST /tokenize
POST /v1/tokenize
POST /detokenize
POST /v1/detokenize

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

{
  "model": "Qwen/Qwen3-32B",
  "tokens": [9707, 11, 1879, 0],
  "skip_special_tokens": true
}
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
{
  "prompt": "Hello, world!",
  "text": "Hello, world!"
}

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 against vllm serve 0.32.0, commit 5df63a720, captured 2026-10-19 with Qwen/Qwen3-0.6B in src/services/tokenize/fixtures/vllm_0_32_0.md):
    • /tokenize returns {"count", "max_model_len", "tokens", "token_strs"} (token_strs is null unless return_token_strs: true, which returns Hugging Face byte-level BPE token strings). Honors messages with add_generation_prompt, continue_final_message, chat_template_kwargs, and tools; setting both switches to true or passing an untrusted per-request chat_template returns 400.
    • /detokenize returns {"prompt": "..."} and always renders special tokens regardless of skip_special_tokens. Out-of-vocabulary u32 IDs ([999999999], alone or among valid IDs) are dropped by the Hugging Face tokenizers decoder (200), whereas IDs above u32::MAX ([5000000000]) return 400.
    • Neither route has a /v1 alias (404). In register_vllm_serve_api_routers, attach_tokenize_router is called unconditionally (only GET /tokenizer_info requires --enable-tokenizer-info-endpoint).
  • sglang (verified against sglang.launch_server 0.5.22, commit baa9befa3, captured 2026-10-19 with Qwen/Qwen3-0.6B in src/services/tokenize/fixtures/sglang_0_5_22.md):
    • /v1/tokenize returns {"tokens", "count", "max_model_len"} and silently ignores return_token_strs: true on both prompt and messages. On messages, chat_template_kwargs and tools are honored (matching vLLM, llama.cpp, and mlxcel token IDs), while add_generation_prompt: false, add_special_tokens: true, chat_template, and continue_final_message: true after a user turn are ignored.
    • A final assistant message with continue_final_message: true is continued as a raw prefix (11 tokens on Qwen3, omitting the 4-token empty think block present in vLLM's 15-token render), and without continue_final_message is rewritten by _handle_last_assistant_message into a user turn (16 tokens, identical to two user turns).
    • /v1/detokenize returns {"text": "..."} and honors skip_special_tokens (default true). Out-of-vocabulary u32 IDs are dropped (200), while IDs above u32::MAX ([5000000000]) return 500 InternalServerError (mapped by the router to 502 tokenize_upstream_error).
  • llamacpp (verified against llama-server build 11344, commit ec7630a64, captured 2026-10-02, and build 11424, commit 6c59c4007, captured 2026-10-06 with Qwen3-0.6B):
    • /tokenize returns {"tokens": [...]} or {"tokens": [{"id", "piece"}]} when with_pieces is set (add_special defaults to false). /detokenize returns {"content": "..."} (the router also accepts prompt or text). Neither route has a /v1 alias.
    • /apply-template accepts a chat-completions body and returns {"prompt": "..."}. Honors add_generation_prompt, continue_final_message, chat_template_kwargs, and tools; setting both switches to true returns 400, per-request chat_template is ignored, and a final assistant message is continued as a prefix even without continue_final_message.
  • mlxcel (verified against mlxcel-server 0.7.0, commit 1e561f1e, captured 2026-10-06 with mlx-community/Qwen3-0.6B-bf16):
    • /tokenize, /detokenize, and /apply-template use the llama.cpp wire shapes and produce byte-identical default, tools, and chat_template_kwargs renders and token IDs. Incomplete UTF-8 pieces arrive as multi-byte arrays.
    • add_generation_prompt: false, continue_final_message, and per-request chat_template are ignored, and a final assistant message is continued as a prefix without any switch set.
    • Out-of-vocabulary IDs on /detokenize return 500 (type: server_error), which the router maps to 502 tokenize_upstream_error (older mlxcel builds returned 200 with the ID dropped, e.g., [999999999] -> ""). No /v1 aliases exist.
  • continuumrouter: Uses this router's own contract. Downstream routers prior to the release that added these routes return 404.
  • tensorrt-llm, ollama, and lmstudio: Marked unsupported based on upstream source and API documentation as of 2026-10-02; the router returns 501 for 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_chains hops, no response caching, and no model-experiment resolution (hide_variant_models variants return 404).
  • Backend preference and attribution: x-backend narrows selection among capable backends. x-served-backend is returned on upstream 200 responses 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 (internal and 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_tokens with 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, or 5xx) is neutral; only connection failures and timeouts count as circuit-breaker failures. A 2xx tokenizer response does not reset failure counters, and an input-triggered 5xx cannot 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.

POST /v1/decisions
POST /v1/systemone

/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 inline endpoint, or a backend: reference to a self-hosted backends[] entry) and its answer is returned unchanged except for usage.
  • Emulation: any other name is treated as a chat model. The question map is rebuilt as one forced emit_decision function tool (boolean for noul, an enum of option ids for choice, an integer level range for score), sent through the router's ordinary chat dispatch at temperature: 0 without 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 example auto) is refused with 400 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 write scope in blocking mode (403 for read-only keys). Per-key allowed_models gates the requested model (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-empty allowed_backends list can reach a backend: 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 send state outside their permitted backends. Direct requests to a disallowed target return 403; 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.0 for jev-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_params policy (max_tokens clamps decisions.emulation.max_tokens; refusals return 400 invalid_request_parameter). The x-continuum-request-params response header is not emitted on this route.
    • Input guardrails: Runs the input pre-call check (enforce-mode blocks return 400 with x-guardrail-* headers; transforms rewrite the prompt).
    • Payload caps: Bounded by decisions.emulation.max_state_bytes for state (422 state_too_large) and a fixed 64 KiB cap for the serialized question set (422 questions_too_large).
  • Passthrough targets: Forward state as 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: model is 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 it
  • 403: The key lacks the write scope, or its allow-list excludes the model, the target's backend, or (with any allowed_backends list) an inline-endpoint target
  • 404: The endpoint is disabled (decisions_disabled), or the name is neither a target nor an emulable chat model
  • 415: Unsupported media or content type (for example multipart/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-after is forwarded)
  • 502: The target failed or returned a malformed answer, or an emulated chat reply had no usable emit_decision call (decision_emulation_failed)
  • 503: The target is not available (decision_target_unavailable)
  • 504: The target did not answer within its timeout_ms
  • 529: The target reported itself overloaded (retry-after is 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.

POST /embed_sparse

Request Body:

{
  "model": "naver/splade-v3",
  "input": "What is sparse embedding?"
}

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:

{
  "model": "naver/splade-v3",
  "input": "What is sparse embedding?"
}

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 vocabulary
  • values: [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 successfully
  • 400: Invalid request (missing model/input, empty input)
  • 401: Invalid API key
  • 404: Model not found or doesn't support sparse embeddings
  • 429: Rate limit exceeded
  • 500: Backend error
  • 503: 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.

POST /v1/files
Content-Type: multipart/form-data

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:

curl -X POST http://localhost:8080/v1/files \
  -F "file=@image.png" \
  -F "purpose=vision"

Response:

{
  "id": "file-abc123def456",
  "object": "file",
  "bytes": 12345,
  "created_at": 1699061776,
  "filename": "image.png",
  "purpose": "vision"
}

Status Codes:

  • 200: File uploaded successfully
  • 400: Invalid request (missing file, invalid purpose)
  • 413: File too large (exceeds configured max_file_size)

List Files

Retrieve a list of uploaded files.

GET /v1/files
GET /v1/files?purpose=vision

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.

GET /v1/files/{file_id}

Response:

{
  "id": "file-abc123def456",
  "object": "file",
  "bytes": 12345,
  "created_at": 1699061776,
  "filename": "image.png",
  "purpose": "vision"
}

Status Codes:

  • 200: File metadata retrieved
  • 404: File not found

Download File Content

Download the content of an uploaded file.

GET /v1/files/{file_id}/content

Response: Binary file content with appropriate Content-Type header.

Status Codes:

  • 200: File content returned
  • 404: File not found

Delete File

Delete an uploaded file.

DELETE /v1/files/{file_id}

Response:

{
  "id": "file-abc123def456",
  "object": "file",
  "deleted": true
}

Status Codes:

  • 200: File deleted successfully
  • 404: 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:

  1. Validates the file ID format
  2. Loads the file content from storage
  3. Converts the file to a base64 data URL
  4. Replaces the image_file block with an image_url block

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
PDF 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 file blocks for PDF, Anthropic uses document blocks for both PDF and plain text). Non-image and non-document files will return a 400 Bad Request error 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 backends
  • image/jpeg - All backends
  • image/gif - All backends
  • image/webp - All backends
  • application/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.

POST /anthropic/v1/messages

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, or 2023-06-01 when unset. This fallback is a router convenience.
  • Present: each comma-separated or repeated value must be a real YYYY-MM-DD date 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_error before 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_control blocks are forwarded unchanged in the native body.
  • vLLM and SGLang: prefix caching runs automatically and needs no per-request breakpoint. Enable --enable-prompt-tokens-details on vLLM or --enable-cache-report on SGLang so the engine reports cached tokens and the router maps them to cache_read_input_tokens.
  • Other OpenAI-wire backends: a converted request drops client cache_control breakpoints. Set backends[].forward_cache_control: true on 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 is true for type: continuum-router and false for every other type, because strict cloud endpoints and most local engines reject or ignore the field. Breakpoints on tool_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_tokens missing.
  • budget_tokens below 1024.
  • budget_tokens at or above max_tokens, unless the anthropic-beta header carries an interleaved-thinking- token, under which Anthropic allows the budget to exceed max_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 thinking block is not signed by Anthropic. A thinking block from a backend that supplies no signature has no signature field, 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:

{"type": "image", "source": {"type": "file_id", "file_id": "file-abc123def456"}}

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.

POST /anthropic/v1/messages/count_tokens

Request Body:

{
  "model": "claude-sonnet-4-5-20250929",
  "messages": [
    {
      "role": "user",
      "content": "Hello, how are you?"
    }
  ],
  "system": "You are a helpful assistant."
}

Response:

{
  "input_tokens": 18
}

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.

GET /anthropic/v1/models

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_minute and tokens_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[].index and tool_calls[].index are different numbers. The first is the choice ordinal, which is 0 for a single-choice completion; the second is the tool call ordinal.
  • Non-streaming responses carry no index on tool_calls at 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

curl http://localhost:8080/admin/backends | jq

Monitor Service Health

curl http://localhost:8080/admin/health | jq '.services'

List Available Models

curl http://localhost:8080/v1/models | jq '.data[].id'

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.