Skip to content

Commit 9cfb1b6

Browse files
njbrakeclaude
andcommitted
docs: add AGENTS.md/CLAUDE.md agent guide, refresh README
Add an AGENTS.md agent guide (CLAUDE.md symlinked to it, matching the otari repo) covering the SDK architecture, both auth modes, the endpoint-coverage drift gate, and the exact uv build/test/lint commands. README: add the 409 BatchNotCompleteError row, correct the stale 'built on the OpenAI Python SDK' claim, retitle the heading to 'Otari Python Client SDK', and rebrand otari-gateway wording to otari. Drop the internal 'Option C' codename from source docstrings/comments. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c57cb5e commit 9cfb1b6

10 files changed

Lines changed: 111 additions & 49 deletions

File tree

AGENTS.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# AGENTS.md
2+
3+
Guidance for agentic coding tools working in this repository.
4+
Scope: entire repo.
5+
6+
`CLAUDE.md` is a symlink to this file. Always edit `AGENTS.md` directly; never modify `CLAUDE.md`.
7+
8+
## Project Snapshot
9+
- Project: `otari` (PyPI package name `otari`) — the **Python client SDK** for the otari
10+
gateway / platform. `OtariClient` (sync) and `AsyncOtariClient` (async) talk to a running
11+
gateway over HTTP.
12+
- Language/runtime: Python 3.11+ (CI matrix: 3.11, 3.12, 3.13).
13+
- Package manager + task runner: `uv`.
14+
- Source root: `src/otari` (imported as `otari`).
15+
- Tests: `tests/unit` (mocked, offline) and `tests/integration` (real gateway).
16+
17+
## Architecture (Big Picture)
18+
This SDK is a **thin, hand-written shell over an OpenAPI-generated typed core**. Read these
19+
together before changing request behavior.
20+
21+
- **Generated core — `src/otari/_client/`**: produced by OpenAPI Generator from the gateway's
22+
OpenAPI spec. It is **generated, not hand-edited.** Regeneration happens upstream in the
23+
gateway repo (`.github/workflows/gateway-sdk-codegen.yml`), which opens a
24+
`sdk-codegen/client-core` PR here. The core is **excluded from ruff and mypy**
25+
(`pyproject.toml`: `extend-exclude` / mypy `exclude`). Do not edit it to fix a lint error;
26+
fix the shell or the upstream spec/generator instead.
27+
- **Hand-written shell** (everything else under `src/otari/`):
28+
- `client.py` / `async_client.py` — ergonomic `OtariClient` / `AsyncOtariClient` with
29+
`completion`, `response`, `message`, `embedding`, `moderation`, `rerank`, `list_models`,
30+
batch operations, and a `control_plane` accessor.
31+
- `_base.py` — shared logic: auth-mode resolution, default headers, URL normalization, and
32+
the single seam where generated `ApiException` is caught and mapped to typed errors.
33+
- `_streaming.py` — hand-written SSE shim. The generated core buffers and **cannot stream**,
34+
so streaming endpoints use raw `httpx` + a line/event parser. Chat streaming yields typed
35+
`ChatCompletionChunk`; responses/messages streaming yields raw event `dict`s (no chunk
36+
model exists for those).
37+
- `errors.py` — typed error hierarchy (`OtariError` base + subclasses).
38+
- `types.py` — re-exports of generated models plus hand-written TypedDicts (batch/options).
39+
- `control_plane.py` — wrapper over the management endpoints (keys/users/budgets/pricing/usage).
40+
41+
### Two auth modes (must both keep working)
42+
Resolved in `_base.py` from constructor args, then environment:
43+
- **Platform** (`OTARI_AI_TOKEN` / `platform_token`): `Authorization: Bearer <token>`, base URL
44+
defaults to `https://api.otari.ai`.
45+
- **Self-hosted** (`api_key` + `api_base`, env `GATEWAY_API_KEY` / `GATEWAY_API_BASE`):
46+
`Otari-Key` header; `api_base` is **required** in this mode.
47+
Error mapping applies in both modes; do not regress one when changing the other.
48+
49+
### Endpoint-coverage drift gate
50+
`tests/unit/test_endpoint_coverage.py` fetches the canonical gateway spec
51+
(`https://raw.githubusercontent.com/mozilla-ai/otari/main/docs/public/openapi.json`) and
52+
asserts every gateway endpoint is accounted for in `sdk-endpoints.txt` (`[covered]` or
53+
`[excluded]` with a reason). A new gateway endpoint with no wrapper and no explicit exclusion
54+
fails CI. When you add or intentionally skip an endpoint, update `sdk-endpoints.txt`.
55+
56+
## Setup Commands
57+
- Install (dev): `uv sync --extra dev`
58+
59+
## Test Commands
60+
- Full suite: `uv run pytest`
61+
- Unit only: `uv run pytest tests/unit`
62+
- Single test: `uv run pytest tests/unit/test_client.py::TestOtariClient::test_completion -v`
63+
- Drift gate (needs network): `uv run pytest tests/unit/test_endpoint_coverage.py -v`
64+
- Integration tests under `tests/integration/` spawn / require a real gateway and are skipped
65+
when one is not available.
66+
67+
## Lint / Typecheck / Build Commands
68+
- Lint: `uv run ruff check .`
69+
- Typecheck (mypy strict): `uv run mypy src/`
70+
- Build: `uv build`
71+
72+
## Repository Conventions
73+
- `from __future__ import annotations` at the top of modules; `TYPE_CHECKING` for type-only
74+
imports; import `Callable`/`Iterator` from `collections.abc`.
75+
- mypy is `strict`; the generated `otari._client` is excluded. New/changed shell code must be
76+
fully typed. Use `@overload` for streaming polymorphism (`stream=True` vs not), as `client.py`
77+
already does.
78+
- Public API is exported from `src/otari/__init__.py` (clients, errors, types); don't remove or
79+
rename exports without auditing callers.
80+
- Unit tests mock at the transport seam (the generated core's REST client) and use `respx` for
81+
the raw-`httpx` streaming path. Test classes are `Test<Feature>`.
82+
83+
## Change Validation Checklist
84+
- Touched request handling, auth, or errors → run `tests/unit` and confirm both auth modes still
85+
map errors correctly.
86+
- Touched streaming → run the streaming tests; verify chat yields `ChatCompletionChunk` and
87+
responses/messages yield raw dicts.
88+
- Added/removed an endpoint wrapper → update `sdk-endpoints.txt` and run the drift gate.
89+
- Always run `uv run ruff check .` and `uv run mypy src/` before opening a PR.
90+
91+
## Notes for Agents
92+
- Never hand-edit `src/otari/_client/`; it is regenerated from the gateway spec.
93+
- Prefer minimal, targeted edits; match existing typing and import style in touched files.
94+
- Preserve security-relevant behavior (header/auth handling, error-detail boundaries).

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
AGENTS.md

README.md

Lines changed: 9 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,23 @@
44

55
<div align="center">
66

7-
# otari (Python)
7+
# Otari Python Client SDK
88

99
![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)
1010
[![PyPI](https://img.shields.io/pypi/v/otari)](https://pypi.org/project/otari/)
1111
<a href="https://discord.gg/4gf3zXrQUc">
1212
<img src="https://img.shields.io/static/v1?label=Chat%20on&message=Discord&color=blue&logo=Discord&style=flat-square" alt="Discord">
1313
</a>
1414

15-
**Python client for [otari-gateway](https://github.com/mozilla-ai/otari).**
16-
Communicate with any LLM provider through the gateway using a single, typed interface.
15+
**Python client for [otari](https://github.com/mozilla-ai/otari), the open-source core that powers [otari.ai](https://otari.ai).**
16+
Communicate with any LLM provider through otari using a single, typed interface.
1717

1818
[TypeScript SDK](https://github.com/mozilla-ai/otari-sdk-ts) | [Documentation](https://mozilla-ai.github.io/otari/) | [Platform (Beta)](https://otari.ai/)
1919

2020
</div>
2121

22+
> New to otari? The [otari repo](https://github.com/mozilla-ai/otari) explains what it is and why you’d use it.
23+
2224
## Quickstart
2325

2426
Generate an API token at [otari.ai/organization-settings/api-tokens](https://otari.ai/organization-settings/api-tokens), then add a provider key (e.g. OpenAI) at [otari.ai/organization-settings/provider-keys](https://otari.ai/organization-settings/provider-keys) so the gateway can route requests to that provider. Then use the client:
@@ -46,7 +48,7 @@ Prefer to keep secrets out of code? Set `OTARI_AI_TOKEN` in your environment and
4648

4749
## Self-hosting the gateway
4850

49-
Prefer to run the gateway yourself instead of using the hosted otari.ai? Follow the setup in the [otari gateway repo](https://github.com/mozilla-ai/otari), then point the SDK at it:
51+
Prefer to run the gateway yourself instead of using the hosted otari.ai? Follow the setup in the [otari repo](https://github.com/mozilla-ai/otari), then point the SDK at it:
5052

5153
```python
5254
client = OtariClient(
@@ -57,14 +59,14 @@ client = OtariClient(
5759

5860
The SDK sends `api_key` via the custom `Otari-Key: Bearer …` header. Env: `GATEWAY_API_BASE` + `GATEWAY_API_KEY`.
5961

60-
Make sure your gateway has provider keys configured (e.g. OpenAI) so it can route requests upstream — see the [otari gateway repo](https://github.com/mozilla-ai/otari) for setup.
62+
Make sure your gateway has provider keys configured (e.g. OpenAI) so it can route requests upstream — see the [otari repo](https://github.com/mozilla-ai/otari) for setup.
6163

6264
## Installation
6365

6466
### Requirements
6567

6668
- Python 3.11 or newer
67-
- A running [otari-gateway](https://mozilla-ai.github.io/otari/gateway/overview/) instance
69+
- A running [otari](https://mozilla-ai.github.io/otari/gateway/overview/) instance
6870

6971
### Install
7072

@@ -91,33 +93,6 @@ export GATEWAY_API_KEY="your-key-here"
9193

9294
Alternatively, pass credentials directly when creating the client (see [Usage](#usage) examples).
9395

94-
## otari-gateway
95-
96-
This Python SDK is a client for [otari-gateway](https://github.com/mozilla-ai/otari), an **optional** FastAPI-based proxy server that adds enterprise-grade features on top of the core library:
97-
98-
- **Budget Management** - Enforce spending limits with automatic daily, weekly, or monthly resets
99-
- **API Key Management** - Issue, revoke, and monitor virtual API keys without exposing provider credentials
100-
- **Usage Analytics** - Track every request with full token counts, costs, and metadata
101-
- **Multi-tenant Support** - Manage access and budgets across users and teams
102-
103-
The gateway sits between your applications and LLM providers, exposing an OpenAI-compatible API that works with any supported provider.
104-
105-
### Quick Start
106-
107-
```bash
108-
docker run \
109-
-e GATEWAY_MASTER_KEY="your-secure-master-key" \
110-
-e OPENAI_API_KEY="your-api-key" \
111-
-p 8000:8000 \
112-
ghcr.io/mozilla-ai/otari/gateway:latest
113-
```
114-
115-
> **Note:** You can use a specific release version instead of `latest` (e.g., `1.2.0`). See [available versions](https://github.com/orgs/mozilla-ai/packages/container/package/otari%2Fgateway).
116-
117-
### Managed Platform (Beta)
118-
119-
Prefer a hosted experience? The [otari platform](https://otari.ai/) provides a managed control plane for keys, usage tracking, and cost visibility across providers, while still building on the same `otari` interfaces.
120-
12196
## Usage
12297

12398
> **Migrating from a previous version?** `OtariClient` is now **synchronous** — call its methods directly (no `await`). For asynchronous code, switch to `AsyncOtariClient`, which keeps the previous `await`-based API. See [Async usage](#async-usage).
@@ -285,6 +260,7 @@ except RateLimitError as e:
285260
| 401, 403 | `AuthenticationError` | Invalid or missing credentials |
286261
| 402 | `InsufficientFundsError` | Budget or credits exhausted |
287262
| 404 | `ModelNotFoundError` | Model not found, or no provider key configured for the requested provider. The exception's `message` carries the gateway's detail. |
263+
| 409 | `BatchNotCompleteError` | Batch results requested before the batch finished |
288264
| 429 | `RateLimitError` | Rate limit exceeded (includes `retry_after`) |
289265
| 502 | `UpstreamProviderError` | Upstream provider unreachable |
290266
| 504 | `GatewayTimeoutError` | Gateway timed out waiting for provider |
@@ -313,15 +289,6 @@ async with AsyncOtariClient(api_base="http://localhost:8000") as client:
313289
)
314290
```
315291

316-
## Why choose `otari`?
317-
318-
- **Simple, unified interface** - Single client for all providers through the gateway, switch models with just a string change
319-
- **Developer friendly** - Full type hints for better IDE support and clear, actionable error messages
320-
- **Leverages the OpenAI SDK** - Built on the official OpenAI Python SDK for maximum compatibility
321-
- **Sync and async** - Use the synchronous `OtariClient` or the asynchronous `AsyncOtariClient`, both with the same typed interface
322-
- **Stays framework-agnostic** so it can be used across different projects and use cases
323-
- **Battle-tested** - Powers our own production tools ([any-agent](https://github.com/mozilla-ai/any-agent))
324-
325292
## Development
326293

327294
```bash

src/otari/_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
:class:`~otari.async_client.AsyncOtariClient`) construct their own generated
88
``_client`` and httpx clients and implement the I/O methods on top of this base.
99
10-
Option C: the inference path is a thin shell over the OpenAPI-generated core in
10+
The inference path is a thin shell over the OpenAPI-generated core in
1111
:mod:`otari._client` (typed models + per-endpoint API classes). The generated
1212
``ApiException`` is the single error type all generated calls raise; this module
1313
maps it to the typed otari exception hierarchy in :mod:`otari.errors`.

src/otari/async_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""AsyncOtariClient: asynchronous Python client for the otari gateway.
22
3-
Option C: a thin async shell over the OpenAPI-generated core in
3+
A thin async shell over the OpenAPI-generated core in
44
:mod:`otari._client`. The generated core is synchronous (urllib3-based), so
55
non-streaming calls are dispatched to a worker thread via ``asyncio.to_thread``;
66
streaming is natively async over ``httpx.AsyncClient`` and the SSE shim in

src/otari/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""OtariClient: synchronous Python client for the otari gateway.
22
3-
Option C: a thin, ergonomic shell over the OpenAPI-generated core in
3+
A thin, ergonomic shell over the OpenAPI-generated core in
44
:mod:`otari._client`. Non-streaming calls go through the generated typed API
55
classes (returning typed models such as ``ChatCompletion``); streaming calls go
66
through the hand-written SSE shim in :mod:`otari._streaming`; generated

src/otari/control_plane.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Typed client for the gateway control-plane (management) endpoints.
22
33
Wraps the OpenAPI-generated :mod:`otari._client` core (the same core that backs
4-
the inference path under Option C). The control-plane endpoints (API keys,
4+
the inference path). The control-plane endpoints (API keys,
55
users, budgets, pricing, usage) authenticate with
66
``Authorization: Bearer <admin/master key>``, which is distinct from the
77
``Otari-Key`` virtual key used for inference. Obtain an instance via

tests/unit/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Shared test helpers for mocking the generated core's transport.
22
3-
Option C wires the SDK over the OpenAPI-generated core (:mod:`otari._client`),
3+
The SDK is a thin shell over the OpenAPI-generated core (:mod:`otari._client`),
44
whose non-streaming calls go through ``RESTClientObject.request`` (urllib3) and
55
whose streaming path is a hand-written raw httpx request. These helpers mock both
66
seams without a live gateway:

tests/unit/test_async_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Tests for the asynchronous AsyncOtariClient (Option C: generated-core shell).
1+
"""Tests for the asynchronous AsyncOtariClient (generated-core shell).
22
33
The async client dispatches the (synchronous) generated calls off-thread via
44
``asyncio.to_thread`` and streams natively over ``httpx.AsyncClient``. Non-

tests/unit/test_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Tests for the synchronous OtariClient (Option C: generated-core shell).
1+
"""Tests for the synchronous OtariClient (generated-core shell).
22
33
Covers constructor / auth-mode wiring, request shaping, typed response parsing,
44
generated ``ApiException`` -> typed error mapping, and the hand-written SSE

0 commit comments

Comments
 (0)