OpenAI compatibility¶
ApiPi speaks the OpenAI Agents API shapes that official clients use. You bring the model host and the isolation. This page is the contract: what those clients can do today, what uses the same JSON with a different backend, and what returns an error.
Request bodies are strict. An unknown JSON key is 400 with code
unknown_field. A known OpenAI field we have not implemented is 400
with type not_implemented. Nothing extra is stored or ignored. Route
and field detail stays on API.
Status in the tables:
| Status | Meaning |
|---|---|
| Same API | Official client calls work with the same request and response shapes. |
| Same shape, different backend | The JSON matches; the computer, model, or harness is ApiPi's. |
| Error | Extra key (unknown_field) or a named OpenAI field (not_implemented). |
The beta header OpenAI-Beta: agents=v1 is accepted and ignored.
Same for builders¶
Point the official OpenAI Python client at this gateway
(OPENAI_BASE_URL=http://localhost:8000/v1 on the client, with a
bearer). Create an agent, open a session, stream events, send a
follow-up, cancel, and delete. examples/sessions/openai_sdk.py and
Using the API walk through that flow.
You can:
- Create, list, get, update, and delete agents. Fields are
name,model,instructions,metadata, andtools(function, MCP HTTP, MCP stdio). - Create a session with
agentoragent_id,environment,input,metadata, andstream. A non-emptyinputstarts the first turn. - Stream with
GET /v1/agents/sessions/{id}/events?stream=trueand reconnect withafter_seq. - Send follow-ups with
POST /v1/agents/sessions/{id}/events. Official SDK helpers (sessions.events.create) send a nestedeventslist with one item. Curl can send the flat{type, text|content, …}body. Both mean the same thing. Message, cancel, and tool_result are supported. Send one shape, not both. - List turns, items, and artifacts; fetch artifact bytes; export the transcript.
- List models with
GET /v1/modelswhenAPIPI_FORWARD_MODELSis on (the default). That call proxies to your model host. - Use function tools, MCP, and skills via
environment.capability_directories(SKILL.mdtrees on the computer). - Set
environment.typetoopenai_hosted(default),hosted(alias),none, orself_hosted. On hosted computers,packagesandsetup_commandsrun before the first turn. - Authenticate with
Authorization: Beareron every route except/healthand/metrics.
agent.model must exist on the model host. Missing model is
model_required. Unknown id is model_not_found.
Same shape, different backend¶
These fields and routes look like OpenAI's. The machine behind them is yours.
| Topic | OpenAI | ApiPi |
|---|---|---|
openai_hosted / hosted |
Cloud Linux sandbox | A local session directory next to Pi. In microvm, guest cwd is /workspace. OpenAI's field name, a folder on your machine. |
| Model | OpenAI-hosted models | OPENAI_BASE_URL on the gateway is the model host Pi calls. Clients use a different URL for this API. |
| Agent loop | OpenAI Codex | Pi over RPC |
| Isolation | OpenAI's managed sandbox | Run mode none or microvm (run modes) |
| Auth | OpenAI account keys | A callback maps the bearer to a tenant. The gateway does not mint keys. See auth. |
self_hosted |
OpenAI codex exec-server |
ApiPi runner WebSocket at /v1/environments/{environment_id}. Create returns environment_id and a one-time key. |
| Hosted files | Last until OpenAI's sandbox idle expiry | Last until APIPI_SANDBOX_TTL_OPENAI_HOSTED (default 1 hour). Then Pi stops and /workspace is deleted. The next turn rebuilds skills, packages, and setup commands, and reloads the harness session cache. The session transcript stays. |
output_text.delta |
May be durable on their side | Live SSE only. Reconnect and export use output_text.done and items. |
| Thinking | May stream the full reasoning text | Stored preview (first 100 Unicode code points), duration, and reasoning token count. The full thinking text is not a public event. |
| SSE events | Typed OpenAI stream objects | {type, seq, data, …}. Extra OpenAI fields such as delta at the top level are omitted. Use raw SSE / with_streaming_response. |
HTTP routes¶
| Route | Status |
|---|---|
POST/GET /v1/agents, GET/POST/DELETE /v1/agents/{id} |
Same API (field subset) |
GET /v1/models |
Same API when forwarding is on; not_implemented forward_models when off |
POST/GET /v1/agents/sessions, GET/POST/DELETE /v1/agents/sessions/{id} |
Same API |
POST/GET /v1/agents/sessions/{id}/events |
Same API (nested events and flat body) |
GET /v1/agents/sessions/{id}/export |
Same API |
GET …/turns, GET …/items, GET/DELETE …/artifacts |
Same API |
GET /v1/usage |
ApiPi operator route (tokens and turn counts) |
WS /v1/environments/{environment_id} |
Same shape, different backend (ApiPi runner protocol) |
GET /health, GET /metrics |
ApiPi operator routes |
POST/GET/DELETE /v1/files, GET /v1/files/{id}/content |
Same API (purpose user_data or assistants; max APIPI_MAX_FILE_BYTES) |
POST/GET/DELETE /v1/skills |
Same shape, zip upload (no version endpoints). Max APIPI_MAX_FILE_BYTES. |
/v1/chat/completions |
Error (no such route) |
| ChatKit | Error (no such routes) |
| Vaults | /v1/agents/vaults and credentials. static_bearer only. GET omits token values. Tokens encrypted at rest. mcp_oauth is not_implemented. |
Agent fields and tools¶
| Field or tool | Status |
|---|---|
name, model, instructions, metadata |
Same API |
idle_ttl |
ApiPi extension. Duration (30m, 1h) or 0 to turn idle off. Stock SDKs can set metadata["apipi.idle_ttl"] instead. |
tools type function |
Same API |
tools type mcp with nested transport (http or stdio) |
Same API |
Top-level MCP server_url / command / args |
Error (unknown_field) |
multi_agent, tool_search, programmatic_tool_calling |
Error (not_implemented) |
First-party web_search |
Error; use MCP (example: Tavily) |
| First-party browser | Error; use MCP (example: Playwright) |
| Unknown JSON keys | Error (unknown_field) |
Environment fields¶
| Field | Status |
|---|---|
type: openai_hosted, hosted, none, self_hosted |
Same shape, different backend for hosted; same API for none |
capability_directories |
Same API (skills on the computer) |
packages, setup_commands |
Same API on openai_hosted only; 400 on none or self_hosted |
sandbox_size |
ApiPi extension (S | M | L). Stock SDKs can set metadata["apipi.sandbox_size"]. Top-level session sandbox_size is unknown_field. |
sandbox_image |
ApiPi extension. Stock SDKs can set metadata["apipi.sandbox_image"]. Top-level session sandbox_image is unknown_field. |
env |
Same API on openai_hosted only; reserved names 400; 400 on none or self_hosted |
files with type: "inline" or type: "file_id" |
Same API on openai_hosted only. file_id mounts a Files API object. Other file types are not_implemented. |
network |
Same API on openai_hosted only. Session policy cannot widen [sandbox.network]. Isolation none cannot enforce disabled / restricted. |
skills with type: "skill_reference" |
Same API on openai_hosted only. Zip unpacks under .agents/skills/. Other skill types are not_implemented. |
environment_template_id, plugins |
Error (not_implemented) |
| Unknown JSON keys | Error (unknown_field) |
Lifecycle¶
| Piece | OpenAI | ApiPi |
|---|---|---|
| Session / transcript | Durable on their side | Durable in SQLite or Postgres until you delete the session. Export is enough to leave. |
| Computer / files | Cloud sandbox, about an hour idle | Hosted directory until sandbox TTL (default 1 hour), then a fresh /workspace. none has no files. self_hosted files stay on the runner. |
| Idle Pi | Their sandbox runtime | none and self_hosted: APIPI_IDLE_TTL (default 15 minutes) stops Pi. Hosted computers use sandbox TTL. The session row stays. The next turn starts a new Pi and reloads the cached session file. |
| Artifacts | /workspace/outputs published on turn complete |
/workspace/outputs copied to the host store on turn complete. Immutable. Downloadable after the workspace expires. |
| Follow-up affinity | OpenAI's fleet | API-only plus workers: any API replica. Combined apipi serve: sticky to the node that holds Pi. See multiple nodes. |
Errors¶
| Case | Type | Code |
|---|---|---|
| Extra JSON key | invalid_request |
unknown_field |
| Known OpenAI field we skip | not_implemented |
The field name (multi_agent, files, …) |
| Missing or bad bearer | invalid_request |
unauthorized (401) |
| Id on another tenant | invalid_request |
not_found (404) |
Unknown agent.model |
invalid_request |
model_not_found |
| Known image, no worker has it | api_error |
image_unavailable (503) |
Nested events length not 1, or mixed flat+nested body |
invalid_request |
validation_error |
Non-text input parts (for example input_image) |
not_implemented |
The part type |
The envelope is { "error": { "type", "code", "message" } }. When
create already stored a session and the first turn failed, the error
also has session_id and HTTP status 502. See
API errors.
Events request and response¶
POST /v1/agents/sessions/{id}/events accepts:
{
"events": [
{
"type": "agent.session.input.message",
"input": [
{
"role": "user",
"content": [{ "type": "input_text", "text": "follow up" }]
}
]
}
]
}
That is what client.beta.agents.sessions.events.create sends. Cancel
and tool_result use the same events list with one object. The flat
body { "type": "agent.session.input.message", "text": "…" } is the
other supported form.
SSE events use ApiPi public types (agent.session.created,
agent.session.turn.output_text.done, and the rest listed on
API). Each event has seq. Payload fields live under
data. Typed OpenAI stream objects that expect top-level delta or
item_id will not see those keys; parse data or use
with_streaming_response.
How to verify¶
- Install and serve the gateway (Install).
- Run
examples/sessions/openai_sdk.pyas on Using the API. - Send a follow-up with the nested
eventsbody (SDKsessions.events.create) or the flat curl example on that page. - Confirm unknown keys return
unknown_fieldandmulti_agentreturnsnot_implemented.
Field-level HTTP reference remains API. Computers and TTL are on environments and concepts.