# Python

URL: https://docs.router.ezat.io/examples/python

> Python examples with the Anthropic and OpenAI SDKs - a plain request, streaming, a tool call round trip, an image, counting tokens, listing models and handling errors.

## When to use this page

You are calling EZAT Router from Python with the official Anthropic SDK (`anthropic`) or OpenAI SDK (`openai`). The raw HTTP requests are on [curl](https://docs.router.ezat.io/examples/curl.md).

## Set up

Install the SDK you want (or both), and put your key in an environment variable:

```bash
pip install anthropic openai
export EZAT_API_KEY="ezat_sk_…"
```

Each SDK needs only its base URL changed:

| SDK | `base_url` | Endpoint it calls |
| --- | --- | --- |
| `anthropic` | `https://api.router.ezat.io` | `/v1/messages` |
| `openai` | `https://api.router.ezat.io/openai/v1` | `/openai/v1/chat/completions` |

Both SDKs also read the base URL and key from the environment, which is handy for tools you don't write yourself: `ANTHROPIC_BASE_URL` and `ANTHROPIC_API_KEY`, or `OPENAI_BASE_URL` and `OPENAI_API_KEY`.

## A plain request

```python tab="Anthropic SDK" test
import os

import anthropic

client = anthropic.Anthropic(base_url="https://api.router.ezat.io", api_key=os.environ["EZAT_API_KEY"])

message = client.messages.create(
    model="anthropic/claude-sonnet-5-5",
    max_tokens=64,
    messages=[{"role": "user", "content": "Say hello in five words."}],
)
for block in message.content:
    if block.type == "text":
        print(block.text)
print(message.usage)
print("request-id:", message._request_id)
```

```python tab="OpenAI SDK" test
import os

import openai

client = openai.OpenAI(base_url="https://api.router.ezat.io/openai/v1", api_key=os.environ["EZAT_API_KEY"])

completion = client.chat.completions.create(
    model="anthropic/claude-sonnet-5-5",
    max_completion_tokens=64,
    messages=[{"role": "user", "content": "Say hello in five words."}],
)
print(completion.choices[0].message.content)
print(completion.usage)
print("x-request-id:", completion._request_id)
```

`usage` has the token counts you are charged for; see [Pricing](https://docs.router.ezat.io/pricing.md).

## Streaming

```python tab="Anthropic SDK" test
import os

import anthropic

client = anthropic.Anthropic(base_url="https://api.router.ezat.io", api_key=os.environ["EZAT_API_KEY"])

with client.messages.stream(
    model="anthropic/claude-sonnet-5-5",
    max_tokens=64,
    messages=[{"role": "user", "content": "Count from one to five."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
    message = stream.get_final_message()
print()
print(message.usage)
```

```python tab="OpenAI SDK" test
import os

import openai

client = openai.OpenAI(base_url="https://api.router.ezat.io/openai/v1", api_key=os.environ["EZAT_API_KEY"])

stream = client.chat.completions.create(
    model="anthropic/claude-sonnet-5-5",
    max_completion_tokens=64,
    stream=True,
    stream_options={"include_usage": True},
    messages=[{"role": "user", "content": "Count from one to five."}],
)
for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)
    # With include_usage, the last chunk has the usage and no choices.
    if chunk.usage:
        print()
        print(chunk.usage)
```

## A tool call round trip

The model asks for a tool, your code runs it, and you send the result back with the conversation so far.

```python tab="Anthropic SDK" test
import os

import anthropic

client = anthropic.Anthropic(base_url="https://api.router.ezat.io", api_key=os.environ["EZAT_API_KEY"])

tools = [
    {
        "name": "get_weather",
        "description": "Get the current weather in a city.",
        "input_schema": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
    }
]
messages = [{"role": "user", "content": "What is the weather in Paris? Use the get_weather tool."}]

first = client.messages.create(model="anthropic/claude-sonnet-5-5", max_tokens=64, tools=tools, messages=messages)
call = next((block for block in first.content if block.type == "tool_use"), None)
if call is None:
    raise SystemExit(f"No tool call (stop_reason: {first.stop_reason})")
print("Tool call:", call.name, call.input)

# Run the tool, then send its result back.
messages.append({"role": "assistant", "content": first.content})
messages.append(
    {
        "role": "user",
        "content": [
            {"type": "tool_result", "tool_use_id": call.id, "content": "Sunny and 18 degrees Celsius"}
        ],
    }
)

second = client.messages.create(model="anthropic/claude-sonnet-5-5", max_tokens=64, tools=tools, messages=messages)
for block in second.content:
    if block.type == "text":
        print(block.text)
```

```python tab="OpenAI SDK" test
import os

import openai

client = openai.OpenAI(base_url="https://api.router.ezat.io/openai/v1", api_key=os.environ["EZAT_API_KEY"])

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather in a city.",
            "parameters": {
                "type": "object",
                "properties": {"city": {"type": "string"}},
                "required": ["city"],
            },
        },
    }
]
messages = [{"role": "user", "content": "What is the weather in Paris? Use the get_weather tool."}]

first = client.chat.completions.create(
    model="anthropic/claude-sonnet-5-5", max_completion_tokens=64, tools=tools, messages=messages
)
reply = first.choices[0].message
if not reply.tool_calls:
    raise SystemExit(f"No tool call (finish_reason: {first.choices[0].finish_reason})")
call = reply.tool_calls[0]
print("Tool call:", call.function.name, call.function.arguments)

# Run the tool, then send its result back.
messages.append(reply)
messages.append({"role": "tool", "tool_call_id": call.id, "content": "Sunny and 18 degrees Celsius"})

second = client.chat.completions.create(
    model="anthropic/claude-sonnet-5-5", max_completion_tokens=64, tools=tools, messages=messages
)
print(second.choices[0].message.content)
```

## An image

Images go inline as base64. This one is an 8×8 red PNG; for a file of your own, use `base64.b64encode(open("photo.png", "rb").read()).decode()`.

```python tab="Anthropic SDK" test
import os

import anthropic

client = anthropic.Anthropic(base_url="https://api.router.ezat.io", api_key=os.environ["EZAT_API_KEY"])

png = "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAEUlEQVR42mP4z8CAFTEMLQkAKP8/wc53yE8AAAAASUVORK5CYII="

message = client.messages.create(
    model="anthropic/claude-sonnet-5-5",
    max_tokens=64,
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": png}},
                {"type": "text", "text": "What colour is this image? Answer in one word."},
            ],
        }
    ],
)
for block in message.content:
    if block.type == "text":
        print(block.text)
```

```python tab="OpenAI SDK" test
import os

import openai

client = openai.OpenAI(base_url="https://api.router.ezat.io/openai/v1", api_key=os.environ["EZAT_API_KEY"])

png = "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAEUlEQVR42mP4z8CAFTEMLQkAKP8/wc53yE8AAAAASUVORK5CYII="

completion = client.chat.completions.create(
    model="anthropic/claude-sonnet-5-5",
    max_completion_tokens=64,
    messages=[
        {
            "role": "user",
            "content": [
                {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{png}"}},
                {"type": "text", "text": "What colour is this image? Answer in one word."},
            ],
        }
    ],
)
print(completion.choices[0].message.content)
```

## Count tokens

Anthropic SDK only. Counting is free, but it counts toward the key's requests per minute.

```python test
import os

import anthropic

client = anthropic.Anthropic(base_url="https://api.router.ezat.io", api_key=os.environ["EZAT_API_KEY"])

count = client.messages.count_tokens(
    model="anthropic/claude-sonnet-5-5",
    messages=[{"role": "user", "content": "Say hello in five words."}],
)
print(count.input_tokens)
```

## List models

These list the models your key can call. See [Models](https://docs.router.ezat.io/models.md) for the public catalog with prices.

```python tab="Anthropic SDK" test
import os

import anthropic

client = anthropic.Anthropic(base_url="https://api.router.ezat.io", api_key=os.environ["EZAT_API_KEY"])

for model in client.models.list():
    print(model.id, model.display_name)
```

```python tab="OpenAI SDK" test
import os

import openai

client = openai.OpenAI(base_url="https://api.router.ezat.io/openai/v1", api_key=os.environ["EZAT_API_KEY"])

for model in client.models.list():
    print(model.id, model.owned_by)
```

## Handle errors

Each SDK raises its own exception classes. The SDKs already retry connection errors, 429 and 5xx twice, so an exception you catch has run out of retries (raise `max_retries` on the client to try longer). This request asks for a model that doesn't exist, so it fails with 404.

```python tab="Anthropic SDK" test
import os

import anthropic

client = anthropic.Anthropic(base_url="https://api.router.ezat.io", api_key=os.environ["EZAT_API_KEY"])

try:
    client.messages.create(
        model="anthropic/no-such-model",
        max_tokens=16,
        messages=[{"role": "user", "content": "Hi"}],
    )
except anthropic.APIConnectionError as error:
    raise SystemExit(f"Could not reach the API: {error}")
except anthropic.RateLimitError as error:
    print("Rate limited. Retry after", error.response.headers.get("retry-after"), "seconds")
except anthropic.APIStatusError as error:
    print("status:", error.status_code, "type:", error.type)
    # request-id is set once a request reaches a model; x-request-id is on every response.
    print("request-id:", error.request_id)
    print("x-request-id:", error.response.headers.get("x-request-id"))
```

```python tab="OpenAI SDK" test
import os

import openai

client = openai.OpenAI(base_url="https://api.router.ezat.io/openai/v1", api_key=os.environ["EZAT_API_KEY"])

try:
    client.chat.completions.create(
        model="anthropic/no-such-model",
        max_completion_tokens=16,
        messages=[{"role": "user", "content": "Hi"}],
    )
except openai.APIConnectionError as error:
    raise SystemExit(f"Could not reach the API: {error}")
except openai.RateLimitError as error:
    print("Rate limited. Retry after", error.response.headers.get("retry-after"), "seconds")
except openai.APIStatusError as error:
    print("status:", error.status_code, "code:", error.code)
    print("x-request-id:", error.request_id)
    print("request-id:", error.response.headers.get("request-id"))
```

| Anthropic SDK | OpenAI SDK | Status |
| --- | --- | --- |
| `BadRequestError` | `BadRequestError` | 400 |
| `AuthenticationError` | `AuthenticationError` | 401 |
| `APIStatusError` with `status_code` 402 | `APIStatusError` with `status_code` 402 | 402: add credit |
| `PermissionDeniedError` | `PermissionDeniedError` | 403 |
| `NotFoundError` | `NotFoundError` | 404 |
| `RequestTooLargeError` | `APIStatusError` with `status_code` 413 | 413 |
| `RateLimitError` | `RateLimitError` | 429 |
| `InternalServerError` | `InternalServerError` | 500 and above |
| `OverloadedError` | `InternalServerError` | Overloaded: 529 (Anthropic format), 503 (OpenAI format) |
| `APIConnectionError` | `APIConnectionError` | No response: network problem or timeout |

What each status means and whether to retry it is on [Errors](https://docs.router.ezat.io/reference/errors.md).

## For AI agents

1. Set `base_url` to `https://api.router.ezat.io` for `anthropic`, or `https://api.router.ezat.io/openai/v1` for `openai`, and read the key from `os.environ["EZAT_API_KEY"]`.
2. Use model ids from `GET https://api.router.ezat.io/v1/models`; don't invent them.
3. `max_tokens` is required with the Anthropic SDK.
4. Don't add your own retry loop on top of the SDK's for 429 and 5xx unless you need more than its two retries; raise `max_retries` instead.
5. Log `request-id` (or `x-request-id`) from errors so the user can give it to support@ezat.io.