---
title: "Anthropic Messages"
description: "Call the models on pirouter that accept the Anthropic protocol, Claude included, with the official Anthropic SDK. This page covers Messages requests, the two authentication headers, how prompt caching is priced, how to write thinking parameters, and how pirouter differs from Anthropic's own behavior."
canonical: "https://pirouter.ai/docs/protocols/anthropic"
last-updated: "2026-10-17"
lang: "en"
source: "content/docs/en/(start)/protocols/anthropic.mdx"
---

# Anthropic Messages

Call the models on pirouter that accept the Anthropic protocol, Claude included, with the official Anthropic SDK. This page covers Messages requests, the two authentication headers, how prompt caching is priced, how to write thinking parameters, and how pirouter differs from Anthropic's own behavior.

To call pirouter with the official Anthropic SDK, change only the base URL and the key. This page covers sending requests, the two authentication headers, prompt caching, thinking parameters, and where pirouter differs from Anthropic's own behavior.

## Send a request with the official SDK

The endpoint is `POST /v1/messages`. Set the SDK's base URL to `https://api.pirouter.ai` without `/v1`; the SDK adds it. Write model IDs as `vendor/model`.

cURL:

```bash
curl https://api.pirouter.ai/v1/messages \
  -H "x-api-key: $PIROUTER_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-sonnet-5.5",
    "max_tokens": 1024,
    "messages": [{ "role": "user", "content": "Hello!" }]
  }'
```

Python:

```python
import os
import anthropic

client = anthropic.Anthropic(
    base_url="https://api.pirouter.ai",
    api_key=os.environ["PIROUTER_API_KEY"],
)

message = client.messages.create(
    model="anthropic/claude-sonnet-5.5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello!"}],
)
print(message.content[0].text)
```

TypeScript:

```ts
import Anthropic from '@anthropic-ai/sdk'

const client = new Anthropic({
  baseURL: 'https://api.pirouter.ai',
  apiKey: process.env.PIROUTER_API_KEY,
})

const message = await client.messages.create({
  model: 'anthropic/claude-sonnet-5.5',
  max_tokens: 1024,
  messages: [{ role: 'user', content: 'Hello!' }],
})
console.log(message.content[0].text)
```

You should see a reply. The request shows up in the console's **Logs**.

To stream, add `"stream": true` to the request body and pass `-N` to curl:

```bash
curl -N https://api.pirouter.ai/v1/messages \
  -H "x-api-key: $PIROUTER_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "anthropic/claude-sonnet-5.5",
    "stream": true,
    "max_tokens": 1024,
    "messages": [{ "role": "user", "content": "Hello!" }]
  }'
```

You should see SSE events in pairs, an `event: ` line and a `data: ` line. The first event is `message_start` and the last is `message_stop`, with no `[DONE]` after it.

## Two authentication headers

Put the key in either of these request headers; the value is your pirouter API key in both:

| Header                  | How to set it in the SDK                                 |
| ----------------------- | -------------------------------------------------------- |
| `x-api-key`             | The SDK default: pass `api_key` (`apiKey` in TypeScript) |
| `Authorization: Bearer` | Pass `auth_token` instead (`authToken` in TypeScript)    |

