Date: 2026-06-20
Archive note. This is a dated design-rationale record, not current behavior documentation. For up-to-date behavior see the published docs at opencodex.me and the maintainer source-of-truth under
structure/. The current injected provider table name is"OpenCodex Proxy"(seesrc/codex/inject.ts).
This document records why opencodex routed models can appear in Codex App's model picker without patching Codex App itself.
Codex CLI, TUI, and App share the Codex home configuration surface. opencodex integrates by writing
Codex-native config and catalog files under the resolved CODEX_HOME:
$CODEX_HOME/config.toml$CODEX_HOME/opencodex.config.toml$CODEX_HOME/opencodex-catalog.json$CODEX_HOME/models_cache.json
When Codex App reads the same config/catalog state, routed opencodex models are visible because they look like valid Codex catalog entries.
The global provider must be a root TOML key:
model_provider = "opencodex"It must not be appended under whichever TOML table happened to be last. TOML root keys after a table header become part of that table, which makes Codex ignore the provider as a global setting.
The custom model catalog path must also be a root TOML key:
model_catalog_json = "/absolute/path/to/opencodex-catalog.json"The provider block must advertise a Responses-compatible provider:
[model_providers.opencodex]
name = "OpenCodex Proxy"
base_url = "http://localhost:10100/v1"
wire_api = "responses"
requires_openai_auth = truerequires_openai_auth = true is important for Codex App/TUI account-gated behavior. Codex derives
ChatGPT-account capability from the active provider; without this flag, fast-related UI can stay
hidden even when the user has ChatGPT auth.
opencodex does not generate minimal JSON entries. It clones a native Codex model catalog entry and then changes the routed fields:
slug = "<provider>/<model>"
display_name = "<provider>/<model>"
description = "Routed via opencodex -> <provider> ..."
priority = <picker priority>
visibility = "list"
Slash-containing native ids: some providers namespace their own model ids
(zenmux moonshotai/kimi-k3-free, openrouter anthropic/..., nvidia moonshotai/...).
Codex's models-manager metadata lookup tolerates exactly one "/", so opencodex aliases
inner slashes to "-" in the Codex-facing slug (zenmux/moonshotai-kimi-k3-free) and
decodes back to the native id in the proxy via an exact known-id lookup
(src/providers/slug-codec.ts). Raw full-slash selectors keep working; upstream
requests, logs, usage, and metadata always carry the native id.
Cloning a native entry preserves fields Codex's strict parser expects, including:
base_instructionssupported_reasoning_levelsdefault_reasoning_levelshell_typesupported_in_api
This is why routed entries can behave like normal picker-visible Codex models.
The recognized Codex effort ladder is low < medium < high < xhigh < max < ultra
(src/reasoning-effort.ts CODEX_REASONING_LEVELS), matching the upstream codex-rs
ReasoningEffort enum order. Semantics ported from upstream (df1199fdd, 80f54d126):
ultrais a client-facing selection: maximum reasoning plus proactive multi-agent delegation (derived in codex-rs core, not by the proxy). Upstream converts it tomaxat the inference boundary; ocx mirrors that in two places — the Responses parser normalizesultra -> maxat ingest, andmapReasoningEffortconverts any directultracaller to themaxwire value.- Routed models default to the
low..maxladder.ultrais per-model opt-in via thereasoningEffortsprovider config; when opted in it renders its canonical description. - Native
gpt-5.6-*slugs are emitted from the pinned upstream models.json snapshot (src/codex/data/upstream-models.json, openai/codex PR #31684): exact per-slug ladders (sol/terraend atultra,lunaends atmax— no ultra), default efforts (sol=low,terra/luna=medium), real display names/descriptions/NUX, andmulti_agent_version(sol/terrav2,lunav1). ocx adaptations:minimal_client_versionis stripped (it would hide the model from older installed clients) andprefer_websockets/supports_websocketsfollow the central websocket gate. A futuregpt-5.6-*slug the snapshot predates falls back to template synthesis plusensureGpt56ReasoningLevels(appendsmax+ultra). - Snapshot scope is deliberately gpt-5.6-only: the bundled upstream entries for
gpt-5.5/gpt-5.4are staler than the installed catalog's live entries (e.g. snapshot gpt-5.5 carriestool_mode: null), so substituting them would downgrade real data. On-disk sync also self-heals fallback-quality 5.6 entries (display_name stamped with the bare slug) by upgrading them to the snapshot entry; genuine entries from a newer installed codex are preserved untouched. - Snapshot refresh: replace
src/codex/data/upstream-models.jsonwith the latestcodex-rs/models-manager/models.jsonfrom openai/codex (e.g. the periodic "Update models.json" bot PR) and re-run the catalog test suite.
Codex uses a split between config spelling and runtime/catalog spelling:
| Surface | Value |
|---|---|
config.toml persistence |
service_tier = "fast" |
| catalog/request tier id | priority |
| feature gate | [features].fast_mode = true |
| provider/account gate | requires_openai_auth = true |
Native OpenAI passthrough models can keep fast metadata. Routed non-OpenAI models must not inherit that metadata from the native template:
delete additional_speed_tiers
delete service_tier
delete service_tiers
delete default_service_tier
This prevents fast from appearing for providers where Codex/OpenAI priority processing is not a valid request option.
Codex caches models in:
$CODEX_HOME/models_cache.json
After changing providers, hidden models, featured models, or service-tier metadata, opencodex should delete that cache so the next Codex process or model refresh sees the updated catalog.
disabledModels is the single enable/disable choke point for BOTH model families:
- Routed ids stay namespaced (
provider/model) and are excluded from the catalog and/v1/modelsentirely. - Bare ids (no
/) are native GPT passthrough slugs. Their catalog entries are NOT removed — the on-disk sync and the/v1/models?client_versionshape flip them tovisibility: "hide"(codex-rs keeps hidden entries out of the picker itself), so the template/backup/restore paths survive and re-enabling restores the exact entry. Only the bare OpenAI list shape of/v1/modelsomits disabled natives. - The dashboard Models page lists natives from the static supported set
(
nativeModelRows), independent of catalog visibility, so a disabled model stays visible in the GUI for re-enabling. The visibility flip runs as the LAST sync pass (applyNativeVisibility) so the gpt-5.6 snapshot-upgrade branch can never clobber it.
Useful probes:
codex doctor --json
codex debug models
ocx sync
ocx statusExpected high-level result:
- active model provider is
opencodex - provider uses ChatGPT auth reachability semantics
- native
gpt-*entries keep fast support - routed
<provider>/<model>entries arevisibility = "list" - routed entries have no fast/service-tier metadata