Metadata-Version: 2.4
Name: aetia-gateway-sdk
Version: 1.1.0
Summary: Public Python SDK for the Aetia LLM Gateway — governed inference, SSE streaming, async jobs, and tamper-evident run records.
Author-email: Aetia <dev@aetia.ai>
License: Proprietary
Project-URL: Homepage, https://aetia.ai
Project-URL: Source, https://github.com/aetia-ai/aetia
Keywords: aetia,llm,gateway,inference,sdk,openai,anthropic
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.0
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Requires-Dist: pytest-asyncio>=0.23; extra == "test"

# aetia-gateway-sdk

The official Python SDK for the **Aetia LLM Gateway** — a governed, versioned
inference facade. It wraps authentication, HTTP, and SSE so you never hand-roll
requests: typed models mirror the API contract exactly, and a contract test keeps
the SDK and the gateway in lockstep (`GATEWAY_CONTRACT_VERSION`).

```bash
pip install aetia-gateway-sdk
```

Requires Python 3.11+. The client is **async** (built on `httpx.AsyncClient`).

## Authentication — two modes

The gateway has two external front doors; the SDK supports both.

### API key (`aek_…`)

A long-lived key, sent directly on every request.

```python
import asyncio
from aetia_gateway_sdk import GatewayClient, ApiKey

async def main():
    async with GatewayClient("https://api.aetia.ai", ApiKey("aek_your_key")) as client:
        result = await client.infer(model="openai:gpt-4o-mini", input="Hello, world")
        print(result.output)

asyncio.run(main())
```

### Client credentials (OAuth2 client-credentials)

Pass a `client_id` / `secret`; the SDK exchanges them at
`POST /api/v1/auth/service-token` for a short-lived scoped bearer, **caches** it,
and **auto-refreshes** it before expiry and once on a `401`. A burst of
concurrent requests coalesces onto a single token exchange.

```python
from aetia_gateway_sdk import GatewayClient, ClientCredentials

creds = ClientCredentials(client_id="svc_...", secret="...")
async with GatewayClient("https://api.aetia.ai", creds) as client:
    result = await client.infer(model="openai:gpt-4o-mini", input="Hello")
    print(result.output, result.usage)
```

> **`base_url` is the host root** (e.g. `https://api.aetia.ai`), not the
> `/gateway` prefix — the SDK builds the full `/api/v1/...` paths itself.

## The surface

### Synchronous inference

```python
result = await client.infer(
    model="openai:gpt-4o-mini",
    input="Summarize the meeting notes.",
    options={"temperature": 0.2, "max_tokens": 256},  # provider params go here
    trace_id="my-trace-123",                            # echoed as correlation_id
)
result.output          # the model output
result.routed_alias    # the provider account the engine routed to
result.usage           # token accounting
result.finish_reason
result.correlation_id  # == trace_id (or a server-stamped id)
```

Unknown top-level fields are rejected **client-side** before any HTTP call — put
provider parameters in `options`, not at the top level.

### Streaming (SSE)

`stream()` is an async context manager + iterator. It yields the engine's delta
events and, after iteration, exposes the terminal `usage` / `finish_reason` /
`correlation_id` plus the assembled `text`.

```python
async with client.stream(model="openai:gpt-4o-mini", input="Write a haiku") as stream:
    async for event in stream:
        ...  # each engine delta event (dict)
    print(stream.text)            # assembled output
    print(stream.usage, stream.correlation_id)
```

Streaming requires a provider-qualified model (`alias:model_id`). A non-streaming
provider raises `StreamingUnsupportedError` at open time (never a silent degrade).

### Async inference jobs

```python
submitted = await client.submit_job(model="openai:gpt-4o-mini", input="long batch job")
status = await client.get_job(submitted.job_id)
if status.status == "succeeded":
    print(status.result)
```

### Run records (tamper-evident WORM ledger)

```python
receipt = await client.submit_run(
    "run-42",
    usage={"total_tokens": 1200},
    project="deep-research",   # arbitrary consumer fields ride along verbatim
)
receipt.ledger_event_id, receipt.record_hash, receipt.previous_hash  # inclusion proof

view = await client.get_run("run-42")   # tenant-scoped read-back
runs = await client.list_runs()
```

### Catalog & usage

```python
models = await client.list_models()   # list[GatewayModelEntry], tenant-scoped
usage = await client.usage()          # own-usage counts + honest rate-limit posture
```

## Errors

Every non-2xx response raises a typed error (all subclasses of `GatewayError`),
carrying `status_code`, the machine `error_code`, `detail`, `correlation_id`, and
`retry_after` where present:

| Class | HTTP | Meaning |
|-------|------|---------|
| `AuthenticationError` | 401 | Missing/invalid/expired/rejected credential |
| `PermissionDeniedError` | 403 | Authenticated but missing scope / cross-tenant |
| `NotFoundError` | 404 | Unknown job/run for this tenant |
| `RequestValidationError` | 422 | Server-side request validation failed |
| `RateLimitError` | 429 | Rate limited (`retry_after`) |
| `StreamingUnsupportedError` | 400 | Streaming requested on a non-streaming provider |
| `ServerError` | 5xx | Upstream/engine failure |
| `StreamError` | — | An error event mid-stream |

Transient failures (429, and 5xx for idempotent requests) are retried with
backoff that honors `Retry-After`.

## Contract versioning

`aetia_gateway_sdk.GATEWAY_CONTRACT_VERSION` pins the semantic gateway contract
this SDK targets. The v1 contract is additive-compatible; a breaking change moves
the API to `/v2` and the SDK to `2.x`. The SDK's `tests/test_contract.py` asserts
the SDK's models match the published external OpenAPI
(`GET /api/v1/gateway/openapi.json`) in both directions, so drift fails the build.

## Development

```bash
pip install -e "sdk/python[test]"
# The in-process tests run against a real backend + real auth (only the LLM is
# mocked); the contract test needs only aetia_webapp importable (no DB).
pytest sdk/python/tests
```

## License

Proprietary — © Aetia.
