halfoff.devDocs
Get a key
Models API

Anthropic Messages

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 stays your manual for every field. Every token is billed at half the list price.

Point the SDK at Half Off Dev#

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

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."}]
  }'

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 is the authoritative list, with prices.

Stream tokens as they arrive#

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.

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)

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 explains the details.

Let Claude call your functions#

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.

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)

tool_choice, strict and the rest of Anthropic's options are forwarded as sent; the tool use guide documents them.

Turn on extended 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.

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)

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#

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.

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)

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 has the full picture.

Count tokens before you send#

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.

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?"}]}'

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 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.

Request headers#

HeaderBehavior
x-api-key or Authorization: BearerYour Half Off Dev key. Required.
anthropic-versionForwarded; 2023-06-01 if you leave it out.
anthropic-betaForwarded unchanged.
x-halfoff-groupOptional. Picks the channel group for this request, unless the key is already bound to one.
x-session-idOptional. Marks requests as one conversation for cache-aware routing. 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.