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:
pip install anthropicIf 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:
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:
export ANTHROPIC_BASE_URL=https://api.zukko.pro
export ZUKKO_API_KEY=fetch_your_keyIf 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
anthropic | openai | |
|---|---|---|
base_url | https://api.zukko.pro | https://api.zukko.pro/v1 |
| Call | client.messages.create(...) | client.chat.completions.create(...) |
max_tokens | required | optional |
| System instruction | system parameter | a message with the system role |
| Response text | message.content[0].text | response.choices[0].message.content |
| Tokens | usage.input_tokens, usage.output_tokens | usage.prompt_tokens, usage.completion_tokens |
| Claude cache | visible, discounted | all 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:
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
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:
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:
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:
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.
Related articles
- Your Own Neural Network Proxy on Node.js: Keep the API Key Off the FrontendA small Express server between your frontend and the model API: the key stays on the server, and models, request size and rate are capped.
- DeepSeek API in Python: Requests, Streaming, JSON and Error HandlingReady code for DeepSeek V4.1 Flash and V4 Pro on the OpenAI SDK: a request, streaming, JSON parsing and careful error handling.
- Gemini API in Python: requests via the OpenAI SDK and OpenAI formatA working Gemini API example in Python with no extra library: change base_url in the OpenAI SDK, add streaming and JSON, and compare with google-genai.
- Claude Code security: a checklist before running the agent on a projectThe agent reads files and runs commands, so security settings belong before the first launch. A checklist: permissions, secrets, sandbox, a separate key with a limit.