> ## Documentation Index
> Fetch the complete documentation index at: https://runinfra.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Anthropic Messages

> POST /v1/messages, an Anthropic-compatible surface over RunInfra hosted models. Switch providers by changing base_url and key.

```http theme={"dark"}
POST https://api.runinfra.ai/v1/messages
```

`POST /v1/messages` speaks the **Anthropic Messages** request and response grammar. If your application already uses the Anthropic SDK, point it at RunInfra by changing two values, the base URL and the key, and leave the rest of your code alone.

The endpoint is a translation layer, not a separate inference path. Requests run through the same serving pipeline as `POST /v1/chat/completions`, so billing, prepaid credit settlement, idempotent retries, rate limits, request limits, and cached input pricing all behave exactly as they do there.

<Warning>
  Tool use, images, and extended thinking are not available on this endpoint yet. Every unsupported field is refused with a `400` that names it, never silently ignored. See [What this endpoint does not do](#what-this-endpoint-does-not-do).
</Warning>

## Authentication

Send your RunInfra API key in **either** header on this endpoint. Both accept the same keys.

| Header                                        | Notes                                   |
| --------------------------------------------- | --------------------------------------- |
| `x-api-key: $RUNINFRA_GATEWAY_KEY`            | What the Anthropic SDK sends by default |
| `Authorization: Bearer $RUNINFRA_GATEWAY_KEY` | What every RunInfra endpoint takes      |

If both are present, `x-api-key` wins.

<Note>
  `x-api-key` is accepted on `/v1/messages` only. Every other endpoint, including `GET /v1/models`, takes `Authorization: Bearer`, so an Anthropic client's `models.list()` will not authenticate. List the model ids your key can reach with a Bearer request instead:

  ```bash theme={"dark"}
  curl https://api.runinfra.ai/v1/models \
    -H "Authorization: Bearer $RUNINFRA_GATEWAY_KEY"
  ```
</Note>

`anthropic-version` is accepted and echoed back on the response. It does not select a behavior: this endpoint publishes one grammar, described on this page.

## Minimal request

<RequestExample>
  ```python Python theme={"dark"}
  import os
  from anthropic import Anthropic

  client = Anthropic(
      base_url=os.environ.get("RUNINFRA_BASE_URL", "https://api.runinfra.ai"),
      api_key=os.environ["RUNINFRA_GATEWAY_KEY"],
  )

  message = client.messages.create(
      model="qwen3-8-27b",
      max_tokens=4096,
      system="Answer in one sentence.",
      messages=[{"role": "user", "content": "Write a deployment health check."}],
  )

  print(message.content[0].text)
  ```

  ```javascript JavaScript theme={"dark"}
  import Anthropic from "@anthropic-ai/sdk";

  const client = new Anthropic({
    baseURL: process.env.RUNINFRA_BASE_URL ?? "https://api.runinfra.ai",
    apiKey: process.env.RUNINFRA_GATEWAY_KEY,
  });

  const message = await client.messages.create({
    model: "qwen3-8-27b",
    max_tokens: 4096,
    system: "Answer in one sentence.",
    messages: [{ role: "user", content: "Write a deployment health check." }],
  });

  console.log(message.content[0].text);
  ```

  ```bash cURL theme={"dark"}
  curl https://api.runinfra.ai/v1/messages \
    -H "x-api-key: $RUNINFRA_GATEWAY_KEY" \
    -H "anthropic-version: 2023-06-01" \
    -H "Content-Type: application/json" \
    -d '{"model":"qwen3-8-27b","max_tokens":4096,"messages":[{"role":"user","content":"Write a deployment health check."}]}'
  ```
</RequestExample>

<Note>
  The Anthropic SDK appends `/v1` itself, so `base_url` is the bare host, `https://api.runinfra.ai`. The OpenAI SDK does not, which is why the `/v1/chat/completions` examples elsewhere in these docs use `https://api.runinfra.ai/v1`.
</Note>

## Request fields

| Field            | Required | Notes                                                                                                                                                                  |
| ---------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model`          | yes      | A model id this key can reach, listed by [`GET /v1/models`](/docs/api-reference/models).                                                                                    |
| `max_tokens`     | yes      | Positive integer, required as on Anthropic. `0` is refused, see the cuts below. Reasoning spends this budget even though it is not returned here, so keep it generous. |
| `messages`       | yes      | At least one message. `role` is `user`, `assistant`, or `system`.                                                                                                      |
| `system`         | no       | A string, or an array of `text` blocks.                                                                                                                                |
| `temperature`    | no       | Passed through.                                                                                                                                                        |
| `top_p`          | no       | Passed through.                                                                                                                                                        |
| `stop_sequences` | no       | Array of strings. Passed through as stop strings.                                                                                                                      |
| `stream`         | no       | `true` returns Server-Sent Events, see below.                                                                                                                          |

### Content blocks

A message's `content` is a string, or an array of `{"type":"text","text":"..."}` blocks. Multiple text blocks, in `system` or in a message, are **concatenated in order with no separator inserted**: nothing you did not write is added, and nothing you wrote is dropped.

An empty or whitespace-only `system` value means no system prompt, and no system turn is sent to the model.

## Response

```json theme={"dark"}
{
  "id": "msg_a1b2c3d4",
  "type": "message",
  "role": "assistant",
  "content": [{ "type": "text", "text": "The service responded in 12 ms." }],
  "model": "qwen3-8-27b",
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": { "input_tokens": 27, "output_tokens": 9 }
}
```

`stop_reason` is derived from how generation ended:

| Generation ended                     | `stop_reason` |
| ------------------------------------ | ------------- |
| The model finished its turn          | `end_turn`    |
| The `max_tokens` ceiling was reached | `max_tokens`  |
| Anything else, or not reported       | `null`        |

`stop_sequence` is always `null` on this endpoint. Your `stop_sequences` are still honored by the model, but the matched sequence is not reported back, so we return `null` rather than name a sequence we did not measure.

`usage.input_tokens` is the **full** prompt token count, including any part served from a cached prefix, and it is exactly what the request was billed on. This endpoint does not report `cache_read_input_tokens` or `cache_creation_input_tokens`, so unlike Anthropic there is nothing to add to `input_tokens` to get the total. Cached input pricing still applies, and is visible on `POST /v1/chat/completions` and in your usage dashboard.

## Streaming

Set `stream: true` to receive Server-Sent Events in the Anthropic event grammar. The Anthropic SDK's `client.messages.stream(...)` works unchanged.

```
event: message_start
data: {"type":"message_start","message":{"id":"msg_...","type":"message","role":"assistant","model":"qwen3-8-27b","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: ping
data: {"type":"ping"}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"The service "}}

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":{"input_tokens":27,"output_tokens":9}}

event: message_stop
data: {"type":"message_stop"}
```

Two details worth knowing if you read the raw stream rather than using the SDK:

* **`message_start` reports zero usage.** The prompt token count is not known until generation ends, and we would rather send a zero you can see than a number we guessed. The authoritative counts arrive on `message_delta`, which is where the Anthropic SDK's own stream accumulator takes final usage from, so `stream.finalMessage()` reports the right totals.
* **`message_delta.usage.input_tokens` can be `null`** in the rare case the upstream reported no usage at all. It is never a stand-in zero.

If generation fails after the stream has opened, the stream ends with an Anthropic `error` event and no `message_stop`:

```
event: error
data: {"type":"error","error":{"type":"api_error","message":"..."},"request_id":"req_..."}
```

The Anthropic SDK raises this as an `APIError`, keyed on `error.type`.

## Errors

Every `4xx` and `5xx` uses the Anthropic error envelope, including refusals raised before the model is reached:

```json theme={"dark"}
{
  "type": "error",
  "error": { "type": "invalid_request_error", "message": "max_tokens: field required. ..." },
  "request_id": "req_..."
}
```

`request_id` also rides on the `x-request-id` response header. Quote it in any support request.

| Status | `error.type`            | Typical cause                                                       |
| ------ | ----------------------- | ------------------------------------------------------------------- |
| `400`  | `invalid_request_error` | A missing required field, or a field this endpoint does not support |
| `401`  | `authentication_error`  | No key, or a key that is not valid                                  |
| `402`  | `billing_error`         | Prepaid credit balance too low for this request                     |
| `403`  | `permission_error`      | The key cannot reach this model                                     |
| `404`  | `not_found_error`       | The model ID is not available to this key                           |
| `413`  | `invalid_request_error` | Request body over the published size ceiling                        |
| `429`  | `rate_limit_error`      | Rate limit or concurrency limit reached, see `Retry-After`          |
| `5xx`  | `api_error`             | Transient failure, safe to retry with the same idempotency key      |

## Idempotent retries

Send an `Idempotency-Key` header exactly as you would on `POST /v1/chat/completions`. The key is bound to the **Anthropic** request body you sent, so retrying the same Messages request replays the original outcome instead of running and billing a second generation. See [Idempotent retries](/docs/api-reference/idempotent-retries).

## What this endpoint does not do

Each of these answers a `400` that names the field. Nothing is dropped silently.

| Not supported                                                                                                          | What to do instead                                                                                          |
| ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `tools`, `tool_choice`                                                                                                 | Use `POST /v1/chat/completions`, which accepts OpenAI-shaped tools and tool\_choice against the same models |
| `image` content blocks, and every non-text block                                                                       | Send text content only                                                                                      |
| `thinking`, and reasoning output generally                                                                             | Use `POST /v1/chat/completions`, where reasoning is returned under its own field                            |
| `top_k`, `metadata`, `service_tier`, `container`, `cache_control`, `output_config`, `inference_geo`, `user_profile_id` | Remove the field                                                                                            |
| `max_tokens: 0` to pre-warm a prompt cache                                                                             | Send a normal request                                                                                       |

Two behaviors also differ from Anthropic and are worth planning around:

* **Reasoning is not returned, but it is still generated and still billed.** Anthropic's carrier for reasoning is a `thinking` block, and both the block and its streamed form require a cryptographic `signature` over the reasoning text that only Anthropic can issue. Rather than ship an empty or invented signature, this endpoint returns the answer alone. The reasoning still spends your `max_tokens`, so a small budget can be used up thinking and return an empty `content` array with `stop_reason: "max_tokens"`, billed for those output tokens. Give the models room, or use [Chat completions](/docs/api-reference/chat-completions) where both channels are visible.
* **Consecutive same-role turns are not merged.** Anthropic combines consecutive `user` or `assistant` turns into one before the model sees them. This endpoint forwards your turns exactly as written, so a model that rejects a non-alternating conversation gives you a clear error rather than a prompt we quietly rewrote.

## Related

<Columns cols={3}>
  <Card title="Chat completions" icon="braces" href="/docs/api-reference/chat-completions">
    The OpenAI-shaped endpoint, with tools.
  </Card>

  <Card title="Streaming" icon="radio" href="/docs/api-reference/streaming">
    The OpenAI-shaped stream, delta by delta.
  </Card>

  <Card title="List models" icon="list" href="/docs/api-reference/models">
    The model ids your key can reach.
  </Card>
</Columns>