When both are present, `Authorization` wins. The rules are in [Send the key with a request](https://pirouter.ai/docs/authentication.md#send-the-key-with-a-request).

Request headers such as `anthropic-version` and `anthropic-beta` are forwarded to the provider as they are.

## Which models you can use

The Claude family accepts the Anthropic protocol, and so do some models from other vendors. To check one model, look for `anthropic` in the `protocols` of `GET /v1/models/{vendor}/{model}/endpoints`, or see each model's protocols in the [model list](https://pirouter.ai/models). A model that does not accept the protocol answers `422`; see [Protocol mismatch](https://pirouter.ai/docs/models.md#protocol-mismatch).

Claude models can also be called by an alias, such as the name without the vendor prefix. The aliases are in `aliases` of the same response. Whichever name you use, logs and bills record the full ID.

## Use prompt caching

Add `cache_control` to the content blocks you want cached, written as in Anthropic's own documentation; pirouter forwards it as is. `ttl` can be `"5m"` or `"1h"`:

```json
"system": [
  {
    "type": "text",
    "text": "A long instruction that stays the same on every request",
    "cache_control": { "type": "ephemeral", "ttl": "1h" }
  }
]
```

Cache usage is reported in the response's `usage`:

| Usage field                                | Meaning                             | Priced at                                             |
| ------------------------------------------ | ----------------------------------- | ----------------------------------------------------- |
| `input_tokens`                             | Input that did not hit the cache    | The input price (`prompt`)                            |
| `cache_read_input_tokens`                  | Input read from the cache           | The cache read price (`input_cache_read`)             |
| `cache_creation.ephemeral_1h_input_tokens` | Input written to the 1-hour cache   | The 1-hour cache write price (`input_cache_write_1h`) |
| `cache_creation.ephemeral_5m_input_tokens` | Input written to the 5-minute cache | The cache write price in the model's pricing          |

The total input is the sum of `input_tokens`, `cache_read_input_tokens` and `cache_creation_input_tokens`. Unit prices are in the model's `pricing`; see [Prices](https://pirouter.ai/docs/billing.md#prices).

## Control thinking

`thinking` is the Messages protocol's own parameter and is forwarded as is. On Claude models, turn thinking on with `thinking: {"type": "adaptive"}` and set the level with `output_config.effort`: `low`, `medium`, `high`, `xhigh` or `max`. A level the model does not accept returns the provider's `4xx`.

Some Claude models accept only `adaptive` thinking, and some cannot turn thinking off. If you write `"type": "enabled"`, or `"type": "disabled"` on such a model, the request is not refused for it, but the thinking is not guaranteed to match what you wrote. For reliable control, use `adaptive` with `output_config.effort`.

When the request has neither `thinking` nor `output_config.effort`, some providers have a default reasoning level on record: pirouter writes `thinking: {"type": "adaptive"}` and that level in. When the default level is `none`, it writes `thinking: {"type": "disabled"}`.

Some providers limit thinking only by a token budget. On those providers, a `budget_tokens` you set is kept below `max_tokens`, with the rest left for the answer; without one, pirouter converts `output_config.effort` into a budget, using `medium` when no level is set, and never goes below 1024.

## How pirouter differs from Anthropic

| Area                    | What pirouter does                                                                                                                                                                                                                                                                                         |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Error body              | pirouter's own errors (`401`, `402`, `404`, `422`, `429`, `5xx`) are `{"error": {"message", "type", "code"}}`, not Anthropic's `{"type": "error", "error": {…}}`. Errors the provider returns are passed on as they are. The SDK raises an exception by status code, and `error.message` is still readable |
| `model` in the response | Rewritten to the pirouter model ID (`vendor/model`). The name the provider reported is in the `X-Pi-Upstream-Model` response header                                                                                                                                                                        |
| Model names             | Aliases are accepted besides the full ID; see [Which models you can use](https://pirouter.ai/docs/protocols/anthropic.md#which-models-you-can-use)                                                                                                                                                         |
| Forced tool calls       | With `tool_choice` set to `any` or to a named tool, some models do not always call the tool: the request still returns `200`, and the reply may be plain text                                                                                                                                              |
| Retries                 | When a provider fails before sending its first byte, pirouter moves to the next one; see [Automatic retry on another provider](https://pirouter.ai/docs/routing.md#automatic-retry-on-another-provider)                                                                                                    |

## Errors you may see

| Status and `error.code`            | Cause                                                | What to do                                                                                                           |
| ---------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `401` `invalid_api_key` and others | The key is missing, wrong or no longer valid         | Check the key; see [When authentication fails](https://pirouter.ai/docs/authentication.md#when-authentication-fails) |
| `402` `insufficient_balance`       | The balance is too low                               | [Add credit](https://pirouter.ai/docs/billing.md#add-credit), then send again                                        |
| `404` `model_not_found`            | No model has this ID                                 | Check the `vendor/model` spelling                                                                                    |
| `422` `validation_failed`          | The model does not accept the Anthropic protocol     | Choose another model, or use a protocol it accepts                                                                   |
| `429` `rate_limited`               | The upstream deployment used up its per-minute quota | Wait `Retry-After` seconds; see [Rate limits](https://pirouter.ai/docs/rate-limits.md)                               |

Every error code is listed in [Errors](https://pirouter.ai/docs/errors.md).

## Not available today

pirouter does not serve these Anthropic endpoints today:

- `POST /v1/messages/count_tokens`. A call returns `400` with the error code `invalid_request` and the `message` `model is required`. The error does not mean your request is missing a field; it means pirouter does not serve that endpoint.
- The Message Batches endpoints.

## Next steps

- [Gemini](https://pirouter.ai/docs/protocols/gemini.md): Call Gemini-protocol models with the Google GenAI SDK or REST.
- [Pricing & billing](https://pirouter.ai/docs/billing.md): How each kind of token is priced.
