Anthropic base URL in Python — working examples
Pointing the Anthropic Python SDK at a custom endpoint is one constructor argument. This page shows the four patterns you actually need: sync, streaming, async, and raw HTTP.
https://aicomp.ai).
Create one free →
curl https://aicomp.ai/v1/models \
-H "Authorization: Bearer sk-your-gateway-key"
A JSON list of model IDs means the key is good. Invalid token means it was copied wrong.
Install
pip install anthropic
The basic pattern
from anthropic import Anthropic
client = Anthropic(
base_url="https://aicomp.ai", # no /v1 here: the SDK appends /v1/messages itself
api_key="sk-your-gateway-key", # not an Anthropic-issued key
)
resp = client.messages.create(
model="claude-sonnet-5",
max_tokens=256, # required on this API
messages=[{"role": "user", "content": "Summarise this in one sentence: {text}"}],
)
print(resp.content[0].text)
Two things to note. max_tokens is required, omit it and the request fails validation. And the text lives in content[0].text, not in a choices array.
Streaming
with client.messages.stream(
model="claude-sonnet-5",
max_tokens=512,
messages=[{"role": "user", "content": "Write a haiku about deployment."}],
) as stream:
for chunk in stream.text_stream:
print(chunk, end="", flush=True)
Streaming fails loudly if the endpoint does not support SSE, so it is a good compatibility check before you migrate real traffic.
Async
import asyncio
from anthropic import AsyncAnthropic
client = AsyncAnthropic(base_url="https://aicomp.ai", api_key="sk-your-gateway-key")
async def main():
resp = await client.messages.create(
model="claude-sonnet-5",
max_tokens=256,
messages=[{"role": "user", "content": "say OK"}],
)
print(resp.content[0].text)
asyncio.run(main())
Raw HTTP, no SDK
import requests
r = requests.post(
"https://aicomp.ai/v1/messages",
headers={
"x-api-key": "sk-your-gateway-key",
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
json={
"model": "claude-sonnet-5",
"max_tokens": 256,
"messages": [{"role": "user", "content": "say OK"}],
},
timeout=60,
)
print(r.status_code, r.json())
Note the headers: this API authenticates with x-api-key, not Authorization: Bearer, and it expects an anthropic-version header. Getting these wrong is the most common 401 when moving from an OpenAI-shaped integration.
Reading usage
print(resp.usage.input_tokens, resp.usage.output_tokens)
Log these per request. Output tokens cost several times more than input, so a model that talks more than it needs is measurably more expensive even at the same rate.
Rates for the models above
| Model | Gateway rate in / out per 1M tokens | Official list in / out per 1M tokens | Diff |
|---|---|---|---|
| claude-haiku-4-5 | $0.5 / $2.5 | $1 / $5 | 50% |
| claude-sonnet-5 | $1 / $5 | $2 / $10 | 50% |
| claude-opus-5 | $2.5 / $12.5 | $5 / $25 | 50% |
Rates checked 2026-11-01. Gateway rates move with upstream promotions (verify the current number in your dashboard before committing to a budget).
Troubleshooting
| Error | Fix |
|---|---|
| 401 authentication_error | Wrong header. Use x-api-key, and include anthropic-version. |
404 on /messages | Doubled /v1. Check what your base URL already ends with. |
| validation error on max_tokens | It is required on this API. Set it. |
| not_found_error: model | Copy the model ID exactly from the /models listing. |
| Connection reset / SSL error | Usually a proxy. Verify with curl from the same machine. |
Where the abstraction leaks
| Symptom | What is actually happening | How to confirm | Fix |
|---|---|---|---|
| Setting base_url appears to do nothing | The wrapper validates constructor arguments and may ignore an unexpected one rather than raising. | Print the resolved client configuration, or send one call and check where it lands. | Pass the endpoint through the documented constructor argument for that class, not a generic dict. |
| Token usage comes back empty | Usage is not propagated by every callback path, especially under streaming. | Compare the usage object on a streaming call versus a non-streaming one. | Turn on usage-in-streaming where supported, or aggregate from the raw response. |
| Streaming callbacks fire but the final message is empty | Streaming aggregates chunks in a handler that is not wired to the final result. | Log every chunk and confirm the last one is being appended. | Use the wrapper's own aggregation path instead of assembling text by hand. |
| Async and sync paths behave differently | The two paths can construct separate clients with separate configuration. | Send one call on each path and compare the usage log entries. | Configure both clients from one place, or stick to a single path. |
| Retries multiply spend without surfacing errors | A wrapper-level retry re-sends the full context; a few retries can double a task's cost. | Count requests per task in the usage log and compare with your own call count. | Cap retries and log every retry as a distinct, attributable event. |
| Tool-calling schemas fail to bind | Tool definitions are serialised by the wrapper, and the target may expect a different shape. | Inspect the outgoing request body for the tool section. | Declare tools in the shape the endpoint documents and verify with one call. |
How to confirm the change actually took effect
"I set it and nothing happened" is the most common failure here, and it is almost never the endpoint. It is usually the client: a desktop app that reads its own settings file rather than your shell environment, a process started before you exported the variable, or an SDK client instance built once at import time and reused after the config changed. Verify before you debug anything else.
Layer 1, is the endpoint reachable, and does your key work?
curl -s https://aicomp.ai/v1/models \
-H "Authorization: Bearer $KEY" | head -c 400
A JSON list of model IDs means both are true. An Invalid token body means the
endpoint answered but rejected the credential The URL is right, the key is not. An HTML page
means something between you and the endpoint (a corporate proxy, a captive portal) answered
instead, and no amount of config change will fix it from inside your app.
Layer 2 — who actually answered?
from openai import OpenAI
client = OpenAI(base_url="https://aicomp.ai/v1", api_key=KEY)
r = client.chat.completions.create(
model="gpt-5.6-luna",
messages=[{"role": "user", "content": "Reply with the single word: ok"}],
max_tokens=8,
)
print(r.model) # 回显的 model 是谁 —— 这是端点身份的唯一硬证据
print(r.usage) # prompt_tokens / completion_tokens
The model field echoed back is the only hard evidence of identity. A request that
silently fell through to a default provider still returns HTTP 200 and still returns sensible
text, which is exactly why this layer exists. If the echoed model is not the one you asked for,
you are talking to someone else.
Layer 3 — did it land in the usage log?
Reachability is not billing. Open the provider's usage view and look for the model ID you just called, on today's date. If the call succeeded but does not appear, you either hit a cached response or you are still on the old path. This layer is also the one that catches the case where two environments share a key and you are reading the wrong one.
Three ways this check lies to you
- A 200 that is not from the API. Proxies and error pages return 200 with HTML. Check that the body parses as JSON before you trust the status code.
- A stale process. Long-running servers and editors capture configuration at startup. Restart the process, not just the shell.
- A client built before the change. SDK clients snapshot
base_urlat construction. If yours is a module-level singleton, the new value never reaches it.
FAQ
How do I set a custom base URL with the Python Anthropic SDK?
Pass base_url to the Anthropic client constructor along with your api_key. Everything else, the messages call, the parameters, the response parsing, stays the same.
Should the base URL end in /v1?
Include the version segment once. If your endpoint already ends in /v1, do not add it again in the request path, or you will get a 404 from the doubled segment.
Does streaming work with a custom base URL?
Yes, if the endpoint supports SSE. Use the same stream context manager you would against the official API.
Can I use the OpenAI Python SDK with an Anthropic endpoint?
Only if the endpoint exposes an OpenAI-compatible layer for those models. Otherwise use the Anthropic SDK, which is what the examples below do.
Why do I get an SSL or proxy error?
Almost always a corporate proxy or a local firewall rather than the endpoint. Test with curl from the same machine first If curl works and Python does not, it is your HTTP client configuration.
What is the Anthropic API base URL?
The default is https://api.anthropic.com and the SDK adds the version itself, so unlike the OpenAI SDK you pass the root without /v1. Check which convention your endpoint expects, sending an OpenAI-style URL to the Anthropic SDK, or the reverse, is the single most common 404 here. When in doubt, curl the URL with the resource path appended.
Does a custom base URL work with the async Anthropic client?
Yes — AsyncAnthropic(base_url=…) takes the same argument as the sync client, and streaming behaves identically. The thing that does change is connection setup: create one client and reuse it rather than building one per request, or you will pay a TLS handshake on every call and, under load, exhaust the connection pool.