Messages API
The Anthropic Messages shape, translated at the edge onto the same order pipeline as chat. A request here is priced, queued, charged, logged and replayed exactly as a chat request; the body, the events and the error envelope are Anthropic's.
Create a message
curl https://api.openfill.ai/v1/messages \ -H "x-api-key: $OPENFILL_API_KEY" \ -H "anthropic-version: 2023-06-01" \ -H "Content-Type: application/json" \ -d '{"model": "market-preview", "max_tokens": 1024, "messages": [{"role": "user", "content": "hi"}], "price_limit": 0.10}'Headers
| Header | Rule |
|---|---|
| x-api-key | Your OpenFill key. Authorization: Bearer is accepted too, and wins when both are sent. |
| anthropic-version, anthropic-beta | Accepted with any value and ignored. |
| X-Price-Limit, X-Expires-After, Idempotency-Key | As on chat. This is how a tool with no body field carries the price limit. |
Request body
The Anthropic fields first, then the OpenFill extensions, which carry the same names and rules as on chat. Unlisted fields pass through unless the adapter drops them as described below.
| Field | Type | Default | Meaning |
|---|---|---|---|
| model | string | the default model | A served model id or alias. |
| max_tokens | integer | required | The output cap, clamped down to the model's max output length. 0 is 400. |
| system | string or array | none | A string, or text blocks joined with a blank line, as the system message. |
| messages | array | required | Content is a string or blocks. User blocks accept text, image and tool_result, an image with a base64 source, or a url source fetched as on chat; assistant blocks accept text, tool_use and thinking, and discard redacted_thinking; system blocks accept text. Tool results precede the user text, and text and images keep their order. |
| tools | array | none | A nonempty name and an input_schema object are required; its type can be object or absent. Description is optional. The tool type can be absent or custom. These become a function tool. cache_control and the other Anthropic-only keys are dropped. |
| tool_choice | object | auto | auto, any, a named tool, or none. disable_parallel_tool_use turns parallel_tool_calls off. |
| temperature, top_p | number | none | Validated as on chat, then forwarded. top_k is dropped. |
| stop_sequences | array | none | Up to four strings of up to 256 characters. |
| stream | boolean | false | Server-sent events, in the Anthropic event vocabulary. |
| metadata.user_id | string | none | Mapped to user in the stored chat request and removed before the model request. Other metadata keys are dropped. |
| thinking | object | none | Accepted and ignored, as are context_management, cache_control and service_tier, so a client that sends them by default keeps working. Reasoning is the model's own decision. |
| container, inference_geo, betas, stream_options | as sent | none | Accepted and dropped. Terminal success events always carry usage. |
| output_config.format | object | none | A json_schema becomes response_format for the model. |
| price_limit | number | key or account | The most you will pay, in USD per 1M input tokens at the reference mix. On a market model, none anywhere is 400 missing_price_limit. Ignored on a fixed-price model. |
| price_lock | boolean | account setting | Bid the lower of the published quote and price_limit, and start within the execute window or expire with 429 capacity_unavailable. Ignored on a fixed-price model. |
| max_wait | integer | account setting | Seconds an unlocked order may wait. Alias expires_after. |
| store, retention_days | as chat | none | store: false keeps nothing durable; retention_days keeps it for fewer days. |
Price, wait, output and storage bounds follow the chat request rules. Every message needs a role and content; text and thinking values must be strings, and tool_use requires string id and name fields with an input object.
Refused with 400
| Sent | Why |
|---|---|
| document blocks, an image with a file source, and an image inside a tool_result | An image travels as a base64 source, or a url source fetched as on chat in a user turn. Nothing is charged. |
| tools with a built-in type | Only function tools run here. |
| mcp_servers | Server-side MCP is not served. |
| detached: true | The stored result is chat-shaped, so the detached path is POST /v1/chat/completions. |
Response
{ "id": "msg_<order_id>", "type": "message", "role": "assistant", "model": "market-preview", "content": [ {"type": "thinking", "thinking": "...", "signature": ""}, {"type": "text", "text": "..."}, {"type": "tool_use", "id": "...", "name": "get_weather", "input": {"city": "Boston"}} ], "stop_reason": "tool_use", "stop_sequence": null, "usage": {"input_tokens": 234, "output_tokens": 56, "cache_read_input_tokens": 1000, "cache_creation_input_tokens": 0, "x_market": {"order_id": "<order_id>", "pricing": "market", "level_at_execution": 0.23, "bid": 0.5, "charged_usd": 0.000446}}}| Field | Meaning |
|---|---|
| content | A thinking block when the model sent reasoning, then the text, then one tool_use block per call. A block absent from the answer is absent from the list. |
| stop_reason | end_turn on a normal finish, max_tokens when the cap stopped it, tool_use when the model called a tool, refusal when content was filtered, and stop_sequence only when the response carries a matched stop. Otherwise a normal finish is end_turn. |
| usage.input_tokens | The uncached prompt tokens, as Anthropic counts them; cache_read_input_tokens is the cached share. cache_creation_input_tokens is always 0. |
| usage.x_market | The receipt: the price the order was charged at, what it bid, and the debit, with pricing and, on a fixed-price model, the prices applied, as on chat. |
Streaming
200 and a text/event-stream body in the Anthropic event vocabulary, each frame an event: line and a data: line.
event: message_start <- at once, with a placeholder usageevent: ping <- optional keepalive during silence, or after requeueevent: content_block_startevent: content_block_delta <- text_delta per fragmentevent: content_block_stopevent: message_delta <- stop_reason and the final usage with x_marketevent: message_stop| Case | Events |
|---|---|
| A tool call | content_block_start with the tool_use id and name, input_json_delta with partial_json per fragment, content_block_stop. |
| Reasoning before the answer | A thinking block with thinking_delta events and one signature_delta, closed before the text opens. |
| An error after the stream opened | One error event carrying the envelope below, then the body ends. The status stays 200. A settlement failure after generation can leave a recorded or uncertain charge; inspect the message and order before retrying. |
Keepalive pings continue during silence while queued or generating. Parallel tool calls are emitted as successive blocks: the first streams as it arrives, and its peers are buffered until it closes. There is no [DONE] frame; message_stop ends a successful stream.
Count tokens
curl https://api.openfill.ai/v1/messages/count_tokens \ -H "x-api-key: $OPENFILL_API_KEY" -H "Content-Type: application/json" \ -d '{"model": "market-preview", "messages": [{"role": "user", "content": "hi"}]}'{"input_tokens": 12}The same body with max_tokens optional. The count estimates prompt tokens from the serialized translated body; it is not a tokenizer count. It creates no order and is never charged. It shares the inference rate budget.
Errors
Every error handled by these two routes is the Anthropic envelope, with the gateway's message unchanged and its code kept beside the type so missing_price_limit and capacity_unavailable stay machine-readable.
{"type": "error", "error": {"type": "rate_limit_error", "message": "...", "code": "capacity_unavailable"}}| Status | error.type | When |
|---|---|---|
| 400 | invalid_request_error | A bad body, missing_price_limit, model_deprecated, model_preview, a bad header. |
| 401 | authentication_error | The key. |
| 402 | billing_error | insufficient_balance at the current level, or at a fixed-price model's prices. |
| 403 | permission_error | The account is suspended or being deleted. |
| 404 | not_found_error | model_not_found. Unknown routes use the gateway's OpenAI error envelope. |
| 408 | timeout_error | order_expired: the wait on a market model passed max_wait. |
| 409 | conflict_error | order_canceled, or idempotency_error. |
| 410 | not_found_error | result_unavailable on a replay with nothing stored. |
| 413 | request_too_large | The body is over the size limit. |
| 429 | rate_limit_error | A ceiling, or capacity_unavailable on a missed lock window or when a request to a fixed-price model did not start in time. Retry-After where one is sent. |
| 500 | api_error | Our side. |
| 502 | api_error | The backend failed mid-stream. Nothing charged. |
| 503 | overloaded_error | The gateway is restarting or the backend is unavailable. |