---
title: "Anthropic protocol"
description: "Models on pirouter that accept the Anthropic protocol, Claude included, work with the official Anthropic SDK after three changes: the base URL, the API key and the model ID. Prompt caching and thinking parameters are forwarded as they are; the error body differs from Anthropic's."
canonical: "https://pirouter.ai/docs/api/anthropic"
last-updated: "2026-10-17"
lang: "en"
source: "content/docs/en/api/anthropic.mdx"
---

# Anthropic protocol

Models on pirouter that accept the Anthropic protocol, Claude included, work with the official Anthropic SDK after three changes: the base URL, the API key and the model ID. Prompt caching and thinking parameters are forwarded as they are; the error body differs from Anthropic's.

The official Anthropic SDK works with pirouter after three changes: set the base URL to `https://api.pirouter.ai` (without `/v1`), use your pirouter API key, and write model IDs as `vendor/model`. Prompt caching and thinking parameters are written as in Anthropic's own documentation.

## Send a request with the official SDK

The endpoint is `POST /v1/messages`; the SDK adds the `/v1` itself.

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-opus-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-opus-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-opus-5.5',
  max_tokens: 1024,
  messages: [{ role: 'user', content: 'Hello!' }],
})
console.log(message.content[0].text)
```

On success it returns a reply from the model, and 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-opus-5.5",
    "stream": true,
    "max_tokens": 1024,
    "messages": [{ "role": "user", "content": "Hello!" }]
  }'
```

SSE events arrive 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; 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`, and each field is billed at its own price:

| 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/guides/billing.md#prices).

## Control thinking

On Claude models, turn thinking on with `thinking: {"type": "adaptive"}` and set the level with `output_config.effort`: `low`, `medium`, `high`, `xhigh` or `max`. Both are forwarded as they are; a level the model does not accept returns the provider's `4xx`.

> **Note:** For reliable control, use `adaptive` with `output_config.effort`. Some Claude models accept only `adaptive`, and some cannot turn thinking off: on those models `"type": "enabled"` or `"type": "disabled"` is not refused, but the thinking is not guaranteed to match what you wrote.

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

## 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 still raises an exception by status code, and `error.message` is 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/api/anthropic.md#which-models-you-can-use)                                                                                                                                                               |
| Thinking budget         | Some providers limit thinking only by a token budget. On those, a `budget_tokens` you set is kept below `max_tokens`, with the rest left for the reply; without one, the budget comes from `output_config.effort` (`medium` when no level is set) and is never below 1024                                  |
| 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                                                                                                                                              |
| Failover                | When a provider fails before sending its first byte, pirouter moves to the next provider; see [Failover](https://pirouter.ai/docs/guides/routing.md#failover)                                                                                                                                              |

## When a request fails

Branch on `error.code`; [Errors](https://pirouter.ai/docs/api/errors.md) gives the meaning of every code and whether to retry it. A `422` `validation_failed` on this protocol usually means the model does not accept the Anthropic protocol: choose another model, or use a protocol it accepts.

## Not available today

pirouter does not serve these Anthropic endpoints today:

- `POST /v1/messages/count_tokens`. A call returns `400` `invalid_request` with the `message` `model is required`, which means pirouter does not serve the endpoint; see [`invalid_request`](https://pirouter.ai/docs/api/errors.md#invalid_request).
- The Message Batches endpoints.

## Next steps

- [Create message](https://pirouter.ai/docs/api/messages.md): Every parameter and response field of the Messages endpoint.
- [Pricing & billing](https://pirouter.ai/docs/guides/billing.md): How each kind of token is priced.
