Anthropic /v1/messages
cc-router starts a local HTTP proxy that exposes three inbound endpoints: Anthropic Messages (primary), OpenAI Responses, and OpenAI Chat Completions. This page is the full reference for Anthropic /v1/messages — the preferred entry point for Claude Code, Claude Desktop, and any client that speaks the Anthropic Messages protocol, and the only endpoint that involves no protocol translation. For a comparison of all three, see API Overview.
Applies to cc-router v3.0.0 and later.
Listening address and ports
| Setting | Default | Notes |
|---|---|---|
| Bind address | 127.0.0.1 | Becomes 0.0.0.0 after switching Settings → Proxy service → Listen address to LAN |
| HTTP port | 23456 | If busy, cc-router probes +1 up to 100 times |
| HTTPS port | 23457 | Enabled when Listen protocol is HTTPS only or HTTP + HTTPS; also probes +1 if busy |
| Port/protocol/address changes | Require an app restart | The proxy does not hot-reload |
Minimal client configuration
export ANTHROPIC_BASE_URL=http://127.0.0.1:23456
# Auth is on by default; skip the next line only if you turn it off in Settings
export ANTHROPIC_API_KEY=<token from the cc-router settings page>
# Point Claude Code's model slots at cc-router's virtual models
export ANTHROPIC_DEFAULT_FABLE_MODEL=model-fable
export ANTHROPIC_DEFAULT_OPUS_MODEL=model-opus
export ANTHROPIC_DEFAULT_SONNET_MODEL=model-sonnet
export ANTHROPIC_DEFAULT_HAIKU_MODEL=model-haiku
Authentication
- Enabled by default. You can turn it off under Settings → Authentication & CORS → Token authentication (once off, any request goes through).
- When enabled, cc-router reads the token from either header (either one is enough):
x-api-key: <token>— Claude Code’sANTHROPIC_API_KEYlands here (preferred)Authorization: Bearer <token>— Claude Code’sANTHROPIC_AUTH_TOKENlands here
- The extracted token must match exactly the
auth_tokenconfigured in the settings page; otherwise cc-router returns401. - Allowlist:
/v1/models,/health, and allOPTIONSpreflight requests always pass through, even with authentication enabled.- Rationale: clients need to list models at startup; browsers need to probe without auth blocking them. The endpoints that actually consume quota (
/v1/messages,/v1/responses,/v1/chat/completions) are the ones that require auth.
- Rationale: clients need to list models at startup; browsers need to probe without auth blocking them. The endpoints that actually consume quota (
The token cc-router asks for here is for cc-router itself, unrelated to your upstream providers’ real API keys — those are swapped in by cc-router according to virtual-model dispatch rules.
CORS is on by default: Access-Control-Allow-Origin: *, methods GET / POST / OPTIONS, all headers allowed, preflight returns 204. Even 401 responses carry CORS headers, so a browser fetch can read the response body.
Request
POST /v1/messages
Content-Type: application/json
| Header | Required | Notes |
|---|---|---|
Content-Type: application/json | Yes | The body must be JSON |
x-api-key or Authorization: Bearer ... | Per auth settings | One of the two when auth is enabled |
x-claude-code-session-id | No | Session identifier for session affinity (sent automatically by Claude Code); falls back to metadata.user_id in the body |
anthropic-version / anthropic-beta / … | No | Not forwarded by default. Once Forward client headers is enabled for a subscription on its edit page, allowlisted headers are forwarded verbatim to that upstream |
The request body uses the standard Anthropic Messages API format. cc-router reads only two fields for dispatch; every other field (messages / system / tools / temperature / max_tokens / thinking / …) is passed through unchanged to the upstream.
| Field | Type | Required | Behavior |
|---|---|---|---|
model | string | Yes | Resolved to a virtual model — see the mapping table below. Missing this returns 400 |
stream | boolean | No (defaults to false) | true uses SSE; false is non-streaming |
cc-router rewrites the model field in the body:
- Resolves to
model-fable/model-opus/model-sonnet/model-haiku→ rewritten to the real model name bound in that slot (e.g.glm-5.1,deepseek-v4-pro) - Resolves to fallback → not rewritten; passed through as-is to the upstream
Non-streaming request example
curl http://127.0.0.1:23456/v1/messages \
-H 'Content-Type: application/json' \
-d '{
"model": "model-sonnet",
"max_tokens": 256,
"messages": [
{ "role": "user", "content": "Explain cc-router in one sentence" }
]
}' | jq
Response (non-streaming)
200 OK,Content-Type: application/json- The body is the standard Anthropic
messageJSON - cc-router rewrites
message.modelback to the virtual model name (fallback mode skips this) so clients can aggregate caching and stats by virtual model usage.*(includingcache_creation_input_tokens/cache_read_input_tokens) is passed through; cc-router also extracts a copy for internal accounting
{
"id": "msg_xxx",
"type": "message",
"role": "assistant",
"model": "model-sonnet",
"content": [
{ "type": "text", "text": "..." }
],
"stop_reason": "end_turn",
"usage": { "input_tokens": 42, "output_tokens": 128 }
}
Response (streaming SSE)
200 OK,Content-Type: text/event-stream- Upstream SSE frames are passed through byte-for-byte, with two exceptions:
| Event | cc-router behavior |
|---|---|
message_start | Parses the JSON, rewrites message.model to the virtual model name (fallback skips this), extracts usage.* for accounting, re-serializes, and writes out |
message_delta | Not rewritten — cc-router only side-channels usage.output_tokens etc. for accounting; bytes pass through |
All other events (content_block_* / message_stop / ping, …) | Passed through unchanged |
Streaming request example
curl -N http://127.0.0.1:23456/v1/messages \
-H 'Content-Type: application/json' \
-d '{
"model": "model-sonnet",
"max_tokens": 256,
"stream": true,
"messages": [
{ "role": "user", "content": "ping" }
]
}'
-N disables curl buffering so SSE frames print live.
First-frame lookahead: when the upstream returns
200but the very first event is actuallyevent: error(typical case: GLM 1302/1308 quota exhaustion disguised as 200), cc-router does not forward that frame. It silently triggers a retry against the next subscription. The client only ever sees one successful completion or one final failure.Mid-stream disconnect: when the upstream connection drops mid-stream, cc-router appends an
event: errorframe plusdata: [DONE]after the frames already sent, so the client can observe the interruption instead of hanging.
Virtual model mapping
cc-router maps the request’s model field to one virtual model, then tries the subscriptions bound to that virtual model in order according to its dispatch mode (sequential / round-robin / session affinity).
Client-sent model | Resolved virtual model |
|---|---|
model-fable, claude-fable*, gpt-5.6, gpt-*-sol | model-fable |
model-opus, claude-opus*, gpt-5.5, gpt-*-terra | model-opus |
model-sonnet, claude-sonnet*, gpt-5.4, gpt-*-luna | model-sonnet |
model-haiku, claude-haiku*, gpt-*-mini | model-haiku |
model-fallback | Fallback (explicit) |
| Any other value (custom model names, etc.) | Fallback (implicit; model is passed through verbatim) |
- Every name above also accepts an
anthropic/oropenai/prefix (LiteLLM style) with the same effect, e.g.anthropic/claude-opus-4-7,openai/gpt-5.5.claude-opus*is a prefix match:claude-opus-4-7,claude-opus-4-7-20260101, andclaude-opus-100all resolve tomodel-opus.gpt-*-solmatches the tier by--separated segments:gpt-5.6-solandgpt-6-sol-20261201both resolve tomodel-fable;gpt-5.4-miniresolves tomodel-haiku.- The
gpt-*aliases are mainly for clients using the/v1/responsesand/v1/chat/completionsendpoints. They reuse the same four slots — no new virtual model is introduced.- Unknown vendor prefixes (e.g.
google/claude-opus-4-7) are not stripped, so they go to fallback.
Error responses
Errors from /v1/messages follow the Anthropic shape:
{
"type": "error",
"error": {
"type": "<kind>",
"message": "<human-readable message>"
}
}
Errors produced by cc-router itself:
| HTTP status | kind | Trigger |
|---|---|---|
400 | invalid_request_error | JSON parse failure / missing model field |
401 | authentication_error | Token mismatch when auth is enabled |
500 | api_error | Internal pipeline error |
503 | overloaded_error | The virtual model has no bound subscriptions, or every subscription is temporarily unavailable (e.g. rate-limit cooldown); the error message lists why each subscription is unavailable |
| 4xx / 5xx | Depends on upstream | When every subscription fails, cc-router forwards the last upstream’s status and error body |
SSE error frames inside the stream:
- If the very first event is
event: error→ cc-router intercepts and automatically retries the next subscription; the client never sees it - An
errorframe appearing mid-stream → passed through to the client;kindis fixed toupstream_error
Unimplemented Anthropic endpoints
The following official Anthropic endpoints are not implemented in cc-router by design:
POST /v1/messages/count_tokensPOST /v1/messages/batchesand every batches-related endpointPOST /v1/files(Files API)- Workbench / Admin API
cc-router targets Claude-Code-style real-time conversation proxying; Claude Code only depends on POST /v1/messages and GET /v1/models, so the rest is not implemented. To estimate token counts client-side, use a local library such as tiktoken, or send a single /v1/messages call and read usage.input_tokens from the response.
If your client speaks an OpenAI protocol, use the matching endpoint instead — cc-router translates the request into Anthropic Messages and runs it through the same dispatch pipeline:
- OpenAI Responses (e.g. Codex CLI) →
/v1/responses - OpenAI Chat Completions (e.g. Open WebUI, Cherry Studio) →
/v1/chat/completions