Pass the gateway root as the base URL and the key as the auth token: Anthropic(base_ in Python, new Anthropic({ baseURL, in TypeScript. Name the model by its catalogue identifier. Every other call in your code stays the same.
- The base URL has no
/: the library appendsv1 /, and the gateway answers there and onv1/ messages /.api/ v1/ messages - Use
auth_/token authToken, notapi_/key apiKey— the second sends a header the gateway does not read. - Replies keep the library’s own shapes: messages, stream events, the four usage counters and the error envelope its exception classes are built on.
Create the client
Install the vendor’s client library for the Messages API — anthropic on PyPI, @anthropic-ai/ on npm — or keep the one you already have:
pip install anthropicnpm install @anthropic-ai/sdkTwo arguments change: the base URL and the credential. The base URL is the root of the gateway; the library adds the version segment and the route path itself, so a base URL that already ends in / produces / and a 404.
from anthropic import Anthropic
client = Anthropic(
base_url="https://api.gtokens.io", # the root: the library appends /v1/messages
auth_token="sk-sl-your-key", # auth_token, not api_key
)
message = client.messages.create(
model="anthropic/claude-sonnet-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Explain HTTP 402 in one sentence."}],
)
print(message.content[0].text)
print(message.usage)import Anthropic from '@anthropic-ai/sdk'
const client = new Anthropic({
baseURL: 'https://api.gtokens.io', // the root: the library appends /v1/messages
authToken: 'sk-sl-your-key', // authToken, not apiKey
})
const message = await client.messages.create({
model: 'anthropic/claude-sonnet-5-5',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Explain HTTP 402 in one sentence.' }],
})
console.log(message.content)
console.log(message.usage)The model is named by its catalogue identifier in the form vendor/. There are no aliases: a name the catalogue does not carry comes back 404, and the model you name is the model that answers.
Or leave the code alone and use the environment
Both libraries read the base URL and the auth token from the environment when the constructor is called without them. Code that does Anthropic() with no arguments moves to the gateway with three lines and no edit:
export ANTHROPIC_BASE_URL=https://api.gtokens.io
export ANTHROPIC_AUTH_TOKEN=sk-sl-your-key
export ANTHROPIC_API_KEY=Keep ANTHROPIC_ empty so the auth token is the only credential in play: the gateway reads Authorization: Bearer and ignores x-api-key.
Stream the reply
Streaming uses the library’s own helpers. The gateway answers with text/ in the shape the helpers parse — message_, content deltas, message_ with the final counters and message_ — so text arrives while the model is still writing:
with client.messages.stream(
model="anthropic/claude-sonnet-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Write a haiku about latency."}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
final = stream.get_final_message()
print()
print(final.usage.input_tokens, final.usage.output_tokens)const stream = client.messages.stream({
model: 'anthropic/claude-sonnet-5-5',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Write a haiku about latency.' }],
})
stream.on('text', (text) => process.stdout.write(text))
const final = await stream.finalMessage()
console.log('\n', final.usage.input_tokens, final.usage.output_tokens)The counters arrive in two places: input in message_, final output in the last message_. The helpers assemble them into the final message for you, and billing follows the same numbers. If the stream ends before that last message_ — you disconnected halfway, say — the output is billed by what had been delivered up to that point.
Read usage and work out the cost
Every reply carries four counters, and each is billed at its own rate for that model:
| Counter | What it counts |
|---|---|
input_ | Read from the request, outside the cache. |
output_ | Generated in the reply. |
cache_ | Written to the prompt cache. |
cache_ | Served from the prompt cache. |
Scroll the table sideways for the description.
The cost of a call is each counter times its rate per token, summed. A few models step their rate up past a prompt size; the step is named on the model page. Rates are published per model on the models page and in GET /.
The reply has the shape of the client library, and that shape has no field for money. The amount actually charged is in the request log in your account, and GET / returns the running total.
Cache the part that repeats
Prompt caching works as the library documents it: mark the long, stable part of the prompt with cache_, and later calls that start with the same blocks read it from the cache. The first call writes the cache, the next ones read it, and both show up in their own counters:
message = client.messages.create(
model="anthropic/claude-sonnet-5-5",
max_tokens=1024,
system=[
{
"type": "text",
"text": LONG_INSTRUCTIONS, # the part that repeats
"cache_control": {"type": "ephemeral"},
}
],
messages=[{"role": "user", "content": question}],
)
u = message.usage
print(u.cache_creation_input_tokens, u.cache_read_input_tokens)A cache read is the cheapest of the four rates, so long system prompts, tool definitions and documents you ask about repeatedly are worth caching.
Handle errors by status
Failures come back in the library’s native error envelope, so its exception classes work unchanged. Next to error. the body carries error. — the same vocabulary every other route uses — and request_ is present but may be null, so read it without requiring a value:
import anthropic
try:
message = client.messages.create(...)
except anthropic.APIStatusError as e:
print(e.status_code) # 401, 402, 404, 503, ...
error = (e.body or {}).get("error", {})
print(error.get("error_type")) # same vocabulary on every route
print(e.response.headers.get("retry-after")) # set on 429 and 503| Status | Meaning | What to do |
|---|---|---|
401 | No bearer token, or a key that is unknown or revoked. | Check the credential and the base URL. |
402 | The balance or a spending ceiling cannot cover the request. Nothing is charged. | Top up, or wait for the ceiling to reset. |
404 | No model goes by that identifier. | Use a catalogue identifier. |
429 | Slow down. Retry-After is always set. | Wait Retry-After and retry, with a cap on attempts. If the same 429 keeps coming back after waiting, stop the loop and write to support with the request_, if it has one. |
503 | The model is temporarily unavailable. Retry-After is always set. | Wait Retry-After and send the same request again. |
504 | No answer in the time allowed. | Retry with your own pause and a cap on attempts; if it persists, write to support. |
Scroll the table sideways for what to do.
Both libraries also retry some statuses on their own; the max_ option (maxRetries in TypeScript) decides how many times. Set it to 0 if you would rather honour Retry-After yourself.



