Anthropic SDK for Python: base_url, key and a first request via a gateway

The official anthropic package for Python just needs a different address. We cover base_url, the key, streaming, token counting, caching and errors.

5 min · · development · API keys

The anthropic package is the official Claude client for Python, and to use it through a gateway you do not need to rewrite anything: change the address and the key. There is one trap: in the Anthropic SDK base_url is written without /v1, while for the OpenAI SDK it ends with /v1. That is where people most often trip. Below is working code for Zukko: the client, a normal and a streaming request, token counting, caching, error handling and a table of differences from the OpenAI SDK. The package itself and its features are described in the anthropics/anthropic-sdk-python repository and in Anthropic's documentation; this text is about connecting it.

Anthropic Python base URL: installation and the client

Installation is standard. The SDK documentation states that Python 3.10 or newer is required:

bash
pip install anthropic

If you are upgrading from version 0.x to 1.x, the repository has a migration guide listing the breaking changes. Read it before editing old code.

Create a client and set the gateway address:

bash
import os
from anthropic import Anthropic

client = Anthropic(
    base_url="https://api.zukko.pro",
    api_key=os.environ["ZUKKO_API_KEY"],
)

message = client.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Explain what a decorator does in Python"}],
)

for block in message.content:
    if block.type == "text":
        print(block.text)

The fetch_... key comes from the dashboard and goes into the ZUKKO_API_KEY variable. Pass it to api_key explicitly: that way the SDK will not accidentally pick up an environment variable with an old official key. The SDK sends the key in the x-api-key header, and the gateway accepts it just like Authorization: Bearer.

Why no /v1

The SDK appends the /v1/messages path itself. If you write base_url="https://api.zukko.pro/v1", the request goes to /v1/v1/messages and returns 404. It is the mirror image of the OpenAI SDK mistake, where the address must end with /v1. The general rule for clients is described in the Python reference.

The address via environment variables

The SDK documentation says the address can be set with the ANTHROPIC_BASE_URL variable. It is handy when the same code runs in different environments:

bash
export ANTHROPIC_BASE_URL=https://api.zukko.pro
export ZUKKO_API_KEY=fetch_your_key

If you prefer the Authorization: Bearer header instead of api_key, the SDK has an auth_token parameter and a matching environment variable. Check the exact names in the README of your package version.

How the Anthropic SDK differs from the OpenAI SDK

anthropicopenai
base_urlhttps://api.zukko.prohttps://api.zukko.pro/v1
Callclient.messages.create(...)client.chat.completions.create(...)
max_tokensrequiredoptional
System instructionsystem parametera message with the system role
Response textmessage.content[0].textresponse.choices[0].message.content
Tokensusage.input_tokens, usage.output_tokensusage.prompt_tokens, usage.completion_tokens
Claude cachevisible, discountedall input at the input price

The last row matters for the budget: for Claude the gateway applies the cache discount only in the Anthropic format, because that is where cache reads and writes are visible. If your code calls Claude through openai, moving to anthropic can noticeably lower the bill on repeated contexts. More in the article on caching.

Streaming

For a stream in Python the messages.stream helper is more convenient: it accumulates the text and the final message:

bash
with client.messages.stream(
    model="claude-sonnet-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Write a function that merges two sorted lists"}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
    final = stream.get_final_message()

print()
print(final.usage)

There is also messages.create(..., stream=True): it yields raw events, and you assemble the text yourself.

Counting tokens before a request

bash
count = client.messages.count_tokens(
    model="claude-sonnet-5-5",
    messages=[{"role": "user", "content": "Hello, world"}],
)
print(count.input_tokens)

The method calls POST /v1/messages/count_tokens, see the count tokens reference in the Zukko docs. It helps estimate the cost of a long prompt in advance.

Prompt caching, verified through usage

If a large prefix repeats in every request (instructions, documentation, rules), mark it with cache_control:

bash
LONG_TEXT = open("rules.md", encoding="utf-8").read()

