Skip to content

Commit 2ffc205

Browse files
njbrakeclaude
andcommitted
docs: add AGENTS.md/CLAUDE.md agent guide, refresh README
Add an AGENTS.md agent guide (with CLAUDE.md symlinked to it, matching the otari gateway repo) documenting the Option C architecture, the generated core vs hand-written shell seam, both auth modes, the endpoint-coverage drift gate, and the exact build/test/lint commands. Fix the README: add the 409 BatchNotCompleteError row and correct the stale 'built on the OpenAI SDK' claim (the SDK is typed from the gateway's OpenAPI spec and has no openai dependency). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c57cb5e commit 2ffc205

3 files changed

Lines changed: 104 additions & 8 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): "Option C"
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 & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
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

@@ -46,7 +46,7 @@ Prefer to keep secrets out of code? Set `OTARI_AI_TOKEN` in your environment and
4646

4747
## Self-hosting the gateway
4848

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:
49+
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:
5050

5151
```python
5252
client = OtariClient(
@@ -57,14 +57,14 @@ client = OtariClient(
5757

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

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.
60+
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.
6161

6262
## Installation
6363

6464
### Requirements
6565

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

6969
### Install
7070

@@ -91,9 +91,9 @@ export GATEWAY_API_KEY="your-key-here"
9191

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

94-
## otari-gateway
94+
## About otari
9595

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:
96+
This Python SDK is a client for [otari](https://github.com/mozilla-ai/otari), the open-source core that powers [otari.ai](https://otari.ai). otari is an OpenAI-compatible proxy server that adds enterprise-grade features on top of the core library:
9797

9898
- **Budget Management** - Enforce spending limits with automatic daily, weekly, or monthly resets
9999
- **API Key Management** - Issue, revoke, and monitor virtual API keys without exposing provider credentials
@@ -285,6 +285,7 @@ except RateLimitError as e:
285285
| 401, 403 | `AuthenticationError` | Invalid or missing credentials |
286286
| 402 | `InsufficientFundsError` | Budget or credits exhausted |
287287
| 404 | `ModelNotFoundError` | Model not found, or no provider key configured for the requested provider. The exception's `message` carries the gateway's detail. |
288+
| 409 | `BatchNotCompleteError` | Batch results requested before the batch finished |
288289
| 429 | `RateLimitError` | Rate limit exceeded (includes `retry_after`) |
289290
| 502 | `UpstreamProviderError` | Upstream provider unreachable |
290291
| 504 | `GatewayTimeoutError` | Gateway timed out waiting for provider |
@@ -317,7 +318,7 @@ async with AsyncOtariClient(api_base="http://localhost:8000") as client:
317318

318319
- **Simple, unified interface** - Single client for all providers through the gateway, switch models with just a string change
319320
- **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+
- **Typed from the gateway spec** - The typed core is generated from otari's OpenAPI spec, so the client tracks the gateway's real contract (no `openai` dependency)
321322
- **Sync and async** - Use the synchronous `OtariClient` or the asynchronous `AsyncOtariClient`, both with the same typed interface
322323
- **Stays framework-agnostic** so it can be used across different projects and use cases
323324
- **Battle-tested** - Powers our own production tools ([any-agent](https://github.com/mozilla-ai/any-agent))

0 commit comments

Comments
 (0)