Skip to content

Commit c57cb5e

Browse files
njbrakeclaude
andauthored
Auto generated client (#3)
* Add generated control-plane client (preview) Generated control-plane client (keys, users, budgets, pricing, usage) from the otari gateway OpenAPI spec via OpenAPI Generator. Preview for review and integration design; not yet wired into the public client. Part of mozilla-ai/otari#96 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add control-plane integration tests (generated client) Full CRUD lifecycle tests for every control-plane endpoint (keys, users, budgets, pricing, usage) driving the generated client against a real gateway started on SQLite with a master key (no provider creds needed). Verified 22 endpoint operations pass; documents the Bearer-auth requirement and the per-language test pattern. Part of mozilla-ai/otari#96 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Wire generated control-plane client into OtariClient - Regenerate the control-plane client as a clean subpackage (otari._control_plane). - Add OtariClient.control_plane: typed accessors (keys/users/budgets/pricing/usage) sharing one client authed with Authorization: Bearer using an admin credential (admin_key / GATEWAY_ADMIN_KEY / platform_token). Management endpoints require Bearer, not the Otari-Key inference header. - Add admin_key param + ControlPlane facade; export ControlPlane. - Exclude the generated subpackage from ruff/mypy; add its runtime deps. - Integration tests now drive the public control_plane surface end-to-end (full CRUD per endpoint + admin-credential guard); 6 pass against a live gateway on SQLite. Part of mozilla-ai/otari#96 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add PR CI and make control-plane integration tests skip without a gateway - Add .github/workflows/ci.yml: ruff + mypy + pytest on push/PR across Python 3.11-3.13 (the repo previously only ran checks on release). - Integration tests skip cleanly when no gateway is on PATH (set OTARI_GATEWAY_CMD to run them), so CI is green without a gateway and passes with one. - Register the 'integration' marker; per-file-ignore the subprocess/URL audit rules for the test harness. Part of mozilla-ai/otari#96 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Option C: rebuild SDK as a thin shell over the generated typed core Replace the OpenAI-SDK delegation with a hand-written shell over the OpenAPI-generated core (otari._client), generated from the otari spec. The gateway is only OpenAI-compatible and has endpoints (notably the Anthropic-shaped /messages) the OpenAI SDK cannot represent; generating from the otari spec models the real contract. Core (unmodified generator output) replaces the old _control_plane subpackage and covers every endpoint with typed request/response models. Shell (the real work): - Auth modes preserved: platform -> Authorization: Bearer <token>; non-platform -> Otari-Key: Bearer <key>; control-plane -> Authorization: Bearer <admin/master key>. Fed into the generated ApiClient default headers and the streaming shim's httpx requests. - Ergonomic methods keep the existing public names/signatures (completion/response/embedding/moderation/rerank/list_models/batches) and add message(...) for the previously-missing /messages endpoint. control_plane accessor now backed by _client. - SSE streaming shim (_streaming.py): the generated core buffers and cannot stream, so stream=True does a raw httpx streaming POST, parses text/event-stream framing, terminates on [DONE], and yields typed ChatCompletionChunk (chat) / parsed JSON (responses, messages). - Typed error mapping: generated ApiException (.status/.body) -> the errors.py hierarchy (Authentication 401/403, InsufficientFunds 402, ModelNotFound 404, BatchNotComplete 409, RateLimit 429, GatewayTimeout 504, UpstreamProvider 502/5xx, UnsupportedCapability cross-mode, generic OtariError). The streaming path adapts failed responses through the same mapper. - Async client wraps the sync generated core via asyncio.to_thread and streams natively over httpx.AsyncClient. Drop the openai dependency from inference. Update ruff/mypy excludes to src/otari/_client. Rewrite unit tests to mock the generated transport (RESTClientObject.request) and respx for SSE; control-plane integration tests updated to import from _client and verified green against a live gateway. Note: chat streaming cannot be live-verified in this sandbox (no provider key on the gateway); the SSE shim is unit-tested over mocked chunk bytes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add endpoint-coverage drift gate Fail CI when the gateway OpenAPI spec exposes an endpoint the SDK's public API does not account for. A checked-in manifest (sdk-endpoints.txt) pins the covered and intentionally-excluded endpoint sets; a pytest test fetches the canonical spec and asserts spec is a subset of (covered + excluded), naming any unaccounted endpoint. Wired as a dedicated CI step. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2a76998 commit c57cb5e

203 files changed

Lines changed: 36110 additions & 1984 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
test:
15+
runs-on: ubuntu-latest
16+
strategy:
17+
matrix:
18+
python-version: ["3.11", "3.12", "3.13"]
19+
steps:
20+
- uses: actions/checkout@v4
21+
22+
- uses: astral-sh/setup-uv@v6
23+
with:
24+
python-version: ${{ matrix.python-version }}
25+
26+
- name: Install dependencies
27+
run: uv sync --extra dev
28+
29+
- name: Lint
30+
run: uv run ruff check .
31+
32+
- name: Type check
33+
run: uv run mypy src/
34+
35+
- name: Test
36+
# Integration tests skip automatically when no gateway is on PATH.
37+
run: uv run pytest
38+
39+
- name: Endpoint-coverage drift gate
40+
# Fetches the canonical gateway OpenAPI spec and fails if it exposes an
41+
# endpoint absent from sdk-endpoints.txt ([covered] or [excluded]).
42+
# Network is available in CI, so this must not skip here.
43+
run: uv run pytest tests/unit/test_endpoint_coverage.py -v

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,21 @@ response = client.response(
195195
print(response.output_text)
196196
```
197197

198+
### Messages API (Anthropic-shaped)
199+
200+
The gateway's `/messages` endpoint (Anthropic message shape) is exposed via
201+
`message(...)`. Set `stream=True` to iterate raw message-stream event dicts.
202+
203+
```python
204+
message = client.message(
205+
model="anthropic:claude-3-5-sonnet",
206+
messages=[{"role": "user", "content": "Hello!"}],
207+
max_tokens=256,
208+
)
209+
210+
print(message.content)
211+
```
212+
198213
### Embeddings
199214

200215
```python

pyproject.toml

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@ classifiers = [
2222
"Typing :: Typed",
2323
]
2424
dependencies = [
25-
"openai>=1.99.3",
25+
# Streaming shim (raw SSE over httpx); the generated core cannot stream.
2626
"httpx>=0.25.0",
27+
# Runtime deps of the OpenAPI-generated core (otari._client).
28+
"urllib3>=2.1.0",
29+
"python-dateutil>=2.8.2",
30+
"pydantic>=2.0.0",
2731
]
2832

2933
[project.urls]
@@ -36,6 +40,7 @@ Issues = "https://github.com/mozilla-ai/otari-sdk-python/issues"
3640
dev = [
3741
"pytest>=8.0",
3842
"pytest-asyncio>=0.24",
43+
"respx>=0.21",
3944
"ruff>=0.8",
4045
"mypy>=1.13",
4146
]
@@ -46,15 +51,25 @@ packages = ["src/otari"]
4651
[tool.pytest.ini_options]
4752
testpaths = ["tests"]
4853
asyncio_mode = "auto"
54+
markers = [
55+
"integration: tests that require a running gateway (skipped if none is available)",
56+
]
4957

5058
[tool.ruff]
5159
target-version = "py311"
5260
line-length = 120
61+
# Generated core client (OpenAPI Generator output) is not hand-linted.
62+
extend-exclude = ["src/otari/_client"]
5363

5464
[tool.ruff.lint]
5565
select = ["E", "F", "I", "N", "W", "UP", "S", "B", "A", "C4", "DTZ", "T10", "ISC", "ICN", "PIE", "PT", "RSE", "RET", "SIM", "TID", "TCH", "ARG", "PLC", "PLE", "PLW", "TRY", "FLY", "PERF", "RUF"]
5666
ignore = ["S101", "TRY003", "PLW0603"]
5767

68+
[tool.ruff.lint.per-file-ignores]
69+
# Integration tests spawn the gateway subprocess and poll it over HTTP; the
70+
# subprocess/URL/temp-file audit rules don't apply to that test harness.
71+
"tests/integration/*" = ["S603", "S310", "SIM115", "SIM117", "PLC0415", "TC003"]
72+
5873
[tool.ruff.lint.isort]
5974
known-first-party = ["otari"]
6075

@@ -63,3 +78,10 @@ python_version = "3.11"
6378
strict = true
6479
warn_return_any = true
6580
warn_unused_configs = true
81+
# Generated core client (OpenAPI Generator output) is not type-checked here.
82+
exclude = ["src/otari/_client/"]
83+
84+
[[tool.mypy.overrides]]
85+
module = ["otari._client.*"]
86+
ignore_errors = true
87+
ignore_missing_imports = true

sdk-endpoints.txt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Endpoint-coverage drift manifest for the otari gateway.
2+
#
3+
# This file pins which gateway OpenAPI endpoints this SDK's PUBLIC API accounts
4+
# for. CI fetches the canonical spec
5+
# https://raw.githubusercontent.com/mozilla-ai/otari/main/docs/public/openapi.json
6+
# computes its set of "METHOD path" pairs (excluding /health* meta routes), and
7+
# asserts that set is a subset of (covered + excluded). A new gateway endpoint
8+
# in neither section FAILS the build: add a wrapper and list it under [covered],
9+
# or deliberately defer it under [excluded] with a one-word reason.
10+
#
11+
# All four otari SDKs (python/ts/go/rust) keep this list identical: they target
12+
# the same gateway. Lines are "METHOD /path"; blank lines and # comments ignore.
13+
14+
[covered]
15+
# Inference
16+
POST /v1/chat/completions
17+
POST /v1/responses
18+
POST /v1/messages
19+
POST /v1/embeddings
20+
POST /v1/moderations
21+
POST /v1/rerank
22+
GET /v1/models
23+
# Batches
24+
POST /v1/batches
25+
GET /v1/batches
26+
GET /v1/batches/{batch_id}
27+
POST /v1/batches/{batch_id}/cancel
28+
GET /v1/batches/{batch_id}/results
29+
# Control plane: keys
30+
POST /v1/keys
31+
GET /v1/keys
32+
GET /v1/keys/{key_id}
33+
PATCH /v1/keys/{key_id}
34+
DELETE /v1/keys/{key_id}
35+
# Control plane: users
36+
POST /v1/users
37+
GET /v1/users
38+
GET /v1/users/{user_id}
39+
PATCH /v1/users/{user_id}
40+
DELETE /v1/users/{user_id}
41+
GET /v1/users/{user_id}/usage
42+
# Control plane: budgets
43+
POST /v1/budgets
44+
GET /v1/budgets
45+
GET /v1/budgets/{budget_id}
46+
PATCH /v1/budgets/{budget_id}
47+
DELETE /v1/budgets/{budget_id}
48+
# Control plane: pricing
49+
POST /v1/pricing
50+
GET /v1/pricing
51+
GET /v1/pricing/{model_key}
52+
GET /v1/pricing/{model_key}/history
53+
DELETE /v1/pricing/{model_key}
54+
# Control plane: usage
55+
GET /v1/usage
56+
57+
[excluded]
58+
POST /v1/audio/speech # binary, not yet wrapped
59+
POST /v1/audio/transcriptions # binary, not yet wrapped
60+
POST /v1/images/generations # binary, not yet wrapped
61+
GET /v1/models/{model_id} # redundant, list_models covers discovery

src/otari/__init__.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
platform_token="your-token-here",
1010
)
1111
12-
response = await client.completion(
12+
response = client.completion(
1313
model="openai:gpt-4o-mini",
1414
messages=[{"role": "user", "content": "Hello!"}],
1515
)
@@ -20,6 +20,7 @@
2020

2121
from otari.async_client import AsyncOtariClient
2222
from otari.client import OtariClient
23+
from otari.control_plane import ControlPlane
2324
from otari.errors import (
2425
AuthenticationError,
2526
BatchNotCompleteError,
@@ -32,23 +33,20 @@
3233
UpstreamProviderError,
3334
)
3435
from otari.types import (
35-
AsyncStream,
3636
BatchRequestItem,
3737
BatchResult,
3838
BatchResultError,
3939
BatchResultItem,
4040
ChatCompletion,
4141
ChatCompletionChunk,
42-
ChatCompletionMessageParam,
4342
CreateBatchParams,
4443
CreateEmbeddingResponse,
45-
EmbeddingCreateParams,
4644
ListBatchesOptions,
47-
Model,
45+
MessageResponse,
46+
ModelObject,
47+
ModerationResponse,
4848
OtariClientOptions,
49-
Response,
50-
ResponseStreamEvent,
51-
Stream,
49+
RerankResponse,
5250
)
5351

5452
try:
@@ -59,7 +57,6 @@
5957

6058
__all__ = [
6159
"AsyncOtariClient",
62-
"AsyncStream",
6360
"AuthenticationError",
6461
"BatchNotCompleteError",
6562
"BatchRequestItem",
@@ -68,22 +65,21 @@
6865
"BatchResultItem",
6966
"ChatCompletion",
7067
"ChatCompletionChunk",
71-
"ChatCompletionMessageParam",
68+
"ControlPlane",
7269
"CreateBatchParams",
7370
"CreateEmbeddingResponse",
74-
"EmbeddingCreateParams",
7571
"GatewayTimeoutError",
7672
"InsufficientFundsError",
7773
"ListBatchesOptions",
78-
"Model",
74+
"MessageResponse",
7975
"ModelNotFoundError",
76+
"ModelObject",
77+
"ModerationResponse",
8078
"OtariClient",
8179
"OtariClientOptions",
8280
"OtariError",
8381
"RateLimitError",
84-
"Response",
85-
"ResponseStreamEvent",
86-
"Stream",
82+
"RerankResponse",
8783
"UnsupportedCapabilityError",
8884
"UpstreamProviderError",
8985
]

0 commit comments

Comments
 (0)