Metadata-Version: 2.4
Name: tokenrouter
Version: 2.0.0
Summary: Official Python SDK for TokenRouter — the OpenAI-compatible AI gateway
Project-URL: Homepage, https://tokenrouter.io
Project-URL: Documentation, https://docs.tokenrouter.io
License: MIT
License-File: LICENSE
Keywords: ai,chat,completions,gateway,llm,openai,tokenrouter
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27
Description-Content-Type: text/markdown

# TokenRouter Python SDK

Official Python client for [TokenRouter](https://tokenrouter.io) — the OpenAI-compatible AI gateway. Python 3.9+, one dependency (`httpx`).

```bash
pip install tokenrouter
```

## Quickstart

```python
from tokenrouter import TokenRouter

client = TokenRouter()  # reads TOKENROUTER_API_KEY (and TOKENROUTER_BASE_URL)

completion = client.chat.completions.create(
    model="auto",  # or "openai/gpt-5-mini", "claude-sonnet-4-5", "auto:cost", ...
    messages=[{"role": "user", "content": "Hello!"}],
)
print(completion.choices[0].message.content)
print(completion.usage.total_tokens)      # attribute access
print(completion["usage"]["total_tokens"])  # dict access
raw = completion.model_dump()             # plain dict
```

## Migrating from the OpenAI SDK

TokenRouter speaks the OpenAI wire protocol, so migration is just changing the import and the key:

```diff
- from openai import OpenAI
- client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
+ from tokenrouter import TokenRouter
+ client = TokenRouter(api_key=os.environ["TOKENROUTER_API_KEY"])
```

Or keep using the `openai` package and just point it at the gateway:

```python
from openai import OpenAI
client = OpenAI(
    api_key=os.environ["TOKENROUTER_API_KEY"],
    base_url="https://api.tokenrouter.io/v1",
)
```

## Streaming

```python
stream = client.chat.completions.create(
    model="auto",
    messages=[{"role": "user", "content": "Write a haiku."}],
    stream=True,
    stream_options={"include_usage": True},  # opt in to a final usage frame
)
for chunk in stream:
    if chunk.choices:
        print(chunk.choices[0].delta.get("content") or "", end="")
    if chunk.get("usage"):
        print("\nusage:", chunk.usage.model_dump())
```

Streams are context managers. If you might stop iterating early (`break`,
`return`), use `with` so the HTTP connection is released deterministically
(iterating to the end closes it either way):

```python
with client.chat.completions.create(model="auto", messages=messages, stream=True) as stream:
    for chunk in stream:
        if should_stop(chunk):
            break

# async: async with await client.chat.completions.create(..., stream=True) as stream:
```

## Async

```python
from tokenrouter import AsyncTokenRouter

client = AsyncTokenRouter()

completion = await client.chat.completions.create(
    model="auto", messages=[{"role": "user", "content": "Hello!"}]
)

stream = await client.chat.completions.create(model="auto", messages=[...], stream=True)
async for chunk in stream:
    ...
```

## Responses API

```python
response = client.responses.create(
    model="auto",
    instructions="You are terse.",
    input="What is an AI gateway?",
)

# Streaming: an iterator of response.* events (ends when the connection closes)
for event in client.responses.create(model="auto", input="Hi", stream=True):
    if event.type == "response.output_text.delta":
        print(event.delta, end="")
```

> TokenRouter's Responses API is stateless: `previous_response_id` and `store=True` are not supported.

## Models and embeddings

```python
models = client.models.list()  # includes 'auto', 'auto:cost|latency|quality|balanced'
embeddings = client.embeddings.create(
    model="openai/text-embedding-3-small",
    input="The quick brown fox",
)
```

## Errors

All API errors are typed and carry `status_code`, `code`, and `param`:

| Class | Status | Codes |
| --- | --- | --- |
| `AuthenticationError` | 401 | `invalid_api_key` |
| `PermissionError` | 403 | `model_not_allowed`, `subscription_inactive` |
| `NotFoundError` | 404 | `model_not_found` |
| `RateLimitError` | 429 | `rate_limit_exceeded`, `monthly_quota_exceeded` (has `retry_after`) |
| `BudgetExceededError` | 429 | `budget_exceeded` (subclass of `RateLimitError`) |
| `APIError` | any | everything else, e.g. `provider_key_missing`, `upstream_error` |
| `APIConnectionError` / `APITimeoutError` | — | network failures / timeouts |

```python
from tokenrouter import RateLimitError

try:
    client.chat.completions.create(model="auto", messages=messages)
except RateLimitError as err:
    print(f"rate limited, retry in {err.retry_after}s")
```

## Retries and configuration

Failed requests are retried twice by default with exponential backoff — on 429 (honouring `Retry-After`), 5xx, and network errors. Other 4xx are never retried, and a stream is never retried once it has started.

```python
client = TokenRouter(
    api_key="tr_...",
    base_url="https://api.tokenrouter.io/v1",  # or TOKENROUTER_BASE_URL
    max_retries=2,   # 0 disables retries
    timeout=60.0,    # seconds, passed to httpx
)
```

Every request method also accepts per-call overrides:

```python
client.chat.completions.create(
    model="auto",
    messages=messages,
    timeout=10.0,                          # seconds, this request only
    extra_headers={"X-Request-Tag": "cron"},
)
```

## Development

```bash
uv sync
uv run pytest -q
TOKENROUTER_API_KEY=tr_... uv run python scripts/smoke.py
```
