---
title: Anthropic Messages
description: Run Claude at half price with your existing Anthropic SDK code. Swap the base URL; streaming, thinking, tool use, caching and token counts work unchanged.
---

If your code already talks to Claude through the Anthropic SDK, moving it to Half Off Dev takes two settings: base URL `https://halfoff.dev` and your Half Off Dev key. `POST /v1/messages` and `POST /v1/messages/count_tokens` behave as Anthropic specifies, and request bodies are forwarded untouched, so [Anthropic's API reference](https://platform.claude.com/docs/en/api/messages) stays your manual for every field. Every token is billed at half the list price.

## Point the SDK at Half Off Dev {#connect}

The Anthropic SDK puts the key in `x-api-key`; sending it as `Authorization: Bearer` is accepted as well.

```bash title="cURL"
curl https://halfoff.dev/v1/messages \
  -H "x-api-key: $HALFOFF_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-sonnet-5.5",
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "Give me three names for a code review bot."}]
  }'
```

```python title="Python"
import os
import anthropic

client = anthropic.Anthropic(
    base_url="https://halfoff.dev",
    api_key=os.environ["HALFOFF_API_KEY"],
)
reply = client.messages.create(
    model="claude-sonnet-5.5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Give me three names for a code review bot."}],
)
print(reply.content[0].text)
```

```js title="Node.js"
import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic({
  baseURL: 'https://halfoff.dev',
  apiKey: process.env.HALFOFF_API_KEY,
});
const reply = await client.messages.create({
  model: 'claude-sonnet-5.5',
  max_tokens: 1024,
  messages: [{ role: 'user', content: 'Give me three names for a code review bot.' }],
});
console.log(reply.content[0].text);
```

Code that builds a default client needs no edits at all: the SDKs pick up `ANTHROPIC_BASE_URL` and `ANTHROPIC_API_KEY` from the environment, so `export ANTHROPIC_BASE_URL=https://halfoff.dev` plus your key is enough.

The current Claude line-up includes `claude-fable-5.1`, `claude-opus-5.5`, `claude-sonnet-5.5` and `claude-haiku-5.5`, with older versions still available. [`GET /v1/models`](/docs/models/pricing) is the authoritative list, with prices.

## Stream tokens as they arrive {#streaming}

Pass `stream: true` or use the SDK's streaming helper. Events follow Anthropic's sequence: `message_start`, `content_block_start`, `content_block_delta`, `content_block_stop`, `message_delta`, `message_stop`.

```python title="Python"
with client.messages.stream(
    model="claude-sonnet-5.5",
    max_tokens=2048,
    messages=[{"role": "user", "content": "Draft a short release note for a bug-fix version."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
    final = stream.get_final_message()
print("\n", final.usage)
```

```js title="Node.js"
const stream = client.messages.stream({
  model: 'claude-sonnet-5.5',
  max_tokens: 2048,
  messages: [{ role: 'user', content: 'Draft a short release note for a bug-fix version.' }],
});
stream.on('text', (text) => process.stdout.write(text));
const final = await stream.finalMessage();
console.log('\n', final.usage);
```

Until the first token shows up, the gateway keeps the stream to itself; if the channel fails in that window, another one takes over and you never see the failure. [Streaming](/docs/models/streaming) explains the details.

## Let Claude call your functions {#tool-use}

Describe each function with a JSON Schema `input_schema`. A response with `stop_reason: "tool_use"` asks you to run it; answer with a `tool_result` block.

```python title="Python"
tools = [{
    "name": "convert_currency",
    "description": "Convert an amount between two currencies.",
    "input_schema": {
        "type": "object",
        "properties": {
            "amount": {"type": "number"},
            "from_currency": {"type": "string"},
            "to_currency": {"type": "string"},
        },
        "required": ["amount", "from_currency", "to_currency"],
    },
}]
messages = [{"role": "user", "content": "How much is 250 EUR in JPY?"}]

response = client.messages.create(
    model="claude-sonnet-5.5", max_tokens=1024, tools=tools, messages=messages,
)
if response.stop_reason == "tool_use":
    call = next(b for b in response.content if b.type == "tool_use")
    result = {"converted": 40125.0, "currency": call.input["to_currency"]}  # your code here
    messages += [
        {"role": "assistant", "content": response.content},
        {"role": "user", "content": [
            {"type": "tool_result", "tool_use_id": call.id, "content": str(result)},
        ]},
    ]
    response = client.messages.create(
        model="claude-sonnet-5.5", max_tokens=1024, tools=tools, messages=messages,
    )
print(response.content[0].text)
```

```js title="Node.js"
const tools = [{
  name: 'convert_currency',
  description: 'Convert an amount between two currencies.',
  input_schema: {
    type: 'object',
    properties: {
      amount: { type: 'number' },
      from_currency: { type: 'string' },
      to_currency: { type: 'string' },
    },
    required: ['amount', 'from_currency', 'to_currency'],
  },
}];
const messages = [{ role: 'user', content: 'How much is 250 EUR in JPY?' }];

let response = await client.messages.create({
  model: 'claude-sonnet-5.5', max_tokens: 1024, tools, messages,
});
if (response.stop_reason === 'tool_use') {
  const call = response.content.find((b) => b.type === 'tool_use');
  const result = { converted: 40125.0, currency: call.input.to_currency }; // your code here
  messages.push(
    { role: 'assistant', content: response.content },
    { role: 'user', content: [{ type: 'tool_result', tool_use_id: call.id, content: JSON.stringify(result) }] },
  );
  response = await client.messages.create({
    model: 'claude-sonnet-5.5', max_tokens: 1024, tools, messages,
  });
}
console.log(response.content[0].text);
```

`tool_choice`, `strict` and the rest of Anthropic's options are forwarded as sent; the [tool use guide](https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview) documents them.

## Turn on extended thinking {#thinking}

Send `thinking` and `output_config` in the shape Anthropic defines for the model you're calling. Newer models think adaptively and take an effort level; Claude 4.5 and older expect a fixed `budget_tokens`. The per-model rules are in [Anthropic's thinking guide](https://platform.claude.com/docs/en/build-with-claude/extended-thinking).

```python title="Python"
reply = client.messages.create(
    model="claude-opus-5.5",
    max_tokens=16000,
    thinking={"type": "adaptive", "display": "summarized"},
    output_config={"effort": "high"},
    messages=[{"role": "user", "content": "A train leaves at 09:40 and arrives at 14:05. How long is the trip in minutes?"}],
)
for block in reply.content:
    if block.type == "thinking":
        print("Reasoning:", block.thinking)
    elif block.type == "text":
        print("Result:", block.text)
```

```js title="Node.js"
const reply = await client.messages.create({
  model: 'claude-opus-5.5',
  max_tokens: 16000,
  thinking: { type: 'adaptive', display: 'summarized' },
  output_config: { effort: 'high' },
  messages: [{ role: 'user', content: 'A train leaves at 09:40 and arrives at 14:05. How long is the trip in minutes?' }],
});
for (const block of reply.content) {
  if (block.type === 'thinking') console.log('Reasoning:', block.thinking);
  else if (block.type === 'text') console.log('Result:', block.text);
}
```

Thinking counts toward `usage.output_tokens`, so it is billed at the (halved) output rate. When a conversation continues after a tool call, send the thinking blocks back exactly as you received them.

## Cache the prefix you resend {#prompt-caching}

Put `cache_control` on the last block of the part that stays the same between calls. The first call writes the cache; any later call that begins with the identical prefix reads it at a small fraction of the input rate.

```python title="Python"
response = client.messages.create(
    model="claude-sonnet-5.5",
    max_tokens=1024,
    system=[{
        "type": "text",
        "text": STYLE_GUIDE,
        "cache_control": {"type": "ephemeral"},
    }],
    messages=[{"role": "user", "content": "Rewrite this headline to match the guide: 'Q3 numbers are in'"}],
)
u = response.usage
print(u.cache_creation_input_tokens, u.cache_read_input_tokens, u.input_tokens)
```

```js title="Node.js"
const response = await client.messages.create({
  model: 'claude-sonnet-5.5',
  max_tokens: 1024,
  system: [{
    type: 'text',
    text: STYLE_GUIDE,
    cache_control: { type: 'ephemeral' },
  }],
  messages: [{ role: 'user', content: "Rewrite this headline to match the guide: 'Q3 numbers are in'" }],
});
const u = response.usage;
console.log(u.cache_creation_input_tokens, u.cache_read_input_tokens, u.input_tokens);
```

What you pay on `claude-sonnet-5.5`, per million tokens: $1.00 for uncached input, $1.25 for cache writes and $0.05 for cache reads. A prefix below the model's minimum length isn't cached, and both cache fields come back as zero. The gateway routes each conversation back to the channel that holds its cache; [Prompt caching](/docs/models/caching) has the full picture.

## Count tokens before you send {#count-tokens}

`POST /v1/messages/count_tokens` accepts a message body without `max_tokens` and answers with `input_tokens`. It costs nothing and works even when your balance is zero.

```bash title="cURL"
curl https://halfoff.dev/v1/messages/count_tokens \
  -H "x-api-key: $HALFOFF_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{"model": "claude-sonnet-5.5",
       "messages": [{"role": "user", "content": "How long is this prompt?"}]}'
```

```python title="Python"
count = client.messages.count_tokens(
    model="claude-sonnet-5.5",
    messages=[{"role": "user", "content": "How long is this prompt?"}],
)
print(count.input_tokens)
```

```js title="Node.js"
const count = await client.messages.countTokens({
  model: 'claude-sonnet-5.5',
  messages: [{ role: 'user', content: 'How long is this prompt?' }],
});
console.log(count.input_tokens);
```

> [!NOTE] An estimate, not the bill
> The count is good for sizing a prompt against the context window or for budgeting. What you are charged always comes from the `usage` block in the model's actual response.

## Claude Code {#claude-code}

Claude Code uses this same API. Export `ANTHROPIC_BASE_URL=https://halfoff.dev` and put your key in `ANTHROPIC_AUTH_TOKEN`; each session is kept on one channel so its cache stays warm. The step-by-step setup, model settings included, is in [Claude Code](/docs/integrations/claude-code).

## Request headers {#headers}

| Header | Behavior |
| --- | --- |
| `x-api-key` or `Authorization: Bearer` | Your Half Off Dev key. Required. |
| `anthropic-version` | Forwarded; `2023-06-01` if you leave it out. |
| `anthropic-beta` | Forwarded unchanged. |
| `x-halfoff-group` | Optional. Picks the [channel group](/docs/models/channel-groups) for this request, unless the key is already bound to one. |
| `x-session-id` | Optional. Marks requests as one conversation for [cache-aware routing](/docs/models/caching#explicit-session-id). `metadata.user_id` in the body has the same effect. |

All other request headers are dropped. Errors keep Anthropic's shape, `{"type": "error", "error": {"type": "...", "message": "..."}}`; the codes are listed in [Errors](/docs/api/errors).
