Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4

- uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
run: uv sync --extra dev

- name: Lint
run: uv run ruff check .

- name: Type check
run: uv run mypy src/

- name: Test
# Integration tests skip automatically when no gateway is on PATH.
run: uv run pytest

- name: Endpoint-coverage drift gate
# Fetches the canonical gateway OpenAPI spec and fails if it exposes an
# endpoint absent from sdk-endpoints.txt ([covered] or [excluded]).
# Network is available in CI, so this must not skip here.
run: uv run pytest tests/unit/test_endpoint_coverage.py -v
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,21 @@ response = client.response(
print(response.output_text)
```

### Messages API (Anthropic-shaped)

The gateway's `/messages` endpoint (Anthropic message shape) is exposed via
`message(...)`. Set `stream=True` to iterate raw message-stream event dicts.

```python
message = client.message(
model="anthropic:claude-3-5-sonnet",
messages=[{"role": "user", "content": "Hello!"}],
max_tokens=256,
)

print(message.content)
```

### Embeddings

```python
Expand Down
24 changes: 23 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
"openai>=1.99.3",
# Streaming shim (raw SSE over httpx); the generated core cannot stream.
"httpx>=0.25.0",
# Runtime deps of the OpenAPI-generated core (otari._client).
"urllib3>=2.1.0",
"python-dateutil>=2.8.2",
"pydantic>=2.0.0",
]

[project.urls]
Expand All @@ -36,6 +40,7 @@ Issues = "https://github.com/mozilla-ai/otari-sdk-python/issues"
dev = [
"pytest>=8.0",
"pytest-asyncio>=0.24",
"respx>=0.21",
"ruff>=0.8",
"mypy>=1.13",
]
Expand All @@ -46,15 +51,25 @@ packages = ["src/otari"]
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
markers = [
"integration: tests that require a running gateway (skipped if none is available)",
]

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

[tool.ruff.lint]
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"]
ignore = ["S101", "TRY003", "PLW0603"]

[tool.ruff.lint.per-file-ignores]
# Integration tests spawn the gateway subprocess and poll it over HTTP; the
# subprocess/URL/temp-file audit rules don't apply to that test harness.
"tests/integration/*" = ["S603", "S310", "SIM115", "SIM117", "PLC0415", "TC003"]

[tool.ruff.lint.isort]
known-first-party = ["otari"]

Expand All @@ -63,3 +78,10 @@ python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_configs = true
# Generated core client (OpenAPI Generator output) is not type-checked here.
exclude = ["src/otari/_client/"]

[[tool.mypy.overrides]]
module = ["otari._client.*"]
ignore_errors = true
ignore_missing_imports = true
61 changes: 61 additions & 0 deletions sdk-endpoints.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Endpoint-coverage drift manifest for the otari gateway.
#
# This file pins which gateway OpenAPI endpoints this SDK's PUBLIC API accounts
# for. CI fetches the canonical spec
# https://raw.githubusercontent.com/mozilla-ai/otari/main/docs/public/openapi.json
# computes its set of "METHOD path" pairs (excluding /health* meta routes), and
# asserts that set is a subset of (covered + excluded). A new gateway endpoint
# in neither section FAILS the build: add a wrapper and list it under [covered],
# or deliberately defer it under [excluded] with a one-word reason.
#
# All four otari SDKs (python/ts/go/rust) keep this list identical: they target
# the same gateway. Lines are "METHOD /path"; blank lines and # comments ignore.

[covered]
# Inference
POST /v1/chat/completions
POST /v1/responses
POST /v1/messages
POST /v1/embeddings
POST /v1/moderations
POST /v1/rerank
GET /v1/models
# Batches
POST /v1/batches
GET /v1/batches
GET /v1/batches/{batch_id}
POST /v1/batches/{batch_id}/cancel
GET /v1/batches/{batch_id}/results
# Control plane: keys
POST /v1/keys
GET /v1/keys
GET /v1/keys/{key_id}
PATCH /v1/keys/{key_id}
DELETE /v1/keys/{key_id}
# Control plane: users
POST /v1/users
GET /v1/users
GET /v1/users/{user_id}
PATCH /v1/users/{user_id}
DELETE /v1/users/{user_id}
GET /v1/users/{user_id}/usage
# Control plane: budgets
POST /v1/budgets
GET /v1/budgets
GET /v1/budgets/{budget_id}
PATCH /v1/budgets/{budget_id}
DELETE /v1/budgets/{budget_id}
# Control plane: pricing
POST /v1/pricing
GET /v1/pricing
GET /v1/pricing/{model_key}
GET /v1/pricing/{model_key}/history
DELETE /v1/pricing/{model_key}
# Control plane: usage
GET /v1/usage

[excluded]
POST /v1/audio/speech # binary, not yet wrapped
POST /v1/audio/transcriptions # binary, not yet wrapped
POST /v1/images/generations # binary, not yet wrapped
GET /v1/models/{model_id} # redundant, list_models covers discovery
26 changes: 11 additions & 15 deletions src/otari/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
platform_token="your-token-here",
)

response = await client.completion(
response = client.completion(
model="openai:gpt-4o-mini",
messages=[{"role": "user", "content": "Hello!"}],
)
Expand All @@ -20,6 +20,7 @@

from otari.async_client import AsyncOtariClient
from otari.client import OtariClient
from otari.control_plane import ControlPlane
from otari.errors import (
AuthenticationError,
BatchNotCompleteError,
Expand All @@ -32,23 +33,20 @@
UpstreamProviderError,
)
from otari.types import (
AsyncStream,
BatchRequestItem,
BatchResult,
BatchResultError,
BatchResultItem,
ChatCompletion,
ChatCompletionChunk,
ChatCompletionMessageParam,
CreateBatchParams,
CreateEmbeddingResponse,
EmbeddingCreateParams,
ListBatchesOptions,
Model,
MessageResponse,
ModelObject,
ModerationResponse,
OtariClientOptions,
Response,
ResponseStreamEvent,
Stream,
RerankResponse,
)

try:
Expand All @@ -59,7 +57,6 @@

__all__ = [
"AsyncOtariClient",
"AsyncStream",
"AuthenticationError",
"BatchNotCompleteError",
"BatchRequestItem",
Expand All @@ -68,22 +65,21 @@
"BatchResultItem",
"ChatCompletion",
"ChatCompletionChunk",
"ChatCompletionMessageParam",
"ControlPlane",
"CreateBatchParams",
"CreateEmbeddingResponse",
"EmbeddingCreateParams",
"GatewayTimeoutError",
"InsufficientFundsError",
"ListBatchesOptions",
"Model",
"MessageResponse",
"ModelNotFoundError",
"ModelObject",
"ModerationResponse",
"OtariClient",
"OtariClientOptions",
"OtariError",
"RateLimitError",
"Response",
"ResponseStreamEvent",
"Stream",
"RerankResponse",
"UnsupportedCapabilityError",
"UpstreamProviderError",
]
Loading
Loading