Python gateway SDK — OpenAI-compatible client
The osyra Python package is an OpenAI-compatible client for Osyra Edge's
verified Chat Completions, Embeddings, and Models subset. Change one import and
keep the supported OpenAI-shaped request fields; enabled workspace controls add
routing, policy, cache, PII, usage, and evidence behavior. Additional fields may
serialize for source compatibility but are not server-capability claims. Sync
and async.
New to OSYRA? Start with the getting-started quickstart first, then come back here to wire your code.
Package status.
osyrais a monorepo-resident package (osyra-python-sdk/). It is not published to PyPI yet — a PyPI publish is a separate, CEO-reserved greenlight (EXE-24). Install it from the monorepo path (see Install).
Install
The package lives at osyra-python-sdk/ in the OSYRA monorepo. Install it in
editable mode from a checkout:
python3 -m venv .venv && . .venv/bin/activate
pip install -e ./osyra-python-sdkIts only runtime dependency is httpx.
The one-line swap (BEFORE / AFTER)
For the verified request subset, swap the import and keep the request shape.
# BEFORE — native OpenAI
import os
from openai import OpenAI
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
resp = client.chat.completions.create(
model=os.environ["OSYRA_MODEL"],
messages=[{"role": "user", "content": "Hello!"}],
)
print(resp.choices[0].message.content)# AFTER — OSYRA drop-in (one import + your OSYRA key)
import os
from osyra import OpenAI
client = OpenAI(api_key=os.environ["OSYRA_API_KEY"])
resp = client.chat.completions.create(
model=os.environ["OSYRA_MODEL"],
messages=[{"role": "user", "content": "Hello!"}],
)
print(resp.choices[0].message.content)The model string passes through unchanged. The constructor is signature-compatible
with openai-python's OpenAI(api_key=..., base_url=...).
Constructor defaults and config
base_url defaults to https://api.osyra.ai/v1 (the real, wired edge
OpenAI-compatible REST route). Override it (for example staging or localhost) via
the base_url= argument or the OSYRA_BASE_URL environment variable. The credential
comes from api_key=, token_provider=, or the OSYRA_API_KEY environment variable.
import os
from osyra import OpenAI
# Reads OSYRA_API_KEY and OSYRA_BASE_URL from the environment when args are omitted.
client = OpenAI()
# Or fully explicit:
client = OpenAI(
api_key=os.environ["OSYRA_API_KEY"],
base_url="https://api.osyra.ai/v1",
timeout=60.0, # seconds (httpx)
max_retries=2, # safe model-list GETs only; inference POSTs are not replayed
)The client supports the context-manager protocol and exposes close():
with OpenAI(api_key=os.environ["OSYRA_API_KEY"]) as client:
resp = client.chat.completions.create(
model=os.environ["OSYRA_MODEL"],
messages=[{"role": "user", "content": "Hi"}],
)
# client.close() runs on exitImport aliases
Osyra, OsyraOpenAI, AsyncOsyra, and AsyncOsyraOpenAI are explicit-import
aliases of the same classes. Use the native-named ones if you prefer:
from osyra import Osyra
client = Osyra(api_key=os.environ["OSYRA_API_KEY"]) # identical to OpenAI(...)Authentication
The SDK selects exactly one credential channel:
| Input | Wire header |
|---|---|
| Exact IAM-issued osy_(sandbox\|test\|live)_<43 base64url> key | X-Api-Key: <key> |
| OAuth access JWT in api_key= | Authorization: Bearer <jwt> |
| token_provider= callback result | Authorization: Bearer <token> |
The API-key classifier matches the complete grammar; malformed or unknown
values stay on the ordinary opaque Bearer path. api_key and token_provider
are mutually exclusive, credential headers cannot be injected through
default_headers or per-call headers, and the SDK never emits both channels.
API-key issuance is enabled per deployment. IAM must have API-key crypto and a
KMS HMAC key configured. Without it, management/minting fails closed with
501 OSY-SERVER-9050, while an Edge data-plane request that presents a key
currently returns 503 OSY-SERVER-9002 because the IAM verifier is unavailable.
The SDK transport being present does not by itself prove activation. Use OAuth
token_provider= where API keys are not enabled.
def current_access_token() -> str:
return oauth_session.access_token
oauth_client = OpenAI(token_provider=current_access_token)If you provide no credential at all (no api_key= and no OSYRA_API_KEY), the
constructor fails closed with an AuthenticationError — it never creates a
silent unauthenticated client.
Transport security
base_url must be https://. An explicit allow_insecure=True escape hatch permits
http:// for localhost dev only:
# localhost dev only — http:// is rejected without this flag
client = OpenAI(
token_provider=lambda: os.environ["OSYRA_ACCESS_TOKEN"],
base_url="http://localhost:8080/v1",
allow_insecure=True,
)The credential is never logged and never placed in a URL.
Chat completions
client.chat.completions.create(...) builds the exact OpenAI
/v1/chat/completions body and parses the response into typed ChatCompletion,
Choice, ChatCompletionMessage, and CompletionUsage objects.
resp = client.chat.completions.create(
model=os.environ["OSYRA_MODEL"],
messages=[
{"role": "system", "content": "You are concise."},
{"role": "user", "content": "Name three primary colors."},
],
temperature=0.2,
max_tokens=64,
)
print(resp.choices[0].message.content)
print(resp.usage.total_tokens if resp.usage else None)
print(resp.provider) # OSYRA extension: which upstream provider served the callmodel and messages are required keyword arguments; an empty model or empty
messages list raises ValueError.
Verified request-field contract
Edge currently guarantees model; non-empty messages with string role and
string content plus optional string name; max_tokens; temperature;
top_p; stop as a string array; stream; and
stream_options.include_usage.
The client can serialize additional OpenAI-shaped keyword arguments for source compatibility. Serialization does not claim Edge accepts, forwards, or honors those semantics. In particular, tools/function calling, multimodal content, structured output, logprobs, and other extended OpenAI options are not part of the verified Edge contract.
Streaming
Pass stream=True to get a synchronous iterator that yields ChatCompletionChunk
objects parsed from the SSE data: frames (with [DONE] termination and heartbeat
tolerance):
stream = client.chat.completions.create(
model=os.environ["OSYRA_MODEL"],
messages=[{"role": "user", "content": "Tell me a story."}],
stream=True,
)
for chunk in stream:
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True)Async (AsyncOpenAI)
AsyncOpenAI is the literal async twin — same constructor, defaults, auth, extras,
and wire shape. Methods are coroutines, and streaming yields an async iterator. The
swap mirrors the upstream package:
# from openai import AsyncOpenAI -> from osyra import AsyncOpenAI
from osyra import AsyncOpenAI # or: from osyra import AsyncOsyra
async def main():
async with AsyncOpenAI(api_key=os.environ["OSYRA_API_KEY"]) as client:
resp = await client.chat.completions.create(
model=os.environ["OSYRA_MODEL"],
messages=[{"role": "user", "content": "Hello!"}],
)
print(resp.choices[0].message.content)
# async streaming
stream = await client.chat.completions.create(
model=os.environ["OSYRA_MODEL"],
messages=[{"role": "user", "content": "Stream it."}],
stream=True,
)
async for chunk in stream:
print(chunk.choices[0].delta.content or "", end="")The async client also exposes await client.aclose() if you are not using the
async with context manager.
Embeddings
client.embeddings.create(...) posts the OpenAI /v1/embeddings body and returns a
typed CreateEmbeddingResponse. model and input are required; input is a string
or a list of strings (an empty string or empty list raises ValueError).
emb = client.embeddings.create(
model=os.environ["OSYRA_EMBEDDING_MODEL"],
input="hello world",
)
print(emb.data[0].embedding)
# batch
batch = client.embeddings.create(
model=os.environ["OSYRA_EMBEDDING_MODEL"],
input=["alpha", "beta", "gamma"],
)
print(len(batch.data))Optional OpenAI embedding params (for example dimensions, encoding_format, user)
are forwarded as keyword arguments.
Listing models
client.models.list(...) sends a bodyless GET /v1/models and returns an
OpenAI-shaped ModelList. ModelList supports len(), iteration, and
indexing.
for m in client.models.list():
print(m.id, m.owned_by)The live Edge response currently carries id, object, created, and
owned_by. The SDK type has optional extension slots for richer broker
metadata, but those slots are not a current server claim. The catalog does not
identify chat-versus-embedding capability; use operator-assigned
OSYRA_MODEL / OSYRA_EMBEDDING_MODEL values and use the list only to confirm
deployment registration. A listed model can still be denied by workspace
policy; a successful authorized operation is the usable model proof.
To assert the workspace explicitly, pass osyra_workspace=; the SDK sends it
as X-Osyra-Workspace, never in the URL. The credential remains authoritative:
Edge accepts the header only when it equals the authenticated workspace and
returns 403 on a mismatch. The header cannot switch tenants and does not
currently filter the catalog response.
Native client.osyra.* surface
The client.osyra.* namespace holds OSYRA-native methods that have no OpenAI analog,
so they never pollute the drop-in OpenAI surface (they vanish if you point base_url
back at a native OpenAI endpoint for A/B testing).
client.osyra.models() # WIRED — same data as models.list()
client.osyra.estimate_cost(model=os.environ["OSYRA_MODEL"], messages=[...]) # PENDING
client.osyra.routing_decision(prompt="...") # PENDING — raises OsyraNotImplementedError
client.osyra.provider_health() # PENDING — raises OsyraNotImplementedErrorclient.osyra.models(osyra_workspace=...)is wired—it delegates to the sameGET /v1/modelsrouteclient.models.list()uses.estimate_cost/routing_decision/provider_healthmap to broker RPCs (EstimateCost,EstimateRoutingDecision,GetProviderHealth) that exist on the broker but have no edge REST projection yet (AP00023 §12 OQ-2). They raiseOsyraNotImplementedError(naming the missing edge route) rather than fabricate data — an honest, fail-loud "pending" marker. Their public signatures are stable and they go live unchanged when the edge route lands.
from osyra import OsyraNotImplementedError
try:
client.osyra.estimate_cost(model=os.environ["OSYRA_MODEL"], messages=[{"role": "user", "content": "hi"}])
except OsyraNotImplementedError as e:
print(e.edge_route) # names the missing edge routeOn the async client these same methods live at await client.osyra.models(...) and
the pending three raise OsyraNotImplementedError likewise.
Osyra extras (X-Osyra-* headers)
The three OSYRA governance knobs are typed kwargs (a client-level default or a
per-call override) projected onto X-Osyra-* request headers. They never enter
the OpenAI request body, so the same code still works if you point base_url back at
a native OpenAI endpoint for A/B testing — the headers are silently ignored there.
client = OpenAI(
api_key=os.environ["OSYRA_API_KEY"],
osyra_workspace=os.environ["OSYRA_WORKSPACE_ID"],
)
resp = client.chat.completions.create(
model=os.environ["OSYRA_MODEL"],
messages=[{"role": "user", "content": "Summarize this."}],
osyra_cache_ttl=3600, # semantic-cache TTL (seconds); None disables cache for this call
osyra_pii_mask=True, # force PII masking on this call (bool or a strategy string)
)| kwarg | wire header | meaning |
|---|---|---|
| osyra_workspace | X-Osyra-Workspace | optional assertion that must equal the credential's authoritative workspace; it cannot switch tenants |
| osyra_cache_ttl | X-Osyra-Cache-TTL (plus X-Osyra-Cache: enabled/disabled) | semantic-cache freshness override; None disables the cache for the call |
| osyra_pii_mask | X-Osyra-PII-Mask | force PII masking ("true"/"false", or a strategy string) |
A per-call extra overrides the client-level default. Omitting
osyra_cache_ttl emits neither cache header and inherits server/workspace
behavior; omission does not disable the cache. Setting
osyra_cache_ttl=None explicitly disables it for that call (emits
X-Osyra-Cache: disabled). A non-negative integer, including zero, emits that
TTL plus X-Osyra-Cache: enabled; a negative value raises ValueError.
Error handling
Non-2xx responses raise typed exceptions that mirror openai-python's error model.
Every API error carries .status_code, .body, and .request_id:
from osyra import AuthenticationError, RateLimitError, OsyraAPIError
try:
client.chat.completions.create(
model=os.environ["OSYRA_MODEL"],
messages=[{"role": "user", "content": "hi"}],
)
except AuthenticationError:
... # 401 — fail-closed, never a silent unauthenticated call
except RateLimitError as e:
retry_after = e.retry_after # honors the edge's Retry-After (seconds), when present
except OsyraAPIError as e:
print(e.status_code, e.body, e.request_id)Status mapping:
| Status | Exception |
|---|---|
| 400 / 422 | BadRequestError |
| 401 | AuthenticationError |
| 403 | PermissionDeniedError |
| 404 | NotFoundError |
| 429 | RateLimitError (carries .retry_after) |
| 5xx | InternalServerError |
| other non-2xx | APIStatusError |
| transport (DNS/TCP/TLS/timeout) | APIConnectionError |
All of these subclass OsyraAPIError (aliased APIError), which subclasses
OsyraError. OsyraNotImplementedError (from the pending client.osyra.*
methods) subclasses OsyraError directly. Automatic retry is limited to safe
model-list GET requests. Chat, streaming, and embedding POSTs are never
replayed without an idempotency contract. The SDK honors Retry-After on an
eligible retry and sets X-Request-Id for correlation.
OsyraAuthenticationError, OsyraRateLimitError, and OsyraWorkspaceError are
explicit-import aliases of AuthenticationError, RateLimitError, and
PermissionDeniedError respectively.
For the full list of error codes the edge returns in the response body, see the error catalog.
Migrating from the native OpenAI SDK
- Change the import.
from openai import OpenAIbecomesfrom osyra import OpenAI(andAsyncOpenAIlikewise). The constructor signature, resource namespaces (chat.completions,embeddings,models), method names, and parameters are the same. - Use your deployment's active credential. In an API-key-enabled
environment, pass the one-time plaintext from Console in
api_key=(or setOSYRA_API_KEY); the SDK sends an exact key asX-Api-Key. Where key crypto is not activated, pass an OAuthtoken_provider=instead. - Use operator-assigned model IDs. Set chat
OSYRA_MODELand embeddingOSYRA_EMBEDDING_MODEL, then confirm each ID appears inmodels.list(). The current catalog does not expose operation capabilities, so do not infer modality or authorization from list order; prove each with an authorized operation. - (Optional) Add governance knobs. Layer in
osyra_workspace,osyra_cache_ttl, andosyra_pii_maskas a client default or per call. The workspace header is an equality assertion, not a tenant selector. These controls ride request headers, so pointingbase_urlback at OpenAI for an A/B test still works — the headers are ignored there. - Catch the typed errors under
OsyraAPIError(or the OpenAI-named aliases such asAuthenticationError/RateLimitError) — same shape you already handle.
OpenAI-SDK code using the verified field subset keeps its request shape after step 1–2. The compatibility invariant applies to that subset; Osyra-specific controls ride in headers and defaults.
What's implemented vs pending
Implemented and tested (real wire contract):
- Sync + async clients (
OpenAI/AsyncOpenAIand theOsyra/AsyncOsyra/OsyraOpenAI/AsyncOsyraOpenAIaliases), withbase_urldefaulting to the OSYRA edge. client.chat.completions.create(...)with the verified Edge field subset, plus source-compatible serialization that does not overclaim server support; typedChatCompletionparsing.- Streaming (
stream=True) — sync iterator and async iterator over SSE. client.embeddings.create(...).client.models.list(...)through bodylessGET /v1/models.client.osyra.models()(wired); the three Osyra extras →X-Osyra-*headers.- Typed error mapping with
Retry-After, safe-GET-only bounded retry,X-Request-Idcorrelation, environment configuration, and an HTTPS-only guard.
Pending (flagged, never faked):
client.osyra.estimate_cost/routing_decision/provider_health— broker RPCs exist but have no edge REST route yet (AP00023 §12 OQ-2); they raiseOsyraNotImplementedError.- PyPI publish — CEO-reserved greenlight (EXE-24).
See also
- Getting started — quickstart and first request.
- Error catalog — the codes returned in error bodies.