resp = client.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=500,
    system=[
        {"type": "text", "text": LONG_TEXT, "cache_control": {"type": "ephemeral"}},
    ],
    messages=[{"role": "user", "content": "Check whether this paragraph breaks the rules"}],
)

u = resp.usage
print(u.input_tokens, u.cache_creation_input_tokens, u.cache_read_input_tokens, u.output_tokens)

The first request fills cache_creation_input_tokens, later ones fill cache_read_input_tokens. The cost formula: fresh input × input price + cache reads × read price + cache writes × write price + output × output price. An example on claude-sonnet-5-5 (input 0.4, read 0.05, write 0.5, $ per 1M tokens, as of 18 October 2026) for a 20,000-token prefix and 50 requests, assuming the gap between them is shorter than the cache lifetime:

  • without cache: 50 × 20,000 × 0.4 / 1,000,000 = $0.40;
  • with cache: the write is 20,000 × 0.5 / 1,000,000 = $0.01, plus 49 reads at 49 × 20,000 × 0.05 / 1,000,000 = $0.049, total $0.059.

Only prefix tokens are counted; user turns and answers are billed separately. Anthropic has a minimum prefix size below which caching does not engage, and a cache lifetime: check them in Anthropic's documentation, and verify the actual result in the usage fields and in dashboard charges.

Errors and retries

SDK exceptions match status codes:

bash
import anthropic

try:
    client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=100,
        messages=[{"role": "user", "content": "Hello"}],
    )
except anthropic.AuthenticationError:
    print("401: check the key")
except anthropic.NotFoundError:
    print("404: unknown model or a stray /v1 in base_url")
except anthropic.RateLimitError:
    print("429: key limit or provider restriction")
except anthropic.APIStatusError as e:
    print(e.status_code, e.response.text)

In the SDK, 400 gives BadRequestError, 401 AuthenticationError, 403 PermissionDeniedError, 404 NotFoundError, 413 RequestTooLargeError, 429 RateLimitError, 500 and above InternalServerError, and a connection failure APIConnectionError. Code 402 (insufficient balance) has no dedicated class and is caught through the generic APIStatusError. A 503 with model_disabled (the model is switched off) arrives as InternalServerError, and the SDK retries it by default: retries will not help here, switch to a model from GET /v1/models.

By default the SDK retries twice on connection errors, 408, 409, 429 and 5xx. The max_retries parameter changes this. For a 429 caused by a key's daily limit retries will not help: raise the limit in the dashboard. For debugging every response has _request_id, and headers are available via client.messages.with_raw_response.create(...).

The async client

For batch processing there is AsyncAnthropic with the same interface:

bash
import asyncio
from anthropic import AsyncAnthropic

client = AsyncAnthropic(base_url="https://api.zukko.pro", api_key="fetch_your_key")

async def main():
    msg = await client.messages.create(
        model="claude-sonnet-5-5",
        max_tokens=200,
        messages=[{"role": "user", "content": "Say hello"}],
    )
    print(msg.content[0].text)

asyncio.run(main())

If you have thousands of requests, see the walkthrough on asynchronous processing. Create a key in the dashboard, top up from $1 (SBP with the payment page's 8.5% fee on top, MasterCard or cryptocurrency) and run the first example.

FAQ

Why do I get a 404 from the gateway?

Most often base_url contains /v1. For the anthropic package the address must be https://api.zukko.pro. The second cause is a typo in the model name; GET /v1/models lists the valid names.

Can I call models other than Claude through anthropic?

The gateway lets you call any model from its list in either format, but how specific request fields (tools or reasoning, for example) behave can differ between providers. Test with a simple request first.

Do I need the anthropic-version header?

The SDK sets it itself. If you write requests by hand with requests, add anthropic-version: 2023-06-01.

How do I move from the OpenAI SDK without changing all my code?

Switch the call to messages.create, move the system message into system, add max_tokens and fix response parsing. The rest of the application can stay as is.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

Anthropic Python base URL: SDK, key and a gateway request · Zukko