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.

In short: Pass base_url to the Anthropic Python client along with your key. The two differences from an OpenAI-shaped integration: auth uses the x-api-key header plus an anthropic-version header, and max_tokens is required on every request.
You need a key before the code below runs. Create an account, generate a key, and copy the base URL (https://aicomp.ai). Create one free →
Check current rates → Free to sign up · $1 minimum top-up · No prepayment
Confirm the key works first. One command, no SDK, costs nothing:
Don't have an account yet? You need an API key from the gateway before the steps below will work — how the discount works and what you take on. Create one free → (no prepay, $1 minimum top-up)
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

Claude models reachable from Python — gateway rate vs official list
ModelGateway rate
in / out per 1M tokens
Official list
in / out per 1M tokens
Diff
claude-haiku-4-5$0.5 / $2.5$1 / $550%
claude-sonnet-5$1 / $5$2 / $1050%
claude-opus-5$2.5 / $12.5$5 / $2550%

Rates checked 2026-11-01. Gateway rates move with upstream promotions (verify the current number in your dashboard before committing to a budget).

Troubleshooting

ErrorFix
401 authentication_errorWrong header. Use x-api-key, and include anthropic-version.
404 on /messagesDoubled /v1. Check what your base URL already ends with.
validation error on max_tokensIt is required on this API. Set it.
not_found_error: modelCopy the model ID exactly from the /models listing.
Connection reset / SSL errorUsually a proxy. Verify with curl from the same machine.

Where the abstraction leaks

SymptomWhat is actually happeningHow to confirmFix
Setting base_url appears to do nothingThe 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 emptyUsage 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 emptyStreaming 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 differentlyThe 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 errorsA 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 bindTool 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

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.

Related

Get API access

Rate cards: Claude and GPT