API & Error Codes
The Anthropic-native endpoint
For the Anthropic SDK and Claude Code: no translation, the same key, and three behaviours that differ from the official API.
Alongside the OpenAI-compatible surface, the gateway also speaks the Anthropic native protocol:
| Endpoint | Purpose |
|---|---|
POST /v1/messages |
Chat completions, with neither request nor response translated |
POST /v1/messages/count_tokens |
Count tokens; not billed |
Point the official Anthropic SDKs, or Claude Code, at this address and they work as-is.
The address
This address has no /v1 on the end — both tools append /v1/messages themselves. Leave it on and the request lands on …/v1/v1/messages, which returns a 404.
https://api.xisu.ai
Claude Code:
export ANTHROPIC_BASE_URL="the address above"
export ANTHROPIC_AUTH_TOKEN="your key"
export ANTHROPIC_MODEL="the model you want"
Anthropic SDK: set base_url to the address above and api_key to your key.
The OpenAI-compatible
base_urlis a different string, with/v1(see Endpoints and authentication). Both protocols share one key and one balance; only the shape of the address differs.
When to use it
Use it if you need any of these: cache_control (prompt caching), thinking, multimodal and document blocks, tool_result — these fields reach the upstream untouched.
On /v1/chat/completions they meet two very different fates, an order of magnitude apart:
- top-level proprietary parameters (
thinkingand friends) are rejected outright with a 400 — visible, and the error says why; cache_controlnested inside content / system blocks is dropped silently — the request succeeds, the response looks completely normal, and prompt caching simply never hits. Your bill can multiply, and nothing will ever report an error.
That second case is the entire reason this endpoint exists. For everything else the two surfaces are equivalent; stay on whichever you already use.
Authentication: the same key
x-api-key: your key
That is the header the Anthropic SDK and Claude Code send. Authorization: Bearer is also accepted; if both are present, x-api-key wins.
Key, tenant, balance, plan and budget are shared with the OpenAI-compatible side — nothing extra to request, and usage lands on the same account.
Request and response
Standard Anthropic Messages shape, with model, messages and max_tokens required (max_tokens is mandatory in that protocol to begin with).
Responses are returned as-is, never converted into a chat.completion. Streaming is a native Anthropic event stream (message_start, content_block_delta, message_delta, message_stop) with no data: [DONE] — that is an OpenAI dialect, so don't write your termination check against it.
⚠️ Three behaviours that differ from the official API
1. An empty balance is reported as 429
The Anthropic protocol has no "insufficient balance" error type. So an empty balance, an exhausted quota and a key that hit its budget all surface here as:
{"type":"error","error":{"type":"rate_limit_error","message":"…"}}
When you get a 429, check the balance before treating it as rate limiting. The message says explicitly that this is an account limit and that retrying won't help. (SDKs typically retry twice and then hand you that message, which is who it is written for.)
2. The models here are a subset
Only models with an Anthropic upstream configured are callable on this endpoint. If the model name is valid but not available here, you get 404 not_found_error, with a message pointing at /v1/chat/completions.
404 and 503 mean different things, and the next step is the opposite in each case:
- 404
not_found_error— the model does have upstreams serving it, but none of them can serve this endpoint (no Anthropic-type upstream is configured for it). This is permanent; retrying will not help: pick a different model, or switch to/v1/chat/completions(the message points the way). It does not mean you got the model name wrong. - 503
overloaded_error— the model is on this endpoint; it just has no usable upstream at this moment (cooling down, or held back by a limit). This is temporary and you should retry — the response carriesRetry-After; back off by that value. It usually clears within minutes.
3. Two kinds of field are rejected with 400
| Rejected | Why |
|---|---|
Server tools (web_search, code_execution and friends — entries in tools without an input_schema) |
The upstream bills these per call, outside token usage, while our price list is per token |
cache_control.ttl other than 5m |
The one-hour tier costs roughly twice as much to write, and the price list has a single tier |
Custom tools (those with an input_schema) pass through normally — function calling is unaffected. The rejection message explains which rule you hit.
Listing models
Send GET /v1/models with an anthropic-version header and you get the Anthropic shape, listing only the models this endpoint can actually serve:
{"data":[{"type":"model","id":"…"}],"has_more":false}
Without that header you get the original OpenAI shape and the full set, byte for byte unchanged.
Counting tokens
POST /v1/messages/count_tokens returns {"input_tokens": N}.
It is not billed and does not appear in usage details, but it does reach the upstream, so it counts towards your request rate. Using it in a loop to size inputs is fine; treating it as free and unlimited is not.