Anthropic messages

Anthropic messages

POST /api/v1/messages

The Anthropic Messages API. Claude Code, Cursor and Zed speak only this wire and have no
fallback, so a failing endpoint here means the gateway is unusable to them rather than
degraded.

Auth accepts x-api-key as well as Authorization: Bearer, so an Anthropic SDK works
with only its base URL changed.

Request

Field Type Notes
model string An id from GET /models.
max_tokens integer Required by this wire, unlike the OpenAI paths.
messages array Anthropic message objects.
system string or array System prompt, as a string or content blocks.
stream boolean Server-sent events.
tools array Anthropic tool definitions.
tool_choice object Left alone when you omit it.
curl https://hisoka.ai/api/v1/messages \
  -H "x-api-key: $HISOKA_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "glm-5.3-flash",
    "max_tokens": 256,
    "messages": [{ "role": "user", "content": "Name three prime numbers." }]
  }'

Response

The Anthropic envelope, with the hisoka billing block added:

{
  "id": "msg-01M73N2...",
  "type": "message",
  "role": "assistant",
  "model": "glm-5.3-flash",
  "content": [{ "type": "text", "text": "2, 3, 5." }],
  "stop_reason": "end_turn",
  "usage": { "input_tokens": 18, "output_tokens": 8 },
  "hisoka": { "usd_micro_charged": 14, "balance": "4.819986" }
}

stop_reason carries the Anthropic vocabulary (end_turn, max_tokens, tool_use), not
the OpenAI one, because the client reading it expects that.

Errors use the Anthropic shape

{
  "type": "error",
  "error": {
    "type": "authentication_error",
    "message": "Invalid API key (invalid_api_key)",
    "code": "invalid_api_key"
  },
  "request_id": "01M73N2HP4HDDZH803J1C120HT"
}

Our own code is carried alongside the Anthropic error.type, so a client can branch on
either. Sending an OpenAI-shaped error to Claude Code is the failure this shape prevents.

Count tokens

POST /api/v1/messages/count_tokens
curl https://hisoka.ai/api/v1/messages/count_tokens \
  -H "x-api-key: $HISOKA_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{"messages":[{"role":"user","content":"ping"}]}'
{ "input_tokens": 6 }

Answered locally rather than proxied, so a client that calls it before every turn does not
double the upstream request count. It is an estimate: the right shape for budgeting a
context window, not a figure to bill against.

Never rewrite tool_choice on this path

A tool_choice the caller leaves unspecified stays unspecified. Mapping it to required
would forbid the model from answering without a tool call, and an agent then loops forever
on valid 200 responses. If an agent repeats the same tool indefinitely, check the client
before the gateway.

Base URL, one more time

https://hisoka.ai/api/v1, nothing deeper. The SDK appends /v1/messages itself. The
gateway collapses a repeated version segment but never strips a single one, because
clients that send it once must keep working.