Codex: dynamic model catalog from codex debug models + per-model efforts seam #156
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#156
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The codex model/effort catalogs are pinned from
codex debug modelson 0.133.0 (internal/provider/codex/codex.go). The deployed 0.144.1 binary has already drifted: two new models (gpt-5.6-terra,gpt-5.6-luna), a newultraeffort, per-model effort lists, and avisibilityfield. Users can't pick models the binary supports; worse, effort support now varies by model and codex does NOT clamp — an unsupported model+effort combo 400s at the API (live-verified:gpt-5.5+ultra→ 400 "Invalid value: 'max'").Decision (grill 2026-07-13)
Codex gets true dynamic discovery; claude-code stays pinned (it has no machine-readable model listing — drift detection is a separate issue).
Probe
codex debug modelsonce at provider construction (boot), synchronously, through the adapter's existing Runner seam, hard timeout ~10s. Cache for process lifetime — the binary is nix-pinned, so a new binary implies a restart. No TTL, no background refresh.Mapping — trust the binary fully
visibility == "list"(replaces the hardcodedcodex-auto-reviewslug filter).priorityascending; first entry = spawn default (on 0.144.1 the bare default becomesgpt-5.6-terra; stored repo/global defaults still layer on top and skip-layer semantics already handle catalog shrink — a stored default not in the catalog falls through, only explicit per-spawn values 400).display_name. Effort labels from a lab-side map with title-case fallback for unknown values (ultra→ "Ultra").Per-model efforts seam — enriched model options
Models()entries grow per-model fields: efforts list (binary order) + default effort (default_reasoning_level).Efforts()stays as the union catalog — still used by the repo/global defaults pickers, where a model-independent stored effort default is fine because skip-layer validation falls through at spawn.medium, was first-entrylow). Providers without a reported default (claude) keep the first-entry rule.Composer / API
Docs
internal/compat/codex/compat.md: re-pin instructions shrink accordingly; visibility-filter and per-model-effort couplings documented as pinned probe-schema fields.Out of scope
codex debug models+ per-model efforts seam #158Landing audit — PR #158
Verdict: PASS (no blockers; four non-blocking nits below).
Verification signal relied on
ci / native— green (success in 5m13s). Not re-run. That job is the full gate: SPA lint/format/test/build,go build+go testunder theuitag, and golangci-lint.ci-nixflake check is path-gated on**/*.nix,flake.lock,go.mod,go.sum— this diff touches none of them, so its absence is correct rather than a coverage gap.main(branch is behind by the #153 merge; merge-tree resolves clean, no conflicts).Checked
Closes #156; headafk/156authored asdominik.internal/provider/codex/catalog.go): one-shotcodex debug modelsat construction under a 10s budget; 8 MiB stdout cap kills a haywire binary rather than draining it; every malformed-catalog shape the conformance suite would flag (zero listed models, empty slug, empty/duplicate effort, duplicate slug) is a probe error, so the fallback is never bypassed by a bad catalog.Newnever fails on a probe error — one loud Warn, then the compiled-in 0.133.0 list.visibility=="list"filter, stable priority-ascending sort,display_namelabel with slug fallback,default_reasoning_levelsanitized to""when not a member of the model's own list.internal/instance/credential.go): the effort pass now runs against the resolved model's list; explicit effort stays strict (→ the unsupported-combo 400), stored defaults skip-layer, and the all-unset fallback is the model's reported default before the first-entry rule. Model-less providers keep pre-#156 union semantics.CloneModelOptionsdeep-copies the nested per-model effort lists (codex's and claude's catalog entries share one backing array, so a shallow clone would have let one caller poison every model). Conformance now pins this, plus per-model list well-formedness, default membership, and union coverage.resolveEffortOption's candidate order (repo default → global default →default_effort→ first entry) matches the server'slayerSpawnDefaultchain exactly. The composer always POSTs an explicit model+effort, and the server is strict on explicit efforts — so any client/server divergence would surface as a hard 400, not a silent wrong spawn. Stale-pick is impossible: the snap effect is untracked and runs after the provider-change reset.Non-blocking nits (not gating the merge)
internal/httpapi/providers.go—Optionsgets anil → []guard butModels,Efforts, and the new nestedModelOption.Effortsdo not, so a provider returning a nil slice would serializenulland throw inRunChat.tsx's header chip. Unreachable today (both adapters always return non-nil; conformance requires non-empty), but #156 adds a third nil-able slice. Relatedly,api.tstypeseffortsas non-nullable whilespawn.tsdefends with?? []— one of the two is wrong.TestAPI_Providersnever asserts the unioneffortskey is on the wire.internal/provider/providertest/nolink.go—NoLinkFake.Models()returns the shared slice withoutCloneModelOptions, the exact aliasing the conformance check forbids.NoLinkFakenever runs throughConformance(), so nothing fails today.Behaviour change to expect on deploy
The deployed binary is 0.144.1, so the probe (not the fallback) will serve the catalog: the bare spawn default model becomes
gpt-5.6-terraand the bare default effort becomesmedium(was first-entrylow). Both are the grill's explicit decisions, not drift.