Skip to content
18.1k

Pi

Pi reads its providers from a single global JSON file rather than environment variables, so opencodex does not launch it. Instead, ocx export serializes the opencodex provider block — base URL, model list, and the env reference Pi interpolates — and you merge it into your own config.

Start the proxy, then print the config:

Terminal window
ocx start
ocx export --client pi

The output leads with the JSON, then prints the destination path, the merge warning, the env export line, and how many models carry authoritative context limits.

{
"providers": {
"opencodex": {
"baseUrl": "http://127.0.0.1:10100/v1",
"api": "openai-completions",
"apiKey": "$OPENCODEX_API_KEY",
"compat": {
"sendSessionAffinityHeaders": true,
"supportsDeveloperRole": false
},
"models": [
{
"id": "anthropic/claude-opus-5",
"name": "Claude Opus 5 (anthropic)",
"input": ["text"],
"contextWindow": 200000,
"maxTokens": 128000
}
]
}
}
}

Generated Pi providers enable compat.sendSessionAffinityHeaders. Keep this flag when merging or manually editing the provider: Pi supplies a stable session identity and OpenCodex derives canonical OpenCode Go affinity from it. Pi may omit the identity when cacheRetention is none.

Generated Pi providers also set compat.supportsDeveloperRole to false, so Pi sends its system prompt as system instead of developer. OpenCodex forwards Chat Completions roles as sent, and several OpenAI-compatible upstreams reject developer with a 400; every upstream accepts system.

Model ids are the proxy’s canonical selectors, so routed models appear as provider/model (anthropic/claude-opus-5) and native OpenAI slugs stay unprefixed (gpt-5.6-sol). The name suffix — (anthropic), (native), (routed) — is what makes two same-named models from different upstreams distinguishable in Pi’s picker.

Pi’s global model config is:

~/.pi/agent/models.json

The exported block is a static snapshot, not a live view. Re-run ocx export after adding a provider or changing model visibility, and merge the new block over the old one.

Two different keys are easy to confuse here, and only the first one appears in this file:

Key What it is Where it lives
Proxy admission key opencodex’s own credential, generated on the dashboard’s API tab referenced by apiKey as $OPENCODEX_API_KEY; the value stays in your environment
Provider key your Anthropic / OpenAI / OpenRouter key opencodex’s own config, per Providers

The exported config carries only the reference, never a secret. Pi interpolates a bare $NAME, so the variable is:

Terminal window
export OPENCODEX_API_KEY=<your key>

That name is Pi’s alone. opencode uses a different variable (OPENCODEX_OPENCODE_API_KEY, in {env:…} form) — see the opencode guide.

A loopback proxy needs no key at all. opencodex binds 127.0.0.1 by default and authenticates nothing there, so the $OPENCODEX_API_KEY reference is inert and you can leave the variable unset. It matters only when hostname is set beyond loopback, which is also the case where the proxy refuses to start without a token — see Remote access.

contextWindow and maxTokens are emitted only when the catalog reports an authoritative context window. When it does not, both fields are omitted for that model and Pi applies its own defaults; ocx export prints how many rows fell into that case.

Output limits use the model’s known maximum from catalog or generated metadata. Only unknown limits fall back to 32000. The output limit is always clamped to the context window, including known limits below 32000.

Two fields are deliberately absent. cost requires all four price fields and opencodex has no price data for routed models — emitting zeros would assert that every model is free.

reasoning is the one field that used to be absent and now is not: Pi stores a boolean while the catalog carries an effort ladder, and mapping one onto the other used to be a guess. Since the catalog’s ladder is the proxy’s own statement about whether a model accepts reasoning parameters (adapters honor reasoning_effort), an export row with a non-empty ladder now emits "reasoning": true, and a row without one (or with an explicitly empty ladder) stays reasoning-free. Pi then offers its effort control for exactly the models opencodex will accept it on. The export also emits a thinkingLevelMap that hides every pi level with no declared target (null), so pi never offers — and never sends — an effort the ladder does not contain. One fallback keeps the model usable: when ultra is declared without max, pi’s max level maps to ultra (still a ladder member). If you need a different mapping, hand-edit thinkingLevelMap afterward as documented by Pi.

Treat reasoning as Pi-UI metadata: it is derived from the catalog ladder, not proof that the upstream natively supports a reasoning parameter. What the proxy actually sends for a given reasoning_effort value depends on the provider’s adapter and model — it may pass the value through, translate it (wire aliases), clamp it to the configured ladder, emulate it, or omit it entirely (e.g. noReasoningModels). The boolean only controls whether Pi offers the control at all.

OpenCodex normalizes Pi/MCP and Anthropic-shaped user images before choosing the native Chat or translated route. Images returned by tools use a translated user-message carrier after the paired tool results; ordinary user images and text-only tool results can keep the native path. Use modern tool_calls and role: "tool" with tool_call_id: legacy function-result image translation is rejected instead of silently discarding the result.

An explicit reasoning effort of none survives Chat conversion. Output limits and sampling controls are preserved for generic API-key Responses targets; the canonical ChatGPT target still applies its own restrictions. This does not make all providers’ controls equivalent.

Inline WAV/MP3 audio in a user message or tool result survives translation to OpenAI Chat as an input_audio content part; tool-result audio is promoted into a follow-up user message after the paired tool results because Chat tool messages are text-only. A document that carries its own base64 bytes in a user message is also carried: it reaches the Anthropic, OpenAI Chat and Google wires as a native document, file part and inline data part. Everything else still returns an explicit error rather than succeeding without the attachment — remote or unsupported audio, a file-ID or remote reference the proxy cannot dereference, an attachment in a system message, a document in a tool result, and a document routed to a wire with no byte carrier. File-ID-only images have the same restriction because translated adapters cannot resolve those IDs. Convert the attachment to text first, or use a native wire and model that support it. Native Chat and raw Responses (including Azure) retain their existing behavior; this is not a promise of every model’s upstream media support. Video conversion limits remain adapter-specific.

A running opencodex proxy (ocx start) and Pi installed. ocx export reads the live catalog through the proxy’s management API, so a config can never be emitted with an empty model list.