# Messages

Messages is Anthropic's API. llm.buzz serves it at `POST https://api.llm.buzz/api/v1/messages` for every text model, Claude or not, so the Anthropic SDK and the tools built on it work with the base URL alone.

## Send a request

Point the Anthropic SDK at `https://api.llm.buzz/api`, without `/v1`: the SDK adds `/v1/messages` itself. `max_tokens` is required. Claude Code takes the same base URL: [Authentication](authentication.md#send-the-key).

```python tab="Python"
from anthropic import Anthropic
import os

client = Anthropic(
    base_url="https://api.llm.buzz/api",
    api_key=os.environ["LLM_BUZZ_KEY"],
)
message = client.messages.create(
    model="anthropic/claude-sonnet-5.5",
    max_tokens=2048,
    system="You answer in one short sentence.",
    messages=[{"role": "user", "content": "Why do HTTP clients retry with backoff?"}],
)
for block in message.content:
    if block.type == "text":
        print(block.text)
```

```ts tab="TypeScript"
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({
  baseURL: "https://api.llm.buzz/api",
  apiKey: process.env.LLM_BUZZ_KEY,
});
const message = await client.messages.create({
  model: "anthropic/claude-sonnet-5.5",
  max_tokens: 2048,
  system: "You answer in one short sentence.",
  messages: [{ role: "user", content: "Why do HTTP clients retry with backoff?" }],
});
for (const block of message.content) {
  if (block.type === "text") console.log(block.text);
}
```

```bash tab="curl"
curl https://api.llm.buzz/api/v1/messages \
  -H "x-api-key: $LLM_BUZZ_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-sonnet-5.5",
    "max_tokens": 2048,
    "system": "You answer in one short sentence.",
    "messages": [{"role": "user", "content": "Why do HTTP clients retry with backoff?"}]
  }'
```

The loop reads the text blocks because a model that thinks puts a `thinking` block first. The fields llm.buzz adds to Anthropic's request, `provider`, `gateway` and `session_id`, go through `extra_body` in the SDKs, for example `extra_body={"provider": {"data_collection": "deny"}}`. The model name, and the models you can call, come from `GET https://api.llm.buzz/api/v1/models` ([Choosing an API](choosing-an-api.md#find-a-model-for-an-api)).

## Request fields

A field Messages does not know, such as `seed`, is dropped, and the body's `x-sgr-ignored-parameters` names it, on `message_start` and on the final message. A field llm.buzz deliberately refuses is a 400 before any provider is called, in Anthropic's `invalid_request_error`.

| Field | What llm.buzz does |
|---|---|
| `model` | The model's name, `author/slug`. `:floor` tries the cheapest provider first and `:nitro` the fastest, and `:exacto` is accepted and ignored. `:online` is accepted and searches nothing, and `x-sgr-ignored-parameters` names it `model:online`. [Models and routing](models-and-routing.md#suffixes). |
| `max_tokens` | Required. The most output tokens, thinking included. A provider that needs a larger minimum than your limit is skipped, and when none is left the request is refused. |
| `system` | The system instruction: a string, or a list of text blocks. |
| `messages` | The conversation, with the roles `user` and `assistant`. A block is `text`; `image`, from base64 or a URL; `document`, only for a model that reads PDFs, which no model on llm.buzz does today; `tool_use`; `tool_result`, whose content may hold text and images; or `thinking` and `redacted_thinking`, sent back unchanged. A model that cannot read an attachment refuses the request before any provider is called. [Images in requests](images-in-requests.md). |
| `stop_sequences` | Sent to providers that take them. Where a provider has no stop field, llm.buzz cuts the answer at the first match itself and ends it with `stop_reason: stop_sequence`, the matched string in `stop_sequence`. |
| `temperature`, `top_p`, `top_k` | Sent to the provider. A model that takes none has the value dropped and named: [Sampling settings a model does not take](api-reference.md#sampling-settings-a-model-does-not-take). |
| `stream` | `true` streams the answer as server-sent events: [Stream](#stream). |
| `tools`, `tool_choice` | Sent, in Anthropic's shape. Some models cannot be forced to call a tool, and some providers take a tool schema only in part. [Tools and structured output](tools-and-structured-output.md) says what happens to each. |
| `thinking`, `reasoning` | How hard the model thinks, and whether you see it. [Thinking](#thinking). |
| `output_config` | `output_config.effort` sets the reasoning effort, and every provider receives it in its own terms: [Thinking](#thinking). `output_config.format` asks for structured output. An `output_config` that holds more than `effort`, such as a `format`, is sent as you wrote it when every provider that may serve the request takes Anthropic's API. Otherwise llm.buzz drops the object and names `output_config` in `x-sgr-ignored-parameters`, and its `effort` still applies. [Tools and structured output](tools-and-structured-output.md#structured-output). |
| `cache_control` | Anthropic's prompt-cache marker, taken as the Messages API defines it, at every place it allows, the top level included. Passed on or translated, never added by llm.buzz. [Prompt caching](prompt-caching.md). |
| `context_management` | Sent where the provider takes it, with the `anthropic-beta` token it needs, and dropped and named elsewhere. |
| `inference_geo`, `speed`, `diagnostics` | Sent where the provider takes them, and dropped and named elsewhere. |
| `session_id` | A name for the conversation, 1 to 256 characters, so its turns stay on the provider that holds its cache. An empty or whitespace-only value counts as absent; any other value that is not a string of 1 to 256 characters, such as one over 256 characters, is a 400 naming `session_id`. The `x-session-id` header names it too. |
| `provider` | Steers one request: `order`, `only`, `ignore`, `allow_fallbacks`, `sort`, `zdr` and `data_collection`. Other keys, such as `require_parameters`, `quantizations`, `max_price` and `sort.partition`, are accepted and do not change which provider serves. [The provider object](api-reference.md#the-provider-object). |
| `gateway` | llm.buzz's own per-request bounds, `retry` and `routing.allow_fallbacks`. Never sent to a provider. [Retries and fallbacks per request](api-reference.md#retries-and-fallbacks-per-request). |
| `plugins`, `web_search_options` | Dropped whole and named: the whole `plugins` field is named `plugins`, not `plugins.<id>` as on Chat Completions and Responses. llm.buzz runs no web search, so nothing is searched. |
| `models`, `route`, `transforms`, `usage`, `trace`, `debug` | Dropped and named. A model's own providers fail over through its routing preset, in place of `models`. Usage counts and `usage.cost` come back whether or not you send `usage`. |
| `seed`, `logit_bias`, `response_format` | Dropped and named. A Chat `response_format` is not applied on Messages: ask for structured output with `output_config.format`. |
| `defer_loading` on a tool | Keeps the tool out of the prompt until a tool search finds it. [Tool search](api-reference.md#tool-search). |
| Refused fields, including `betas`, `container`, `fallbacks`, `fallback_credit_token`, `mcp_servers`, `service_tier`, `thread` and `user_profile_id` | 400 `invalid_request_error` naming the field. Send the `anthropic-beta` header in place of `betas`. |

## Thinking

You can ask for thinking three ways, and llm.buzz translates each into the terms of the provider that serves the request.

- **`thinking`** takes Anthropic's forms: `adaptive`, `disabled`, and `enabled` with a `budget_tokens`. Every Claude model llm.buzz lists today is in an adaptive-only family, and those families refuse a budget. A `thinking` object with `budget_tokens` answers 400 `invalid_request_error` naming `thinking.budget_tokens`, and a bare `{"type": "enabled"}` runs adaptive thinking, named `thinking.type->adaptive` in `x-sgr-ignored-parameters`. Use `adaptive`, and set the depth with `output_config.effort`.
- A model from another provider gets `thinking` translated. A bare `enabled` or `adaptive` reasons at the model's default effort, and at medium when the model names none. A `budget_tokens` goes to providers that have a budget field, and is refused before any provider is called where none has one.
- For a Claude model that takes a budget, which none on llm.buzz does today, a bare `{"type": "enabled"}` takes half of the output limit as its budget, and `budget_tokens` must be below `max_tokens` (at or above it is a 400 naming `thinking.budget_tokens`).
- **`reasoning`**, OpenRouter's object, is accepted too: `effort` or `max_tokens`, plus `enabled` and `exclude`. Its fields must agree: an effort tier or a budget beside `enabled: false`, or `effort: "none"` beside `enabled: true`, is a 400 `invalid_request_error` whose message ends `(param: reasoning.enabled)`.
- **`output_config.effort`** is a reasoning control that every provider receives in its own terms. A route that cannot serve the effort refuses the request and names `output_config.effort`.

Thinking off is not always available. `thinking: {"type": "disabled"}` is refused before any provider is called for Claude Opus 5.5, Sonnet 5.5, Fable 5 and Fable 5.1, at every effort level. Leave `thinking` out, or use `adaptive` and choose an effort. Sonnet 5.5 takes `{"type": "between_tools"}` in place of `disabled`: it turns off up-front thinking, works with an `output_config.effort` of `low`, `medium` or `high`, and takes no other `thinking` field. `reasoning.exclude: true` keeps reasoning text out of the answer while the model still thinks and bills for it, but Anthropic's own signed thinking blocks stay, because replaying them depends on them.

A conversation can move between models. Claude's signed thinking blocks go back to Claude. Another provider gets the turn without them, and `x-sgr-ignored-parameters` names it `messages.thinking->dropped(unsupported_by_provider)`.

A `max_tokens` under 1,024, with no thinking or effort setting of your own, on a model whose providers all reason by default and offer a `none` effort, is sent at effort `none`, named `reasoning_effort->none(max_tokens_headroom)`. The reply is text, not thinking cut off at the ceiling. [Reasoning](reasoning.md) has the rest.

## Headers

You send:

| Header | Use |
|---|---|
| `x-api-key` or `Authorization` | Your inference key. The Anthropic SDK sends `x-api-key`; `Authorization: Bearer` carries the same key. [Authentication](authentication.md). |
| `anthropic-version` | The SDK sends it, and llm.buzz accepts it. |
| `anthropic-beta` | Anthropic's beta tokens. A token llm.buzz does not pass on is dropped, and `x-sgr-ignored-parameters` names it `anthropic-beta.<token>`; the request still serves. |
| `x-session-id` | Names the conversation, as `session_id` does. |
| `X-Title`, `HTTP-Referer` | Name your app. [App attribution](api-reference.md#app-attribution). |

Messages has no `Idempotency-Key`: see [Retries](#retries). Every answer carries:

| Header | Value |
|---|---|
| `x-request-id` | The request's ID. Its generation ID is `gen-` and the same 32 hex digits: [Cost of a finished request](api-reference.md#cost-of-a-finished-request). |
| `x-gateway-alias` | The model name that served. |
| `x-gateway-canonical-model` | The exact model that produced the answer. |
| `x-gateway-provider` | The display name of the provider that answered, `llm.buzz` for llm.buzz's own provider. The message has no `provider` key, so read it here. |
| `x-gateway-route-depth`, `x-gateway-route-reason` | The answering provider's position in the order tried, and why the route was chosen. |

An answer that asks you to wait, a 429 and some 402 and 503 answers, carries `Retry-After`: wait that many seconds before you retry.

## Count tokens

`POST https://api.llm.buzz/api/v1/messages/count_tokens` takes a Messages request body and answers how many input tokens it comes to. It needs no `max_tokens`; one is accepted and changes nothing. `gateway` is refused with a 400 `invalid_request_error` naming it.

```bash
curl https://api.llm.buzz/api/v1/messages/count_tokens \
  -H "x-api-key: $LLM_BUZZ_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-sonnet-5.5",
    "messages": [{"role": "user", "content": "Why do HTTP clients retry with backoff?"}]
  }'
```

The answer is `{"input_tokens": N}`. The count is llm.buzz's own estimate, because no provider counts tokens for it, and the body says so with `input_tokens->estimated(gateway_tokenizer)` in `x-sgr-ignored-parameters`. It is free, and a key whose spending limit is reached can still call it. It answers the same 404 as a Messages call for a model none of whose providers meets your data policy ([Models and routing](models-and-routing.md#data-policy)).

## The answer

A non-streamed answer is Anthropic's message object. llm.buzz adds or fixes these parts:

- `content` holds `text` blocks, `tool_use` blocks, and the model's reasoning as `thinking` blocks: Anthropic's own, signed, when Anthropic serves the request, and one unsigned block with the readable reasoning from other providers. A tool turn of some models also ends with a `redacted_thinking` block. Send every thinking block back unchanged with the assistant turn. `reasoning.exclude` withholds the unsigned one.
- `stop_reason` is `end_turn` for a finished answer, `tool_use` when the model called tools, `max_tokens` when the output ceiling cut it, and `stop_sequence` when a stop sequence you sent matched, with the matched string in `stop_sequence`.
- `usage` is described [below](#usage-and-cost).
- `x-sgr-ignored-parameters` is a key in the body, not a header. It lists every field llm.buzz dropped and every value it changed, with the reason, such as `temperature->dropped(unsupported_by_provider)`. It is on the final message and on `message_start`. Check it when a setting seems to have no effect.

## Stream

Set `stream: true` and the answer arrives as Anthropic's events.

| Event | What it carries |
|---|---|
| `message_start`, then one `ping` | The message so far, with `usage` and `x-sgr-ignored-parameters`. |
| `content_block_start`, `content_block_delta`, `content_block_stop` | The content blocks: text, tool input, thinking. |
| `message_delta` | The mapped `stop_reason`, the final `usage` and the `cost`. |
| `message_stop` | The end of a finished answer. |
| `error` | One last event when a failure follows the first output, in place of `message_stop`. A failure before the stream opens is an ordinary HTTP error. |

Closing a Messages stream stops the request, and the prompt and the output produced up to then are billed. While the model is silent, comment lines beginning with `:` keep the connection active; clients skip them. [Streaming](streaming.md) covers the event shapes of all three text APIs.

## Usage and cost

| Field | Value |
|---|---|
| `input_tokens` | Uncached input tokens only. |
| `cache_creation_input_tokens` | Input tokens written to the provider's cache. |
| `cache_read_input_tokens` | Input tokens read from it. |
| `output_tokens` | Output tokens. |
| `cache_creation` | Anthropic's split of the writes, `ephemeral_5m_input_tokens` and `ephemeral_1h_input_tokens`, present when the split is known. |
| `cost` | The request's total cost in US dollars at your rate. On the final message and on `message_delta`, never on `message_start`. |

The four counts are always present, `0` when the provider reported none. The usage of `message_start` is a figure taken before the provider answers: Anthropic's own start counts when Anthropic serves the request, and llm.buzz's estimate of the prompt for any other provider, with `output_tokens` of 1 and cache counts of 0. `message_delta` carries the final counts, which replace it.

The Anthropic Python SDK does not copy `cost` into the message it assembles from a stream. Read it from the `message_delta` event, or from the answer of a request that does not stream:

```python
stream = client.messages.create(
    model="anthropic/claude-sonnet-5.5",
    max_tokens=2048,
    messages=[{"role": "user", "content": "Why do HTTP clients retry with backoff?"}],
    stream=True,
)
for event in stream:
    if event.type == "message_delta":
        print(event.usage.model_extra.get("cost"))
```

`message.usage.model_extra["cost"]` is the same value on a non-streamed message. `cost` is a JSON number that includes any billed tool-search rounds, so it can exceed the counts times the listed prices. A failed attempt that another provider replaced adds 0. It is left out when any part of the cost is unknown; a model with no price reports `0`. [Usage and limits](usage-and-limits.md) covers the rest, and [Prompt caching](prompt-caching.md#read-the-cache-in-usage) the cache fields.

## Retries

The Messages API defines no idempotency header, so llm.buzz applies none: a retry is a separate request that runs, and bills, again.

- The Anthropic SDK also retries failed requests on its own, and each retry is a separate request. Set `max_retries=0` when your application must bound the work of one operation.
- A provider that fails or times out before output reaches you is replaced by the next one, and the failed attempt costs nothing; a provider's own 400 is not tried on another provider. Once output has reached you, the request stays on the provider that sent it.

[Retries and idempotency](retries-and-idempotency.md) explains the rest.

## Errors

Errors come in Anthropic's body, `{"type": "error", "error": {"type": "...", "message": "..."}}`, and the message says what to do. A named field is appended to the message as `(param: NAME)`, and a refusal by the model adds `refusal_reason` to `error`. `error.type` follows the status:

| Status | `error.type` | When |
|---|---|---|
| 400 | `invalid_request_error` | You sent a refused field, such as `service_tier`; a field is missing or fails validation, `max_tokens` included; thinking is off on a model that cannot turn it off; a `budget_tokens` on a model that takes none; a route cannot serve `output_config.effort`; or the prompt fits no provider's context window. The message names the field and says what to change. |
| 401 | `authentication_error` | The key is missing, unknown, expired, revoked or disabled. |
| 402 | `billing_error` | Your balance is spent and the model is not free, or requests already running hold what it leaves. |
| 403 | `permission_error` | The key's spending limit is reached, or a management key called Messages. |
| 404 | `not_found_error` | The model is unknown, unavailable, has no provider that meets your data policy, or has no EU provider for an organization whose requests stay in the EU. |
| 413 | `request_too_large` | The body is over 64 MiB. |
| 429 | `rate_limit_error` | A provider is throttling, a route is unavailable, or llm.buzz is overloaded. Wait `Retry-After` seconds, then retry. |
| 503 | `overloaded_error` | Every provider is down, or llm.buzz is restarting. Retry with backoff, and after `Retry-After` seconds when the answer carries it. |
| 500, 502, 504 | `api_error` | llm.buzz or a provider failed, or the request ran past 600 seconds: retry with backoff. A 502 can also mean a provider's answer was over llm.buzz's response limit: ask for less output. |

A stream that fails after output ends with an `error` event, described under [Stream](#stream). [Errors](errors.md) lists every code, including the key, balance, model and provider errors this API shares with the others.

## Limits

- `max_tokens` is required. A model's context window is `context_length` on its entry in `GET https://api.llm.buzz/api/v1/models`, and `top_provider.max_completion_tokens` is its output ceiling.
- A request may run for 600 seconds. A stream still generating then ends with a timeout error and is billed for the prompt and the output you received. A request that is not streaming answers 504.
- A request body over 64 MiB answers 413 `request_too_large`.
- A request makes at most 8 provider calls, every fallback included; `gateway.retry` lowers the ceiling.

[Usage and limits](usage-and-limits.md#limits) collects the limits of every API.

## Related

- [Tools and structured output](tools-and-structured-output.md), [Streaming](streaming.md), [Reasoning](reasoning.md), [Images in requests](images-in-requests.md) and [Prompt caching](prompt-caching.md) cover what the three text APIs share.
- [Chat Completions](chat-completions.md) and [Responses](responses.md) are the other two text APIs; [Choosing an API](choosing-an-api.md) compares them.
- [Coming from OpenRouter](from-openrouter.md) lists every difference from OpenRouter in one place.
