Python SDK for openapi.ai — a drop-in extension of the OpenAI client, plus capability contracts: schema-frozen interfaces called like functions.
Project description
openapi-ai-sdk
The Python SDK for OpenAPI — a drop-in replacement for the OpenAI client, plus the one thing no other gateway offers: capability contracts, versioned schema-frozen interfaces you call like functions while the platform guarantees what goes in and what comes out.
pip install openapi-ai-sdk
Drop-in for the OpenAI SDK
Change one import and one class name; everything else is your existing code.
from openapi_ai import OpenAPI # was: from openai import OpenAI
client = OpenAPI(api_key="gw_…")
response = client.chat.completions.create(
model="openai/gpt-4.1-mini",
messages=[{"role": "user", "content": "hello"}],
)
print(response.usage.cost) # USD, on every response
OpenAPI is an openai.OpenAI — a subclass that overrides nothing.
Streaming, tool calls, retries, and typed responses are the OpenAI SDK's own,
and anything built on that SDK — LangChain, LlamaIndex, Instructor — takes
this client unchanged. AsyncOpenAPI is the same over openai.AsyncOpenAI.
What the platform adds appears as namespaces on the same client:
capabilities, invocations, callbacks, usage.
Capability contracts
The reason this SDK exists. Instead of integrating a provider's API, your organization defines a contract — a named, versioned pair of JSON Schemas — and providers register endpoints that fulfill it. The platform validates input before dispatch and output before you see it, retries across fulfillments, and never returns nonconforming data. Published versions are frozen forever: the schema you integrate against today cannot change under you.
summarize = client.capabilities.get("doc-summary", version=3)
# Synchronous — the handle is callable; blocks until a conforming result:
result = summarize(input={"text": "..."})
# Asynchronous — a job handle now, the result when the work finishes:
job = summarize.submit(input={"text": "..."})
print(job.wait().output["summary"])
version is required on purpose: there is no floating "latest", so
publishing v4 breaks nobody on v3.
From Pydantic models, typed end to end
Define the contract and the call site from one pair of models:
from pydantic import BaseModel
from openapi_ai import capability_contract
class DocSummaryIn(BaseModel):
text: str
max_words: int | None = None
class DocSummaryOut(BaseModel):
summary: str
capability_contract(DocSummaryIn, DocSummaryOut)
# → {"input_schema": …, "output_schema": …} — publish this as the contract version
summarize = client.capabilities.get(
"doc-summary", version=3,
input_model=DocSummaryIn, output_model=DocSummaryOut,
)
out = summarize(DocSummaryIn(text="long document")) # → DocSummaryOut
out.summary # attributes, not dict keys
job = summarize.submit(DocSummaryIn(text="long document"))
job.wait().output # → DocSummaryOut
Input is validated client-side before anything leaves the process (a dict is accepted too, and validated through the model); output comes back as the model the platform already guaranteed conformance for. What you publish and what you type-check are one definition, not two hand-maintained copies.
Failures are typed, and kept deliberately distinct:
| What happened | How you see it |
|---|---|
| Refusal — nonconforming input, in-flight cap, exhausted fulfillments | CapabilityInvocationError at the call site, with the typed code, JSON-pointer paths, and attempt trail |
| Failed invocation — terminal, already retried by the platform | Data: job.failed, job.error; reading job.output raises InvocationFailed so the happy path can't quietly get None |
wait() timeout |
InvocationTimeout — changes nothing; the job keeps running server-side |
For long-running work, skip polling: pass callback_url= to submit, then
verify the signed webhook that arrives —
client.callbacks.verify(raw_body, signature_header, secret="whsec_…") does
the constant-time, tolerance-checked HMAC and returns the full document.
Files in, files out
A file slot is part of the contract like any other field — declare it with
File and the schema, the encoding, and the decoding all fall out of the
one annotation:
from pathlib import Path
from typing import Annotated
from openapi_ai import File, MediaType, capability_contract
class RedactIn(BaseModel):
document: Annotated[File, MediaType("application/pdf")]
reason: str
class RedactOut(BaseModel):
redacted: Annotated[File, MediaType("application/pdf")]
capability_contract(RedactIn, RedactOut) # file slots included — publish as usual
redact = client.capabilities.get(
"doc-redact", version=1, input_model=RedactIn, output_model=RedactOut,
)
out = redact(RedactIn(document=Path("contract.pdf"), reason="pii"))
out.redacted.save("contract-redacted.pdf") # .bytes and .media_type too
Untyped calls are just as direct — a Path or bytes value encodes itself,
and output file slots come back as File objects on a bound handle:
result = client.capabilities.get("doc-redact", version=1)(
input={"document": Path("contract.pdf"), "reason": "pii"}
)
result["output"]["redacted"].save("contract-redacted.pdf")
The contract decides how the bytes travel, and the code above does not
change: inline slots (contentEncoding: "base64") ride the payload, bounded
by the payload cap (10 MB default); by-reference slots (format: "file-id")
upload to the platform file store first and the payload carries only the id
— outputs download back into the same File objects. The store itself is
client.file_store when you want an id without an invocation.
Contracts as tools, for any model
A published contract already is a tool definition:
tools = [client.capabilities.get("doc-summary", version=3).as_tool()]
# … the model answers with tool_calls …
messages.append(client.capabilities.execute_tool_call(call))
execute_tool_call runs the model's call through the platform and answers
with the role:"tool" message. Model-caused failures — hallucinated
arguments, nonconforming input — become tool-message content, never
exceptions: an agent loop that crashes on a bad model turn is a loop nobody
can run. The platform can also run the loop server-side, or serve your
contracts over MCP — see
invoking capabilities.
Static types, generated from the contract
openapi pull doc-summary@3
writes doc_summary_v3.py: TypedDicts for both sides plus typed
invoke/submit helpers, so your type checker enforces the same schema pair
the provider is held to at runtime. Commit the file — it cannot rot, because
published versions are frozen. Constructs a TypedDict cannot express
degrade to Any, never to a crash. The key comes from --api-key or
OPENAPI_API_KEY.
Fulfilling contracts (the provider side)
Your endpoint is your own server; the SDK ships the pieces every fulfiller needs:
from openapi_ai import DispatchedJob, verify_dispatch, push_result, push_failure
verify_dispatch(request.headers.get("Authorization"), secret=DISPATCH_SECRET)
job = DispatchedJob.parse(await request.body()) # input already conforms
push_result(job.completion_url, {"summary": "…"}) # or push_failure(...)
A nonconforming result raises CompletionRejected with JSON-pointer paths —
and the one-time completion token is burned (rejected.gone marks a consumed
or expired one). An honest push_failure is authoritative by default: the
platform does not shop the same input to another provider.
Routing, attribution, and usage
Everything below rides extra_body on standard OpenAI calls:
| Helper | What it does |
|---|---|
provider(...) |
which provider serves the request: order, only, ignore, sort, jurisdictions, data_collection |
models(...) |
fallback models, tried in order |
transforms(...) |
middle-out prompt compression |
metadata(...) |
labels recorded with the request, filterable afterwards |
extra_body(...) |
merges the above into one extra_body (alias: routing) |
session_id(...) |
attributes this request to a conversation, right on the call |
session(...) |
the same, for every request in a with block |
client.usage |
generation, generations, credits, key_info — what it cost, per call or per label |
from openapi_ai import provider, metadata, extra_body, session_id
client.chat.completions.create(
model="openai/gpt-4.1-mini",
messages=[...],
extra_body=extra_body(provider(order=["groq"], sort="throughput"),
metadata(tenant="acme"),
session_id("conversation-42")),
)
client.usage.generations(metadata={"tenant": "acme"}) # that tenant's spend
The default deployment is https://openapi.ai/v1; point base_url at your
own stack to self-host. Where inference physically runs is a per-request
routing choice (provider(jurisdictions=["EU"])), not a hostname.
Local call log and cache
from openapi_ai import OpenAPI, SqliteStore, summary
store = SqliteStore("~/.local/state/openapi-ai/calls.db", store_io=True)
client = OpenAPI(api_key="gw_…", store=store, cache=True) # cache needs bodies to replay
summary(store, metadata={"tenant": "acme"})
# {'calls': 12, 'billable': 11, 'cached': 1, 'errors': 0, 'cost': 0.0241, ...}
Content is never stored by default — ids, model, routing, tokens, cost,
latency, and your labels are; prompts and completions require an explicit
store_io=True, mirroring the server's own opt-in stance. A request sent
with data_collection="deny" never has its content stored at all. The cache
(opt-in, non-streaming, needs store_io=True) keys on everything that
changes the answer and fingerprints your API key, so a shared store cannot
serve one organization's response to another — and note that a client-side
hit never reaches the gateway, so budgets, guardrails, and rate limits see
nothing. Backends: SqliteStore (recommended), JsonFileStore,
PostgresStore (the postgres extra).
The openapi CLI
Installed with the package — the same surface, from a shell:
openapi chat "say hello" --model openai/gpt-4.1-mini --session c42
openapi invoke doc-summary@3 --input '{"text": "..."}'
openapi invoke doc-redact@1 --file document=contract.pdf --save redacted=clean.pdf
openapi submit doc-summary@3 --input @input.json --wait
openapi invocation inv-… --wait
openapi contract doc-summary@3
openapi credits
openapi key
openapi pull doc-summary@3
JSON on stdout for machines, the assistant's words for chat, refusals on
stderr with the typed code and paths. The key comes from OPENAPI_API_KEY
(or --api-key), the base URL from OPENAPI_BASE_URL.
Documentation
- Capability contracts — concepts and the invocation lifecycle
- Invoking capabilities — sync, async, tools, MCP, generated types
- Tutorials — your first capability, end to end
Development
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
PYTHONPATH=. .venv/bin/python -m pytest tests/
tests/test_matches_the_gateway.py reads the gateway source and the
committed spec, so a value this package validates locally cannot silently
drift from what the server accepts.
License
Apache-2.0 — use it against the hosted platform or any self-hosted deployment.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file openapi_ai_sdk-0.3.3.tar.gz.
File metadata
- Download URL: openapi_ai_sdk-0.3.3.tar.gz
- Upload date:
- Size: 73.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3ee1ca839b607ed05ad32152bbff96a7a63599bfb9ffb4a7e9379bebf2853e9c
|
|
| MD5 |
66860fa34822f2c5242e707209665e13
|
|
| BLAKE2b-256 |
6dbc75bccd85822d93aac5e846357ac0cb001ea4075930e06a027e387abf25b8
|
Provenance
The following attestation bundles were made for openapi_ai_sdk-0.3.3.tar.gz:
Publisher:
pypi.yml on sireto/openapi-ai-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
openapi_ai_sdk-0.3.3.tar.gz -
Subject digest:
3ee1ca839b607ed05ad32152bbff96a7a63599bfb9ffb4a7e9379bebf2853e9c - Sigstore transparency entry: 2314830592
- Sigstore integration time:
-
Permalink:
sireto/openapi-ai-sdk@cad637fb1d2cbbb9d0bfc5fc9a0f2bb60efb1755 -
Branch / Tag:
refs/tags/v0.3.3 - Owner: https://github.com/sireto
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@cad637fb1d2cbbb9d0bfc5fc9a0f2bb60efb1755 -
Trigger Event:
push
-
Statement type:
File details
Details for the file openapi_ai_sdk-0.3.3-py3-none-any.whl.
File metadata
- Download URL: openapi_ai_sdk-0.3.3-py3-none-any.whl
- Upload date:
- Size: 56.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
029c8d61ab4503bcca1dbda68b5c52dd7590bd8ff548b78233f1cbf8de8c0794
|
|
| MD5 |
c695c044336566e87c947d4fc324ce5b
|
|
| BLAKE2b-256 |
2bf54ff0d322840b555e6e3f212eaaa63486112f70f0f8a7f4ba795aa1c07e6c
|
Provenance
The following attestation bundles were made for openapi_ai_sdk-0.3.3-py3-none-any.whl:
Publisher:
pypi.yml on sireto/openapi-ai-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
openapi_ai_sdk-0.3.3-py3-none-any.whl -
Subject digest:
029c8d61ab4503bcca1dbda68b5c52dd7590bd8ff548b78233f1cbf8de8c0794 - Sigstore transparency entry: 2314830610
- Sigstore integration time:
-
Permalink:
sireto/openapi-ai-sdk@cad637fb1d2cbbb9d0bfc5fc9a0f2bb60efb1755 -
Branch / Tag:
refs/tags/v0.3.3 - Owner: https://github.com/sireto
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@cad637fb1d2cbbb9d0bfc5fc9a0f2bb60efb1755 -
Trigger Event:
push
-
Statement type: