Docs · Anthropic-compatible API
Messages
The Anthropic Messages dialect. Use it with tools and SDKs that only speak that API, most of all Claude Code. Any model of the gateway can answer, not only Anthropic models: the gateway translates the request.
POST/v1/messages#
- Auth: your API key in
x-api-keyorAuthorization: Bearer. Theanthropic-versionandanthropic-betaheaders are accepted and ignored. - Limit: 20 requests per minute per account by default, one counter that all the
/v1endpoints share with the other endpoints that have a per-minute number of their own. See rate limits and quotas. - Body: JSON.
curl -s "$SHVIA_URL/v1/messages" \
-H "x-api-key: $SHVIA_API_KEY" \
-H "content-type: application/json" \
-d '{
"model": "'"$SHVIA_MODEL"'",
"max_tokens": 256,
"messages": [{ "role": "user", "content": "Say hello in one short sentence." }]
}'Request#
| Field | Notes |
|---|---|
model | required. A model id from models. A real Anthropic model name is not mapped. If it is not in your list and has no @server, it is not rejected: the default server answers with another model, so check model in the reply. With an explicit @server that the gateway does not carry, you get 404 |
messages | required, 1 to 200 messages with role user or assistant. content is a string or a list of blocks |
system | a string, or a list of text blocks |
collections | optional list of 1 to 5 ids of your collections. The pieces of them that bear on the last user message are added to system. Not part of Anthropic's API. The refusals carry error.code, and the headers X-Inference-Collections and X-Inference-Excerpts say what was used |
stream | true for server-sent events |
tools, tool_choice | function tools. tool_choice is an object whose type is auto, any, none or tool (with a name). A bare string is ignored, as is a tool_choice sent without tools |
max_tokens | optional here, unlike Anthropic's API. If you send it, it must be an integer of at least 1. It is not enforced |
Content blocks that are understood are text, tool_use (on an assistant message) and tool_result (on a user message).
What is silently dropped#
Nothing below produces an error. The request succeeds and the item is ignored:
image,document,thinkingand any other block type. A message made only of such blocks disappears from the conversation, and a request that has only such messages may be refused by the model.temperature,top_p,top_k,stop_sequences,thinking,metadata,cache_control, and every other top-level field.- Typed tools such as web search or a computer tool are treated as ordinary function tools, by name.
Response#
{
"id": "msg_0123456789abcdef01234567",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-6@anthropic",
"content": [{ "type": "text", "text": "Hello! Nice to meet you." }],
"stop_reason": "end_turn",
"stop_sequence": null,
"usage": { "input_tokens": 14, "output_tokens": 8 }
}stop_reasonistool_usewhen the reply holds tool calls, andend_turnotherwise. It is nevermax_tokens.- A tool call is a block
{"type": "tool_use", "id": "…", "name": "…", "input": {…}}.inputis always an object. usagehas onlyinput_tokensandoutput_tokens: there are no cache counters.- As in chat completions,
modelis the id that the gateway resolved. It does not show a fallback to your default model.
Streaming#
With "stream": true the reply is text/event-stream and each event is event: <name> plus data: {…}. The events are exactly these:
| Event | When |
|---|---|
message_start | first. usage.input_tokens is always 0 here |
content_block_start | a text block opens with the first text, and each tool call opens a block |
content_block_delta | text_delta for text; for a tool call, one input_json_delta that holds the whole arguments |
content_block_stop | a block closes |
message_delta | stop_reason, and usage.output_tokens |
message_stop | last, on a normal end |
error | in place of the last two, when the reply fails |
event: message_start
data: {"type":"message_start","message":{"id":"msg_…","type":"message","role":"assistant","model":"claude-sonnet-4-6@anthropic","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}
event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
event: content_block_stop
data: {"type":"content_block_stop","index":0}
event: message_delta
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":8}}
event: message_stop
data: {"type":"message_stop"}There is no ping event. Input tokens are not reported in a stream, so use the non-streamed usage or token counting when you need them.
Errors#
Errors use the Anthropic shape {"type": "error", "error": {"type": "…", "message": "…"}}, with two exceptions: a 401 uses the OpenAI shape, and a 429 is the plain {"message": "Too Many Attempts."} when you send Accept: application/json.
| Status | error.type | When |
|---|---|---|
| 400 | invalid_request_error | the body failed validation |
| 401 | the OpenAI shape, code invalid_api_key | the key is not valid |
| 403 | permission_error | the model is not allowed for your group, or a quota is used up |
| 404 | not_found_error | the id has a server and the gateway has no such model there |
| 429 | none | request limit reached. Read Retry-After |
| 502 | api_error | the model returned nothing, or could not be reached |
| 504 | timeout_error | the model stopped mid-reply. The body adds a partial message, and X-Inference-Incomplete: read_timeout is set |
| the provider's status | api_error | every model of the chain failed and a provider refused the call. The status is passed through, and type is api_error whatever it is. A 401 or 403 here can come from the model provider, for example a model server that rejects the gateway's own credentials, and not from your key |
POST/v1/messages/count_tokens#
Returns a local estimate of the input size, so that clients such as Claude Code can decide when to compact. It calls no model and uses no quota, but it does count against the request limit.
curl -s "$SHVIA_URL/v1/messages/count_tokens" \
-H "x-api-key: $SHVIA_API_KEY" -H "content-type: application/json" \
-d '{ "model": "'"$SHVIA_MODEL"'", "messages": [{ "role": "user", "content": "Hello, world" }] }'{ "input_tokens": 3 }The response carries X-Inference-Tokens-Source: estimate-chars-4: the number is the count of characters divided by 4, rounded up, not a tokenizer. It will not match any model's own count. Earlier tool calls in the history are not counted, so an agent's long history is undercounted. The model is not checked.
Use it with Claude Code#
Point Claude Code at the host, without /v1: the client adds /v1/messages itself.
export ANTHROPIC_BASE_URL="https://ai.shvia.org"
export ANTHROPIC_AUTH_TOKEN="$SHVIA_API_KEY"
export ANTHROPIC_MODEL="$SHVIA_MODEL"
claudeSetting ANTHROPIC_BASE_URL to https://ai.shvia.org/v1 makes the client request /v1/v1/messages, which does not exist. The full setup, and the other tools, are in connect your tools.
A coding assistant can send several requests for one turn: the message itself, token counting, and a retry when a request fails. The default limit of 20 requests per minute is easy to reach with one, so ask your administrator to raise it for heavy use.
Data handling#
Like the other /v1 endpoints, nothing is stored as a conversation, each call is metered, and common identifiers in the text are masked before it goes to a model that is not on the operator's own infrastructure. See your data.