ShvIA

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-key or Authorization: Bearer. The anthropic-version and anthropic-beta headers are accepted and ignored.
  • Limit: 20 requests per minute per account by default, one counter that all the /v1 endpoints share with the other endpoints that have a per-minute number of their own. See rate limits and quotas.
  • Body: JSON.
bash
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#

FieldNotes
modelrequired. 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
messagesrequired, 1 to 200 messages with role user or assistant. content is a string or a list of blocks
systema string, or a list of text blocks
collectionsoptional 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
streamtrue for server-sent events
tools, tool_choicefunction 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_tokensoptional 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, thinking and 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#

json
{
  "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_reason is tool_use when the reply holds tool calls, and end_turn otherwise. It is never max_tokens.
  • A tool call is a block {"type": "tool_use", "id": "…", "name": "…", "input": {…}}. input is always an object.
  • usage has only input_tokens and output_tokens: there are no cache counters.
  • As in chat completions, model is 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:

EventWhen
message_startfirst. usage.input_tokens is always 0 here
content_block_starta text block opens with the first text, and each tool call opens a block
content_block_deltatext_delta for text; for a tool call, one input_json_delta that holds the whole arguments
content_block_stopa block closes
message_deltastop_reason, and usage.output_tokens
message_stoplast, on a normal end
errorin place of the last two, when the reply fails
sse
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.

Statuserror.typeWhen
400invalid_request_errorthe body failed validation
401the OpenAI shape, code invalid_api_keythe key is not valid
403permission_errorthe model is not allowed for your group, or a quota is used up
404not_found_errorthe id has a server and the gateway has no such model there
429nonerequest limit reached. Read Retry-After
502api_errorthe model returned nothing, or could not be reached
504timeout_errorthe model stopped mid-reply. The body adds a partial message, and X-Inference-Incomplete: read_timeout is set
the provider's statusapi_errorevery 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.

bash
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" }] }'
json
{ "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.

bash
export ANTHROPIC_BASE_URL="https://ai.shvia.org"
export ANTHROPIC_AUTH_TOKEN="$SHVIA_API_KEY"
export ANTHROPIC_MODEL="$SHVIA_MODEL"
claude

Setting 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.