OpenAI SDK migration
Change the base URL and the key. Your OpenAI SDK code runs on Stav, with routing, a sovereignty floor and a request log on top.
Change two lines
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["STAV_API_KEY"], # sk_stav_live_...
base_url="https://api.stav.ai/v1", # was https://api.openai.com/v1
)
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.STAV_API_KEY,
baseURL: "https://api.stav.ai/v1",
});
These work through the SDK:
| SDK call | Endpoint |
|---|---|
chat.completions.create (incl. streaming, tools, response_format) | POST /v1/chat/completions |
responses.create | POST /v1/responses |
embeddings.create | POST /v1/embeddings |
audio.transcriptions.create | POST /v1/audio/transcriptions — name a model; see Audio transcriptions |
audio.speech.create | POST /v1/audio/speech — name a model; the audio streams back; see Text-to-speech |
models.list, models.retrieve | GET /v1/models |
Model names
Catalogue ids are creator/model. Four forms work in model:
model="auto" # Smart Router, your workspace default Router
model="@acme/eu-only" # a named Router
model="openai/gpt-4o-mini" # a model; the router picks the Provider Endpoint
model="gpt-4o-mini" # bare name, resolved when it is unambiguous
A bare name resolves through the model's aliases, then through a unique match on the last segment. If two creators share it, it resolves to nothing — use the full id. client.models.list() returns what your key can call.
For a first migration, auto is usually the right choice: you get failover and a sovereignty floor without hard-coding a model.
Scopes
A key with no scopes can call everything. A key with scopes needs one per endpoint:
| Scope | Endpoint |
|---|---|
inference:chat | /v1/chat/completions, /v1/responses |
inference:models | /v1/models |
inference:embeddings | /v1/embeddings |
inference:audio | /v1/audio/transcriptions, /v1/audio/speech |
Many IDEs and agent tools fill their model picker from /v1/models. If you scope a key, include inference:models.
What behaves differently
Stav refuses what it cannot honour, by name, instead of dropping it. Expect a few 400s on day one.
| OpenAI behaviour | On Stav |
|---|---|
| Unknown request fields ignored | 400 naming the field |
| Unsupported parameter silently ignored | 400 naming the parameter and model |
n > 1 | 400. Send parallel requests |
max_completion_tokens | Accepted, same as max_tokens. Sending both with different values is a 400 |
store: false | Accepted (Stav never stores chat completions) |
store: true, metadata | 400 |
json_schema with strict: false | 400. strict must be true |
seed, logprobs, logit_bias, service_tier, prediction, verbosity, prompt_cache_key, parallel_tool_calls, allowed_tools | Honoured on OpenAI-compatible models; 400 on Anthropic, Google and Mistral models |
user, safety_identifier | Honoured on OpenAI-compatible models; ignored elsewhere |
modalities | 400. Text output only |
Hosted tools in tools (web_search, …) | Left out, named in X-Stav-Dropped-Params |
See Request parameters for the full list.
Streaming
stream = client.chat.completions.create(
model="auto",
messages=messages,
stream=True,
stream_options={"include_usage": True}, # needed for token counts
)
Check each chunk for an error key: an error after the stream has started arrives as an SSE frame, not an HTTP status.
See what served the request
raw = client.chat.completions.with_raw_response.create(model="auto", messages=messages)
print(raw.headers.get("X-Stav-Request-Id"))
print(raw.headers.get("X-Stav-Model")) # the model that answered
print(raw.headers.get("X-Stav-Sovereignty-Level")) # L0–L4 of the Provider Endpoint
print(raw.headers.get("X-Stav-Route-Reason")) # set when routed
See Response headers.
The Responses API
POST /v1/responses accepts auto, Routers and any catalogue model. There are two paths:
| Model | What happens |
|---|---|
| OpenAI or xAI model (routed commercial) | Proxied to the provider's own Responses API. Hosted tools work |
| Everything else | Translated to chat completions |
On the translated path:
store: trueandprevious_response_idare refused. Send the full input every turn.- Hosted tools such as
web_searchare left out and named inX-Stav-Dropped-Params. parallel_tool_calls: falseandincludeentries cannot be carried and are named in the same header.
On the proxied path, store defaults to false, not OpenAI's true. Nothing is kept at the provider unless you send store: true — and a workspace that requires zero data retention refuses that with 403 store_not_permitted.
r = client.responses.create(model="openai/gpt-5.3-codex", input="…", store=True)
client.responses.retrieve(r.id)
retrieve, delete, cancel and input_items.list work for stored OpenAI responses only.
Codex
Codex speaks only the Responses API. In ~/.codex/config.toml:
model = "openai/gpt-5.3-codex"
model_provider = "stav"
[model_providers.stav]
name = "Stav"
base_url = "https://api.stav.ai/v1"
env_key = "STAV_API_KEY"
wire_api = "responses"
Any catalogue model, auto or a Router works as model. Codex sends store: false, so the translated path works for non-OpenAI models.
Migration checklist
- Create a
sk_stav_test_key. - Point one non-critical service at
https://api.stav.ai/v1. - Run your tests and fix the
400s. - Try
model="auto"and compare on your own evaluation set. - Set a sovereignty floor for the workspace and check
X-Stav-Sovereignty-Level. - Roll out with a
sk_stav_live_key.
Not available
- Assistants, threads, vector stores, files, batch.
- Image generation, audio inside chat, realtime voice, and moderation. (Transcription and text-to-speech are served — see the two Audio pages.)
- Rerank.