feat: codex dynamic model catalog from codex debug models + per-model efforts seam #158

Merged
dominik.polakovics merged 2 commits from afk/156 into main 2026-07-13 19:22:53 +02:00

Codex gets true dynamic model discovery; claude-code stays pinned (drift detection is #157). Implements every decision from the 2026-07-13 grill.

Probe (internal/provider/codex/catalog.go)

  • New runs codex debug models once, synchronously, through the adapter's one-shot exec pattern (exec.CommandContext on CodexBin, same house style as login status/logout) — hard 10s timeout (defaultProbeTimeout), stdout capped at 8 MiB. Process-lifetime cache: no TTL, no refresh (nix-pinned binary ⇒ restart is the refresh event).
  • Trust the binary fully: visibility=="list" filter (replaces the pinned-era codex-auto-review slug omission), priority-ascending order — first entry = spawn default, so the bare default becomes gpt-5.6-terra on 0.144.1 — labels from display_name, per-model efforts in binary order, default_reasoning_level sanitized to "" when not a member. Effort labels: lab-side map (xhigh → "Extra high") + title-case fallback (ultra → "Ultra", max → "Max").
  • Any probe failure (missing binary, timeout, nonzero exit, bad/oversize JSON, malformed catalog) serves the compiled-in 0.133.0 catalog (fallbackModels/fallbackEfforts) with ONE loud structured Warn; boot log records catalog source (probe vs fallback) and binary version. No status surface, no metric.
  • ProbeModels exported for the live compat probe; trimmed real 0.144.1 capture committed as internal/provider/codex/testdata/models-0.144.1.json (same trimming convention as the 0.133.0 fixture).

Per-model efforts seam

  • provider.ModelOption = Option + per-model efforts + default_effort (wire shape {"value","label","efforts":[…],"default_effort"?}); Models() []ModelOption; Efforts() stays the union for the repo/global defaults pickers.
  • Spawn resolution (internal/instance/credential.go): explicit effort validates against the resolved model's list → 400 on unsupported combos (codex does not clamp — live-verified gpt-5.5+ultra 400s); stored defaults skip-layer against that list; all-layers-unset fallback = the model's reported default (codex: medium, was first-entry low), first-entry rule kept for providers reporting none (claude).
  • Claude adapter: uniform effort lists on every model (it clamps itself), no reported default. Conformance suite grows the per-model obligations: lists non-empty/unique/labelled, default membership, union coverage, nested-Efforts aliasing checks; providertest fakes enriched (SetCatalogs stays source-compatible, SetModels added for per-model scenarios).

Composer / API

  • GET /providers ships the enriched models. The composer's effort chip catalogs the selected model; a model switch that invalidates the pick snaps to the new model's default (mirrors the server's skip-layer resolution — with stored defaults present, a still-valid layer wins first, exactly like the spawn path); valid picks are kept. Settings/repo defaults pickers keep the flat union — unchanged UX.

Docs

  • ADR-0043 (amends ADR-0037's pinned-catalog decision — pinned list = fallback only); compat.md §1 re-pins the fragile coupling as the probe SCHEMA (fields, visibility semantics, priority ordering), re-pin instructions shrink (a binary bump no longer re-pins catalog values); new live probe joins the LAB_COMPAT_LIVE=1 re-verification set.

Verification

  • CGO_ENABLED=0 go test ./... green; golangci-lint run 0 issues; gofmt clean.
  • Web: 776 tests green (14 new), eslint clean, tsc --noEmit && vite build green.
  • Live against the real codex-cli 0.144.1 on this host: TestCompat_Live_debugModelsProbe PASS — probe yields 4 models [gpt-5.6-terra gpt-5.6-luna gpt-5.5 gpt-5.4-mini], 6-effort union incl. Max/Ultra.

Closes #156

🤖 Generated with Claude Code

https://claude.ai/code/session_01WKhcbE8KTiAN7LA8xnSS6m

Codex gets true dynamic model discovery; claude-code stays pinned (drift detection is #157). Implements every decision from the 2026-07-13 grill. ## Probe (internal/provider/codex/catalog.go) - `New` runs `codex debug models` once, synchronously, through the adapter's one-shot exec pattern (`exec.CommandContext` on CodexBin, same house style as `login status`/`logout`) — hard 10s timeout (`defaultProbeTimeout`), stdout capped at 8 MiB. Process-lifetime cache: no TTL, no refresh (nix-pinned binary ⇒ restart is the refresh event). - Trust the binary fully: `visibility=="list"` filter (replaces the pinned-era codex-auto-review slug omission), priority-ascending order — **first entry = spawn default, so the bare default becomes `gpt-5.6-terra` on 0.144.1** — labels from `display_name`, per-model efforts in binary order, `default_reasoning_level` sanitized to "" when not a member. Effort labels: lab-side map (xhigh → "Extra high") + title-case fallback (`ultra` → "Ultra", `max` → "Max"). - Any probe failure (missing binary, timeout, nonzero exit, bad/oversize JSON, malformed catalog) serves the compiled-in 0.133.0 catalog (`fallbackModels`/`fallbackEfforts`) with ONE loud structured Warn; boot log records catalog source (probe vs fallback) and binary version. No status surface, no metric. - `ProbeModels` exported for the live compat probe; trimmed real 0.144.1 capture committed as `internal/provider/codex/testdata/models-0.144.1.json` (same trimming convention as the 0.133.0 fixture). ## Per-model efforts seam - `provider.ModelOption` = Option + per-model `efforts` + `default_effort` (wire shape `{"value","label","efforts":[…],"default_effort"?}`); `Models() []ModelOption`; **`Efforts()` stays the union** for the repo/global defaults pickers. - Spawn resolution (`internal/instance/credential.go`): explicit effort validates against the **resolved model's** list → 400 on unsupported combos (codex does not clamp — live-verified `gpt-5.5`+`ultra` 400s); stored defaults skip-layer against that list; all-layers-unset fallback = the model's reported default (codex: medium, was first-entry low), first-entry rule kept for providers reporting none (claude). - Claude adapter: uniform effort lists on every model (it clamps itself), no reported default. Conformance suite grows the per-model obligations: lists non-empty/unique/labelled, default membership, union coverage, nested-Efforts aliasing checks; providertest fakes enriched (`SetCatalogs` stays source-compatible, `SetModels` added for per-model scenarios). ## Composer / API - `GET /providers` ships the enriched models. The composer's effort chip catalogs the **selected model**; a model switch that invalidates the pick snaps to the new model's default (mirrors the server's skip-layer resolution — with stored defaults present, a still-valid layer wins first, exactly like the spawn path); valid picks are kept. Settings/repo defaults pickers keep the flat union — unchanged UX. ## Docs - ADR-0043 (amends ADR-0037's pinned-catalog decision — pinned list = fallback only); compat.md §1 re-pins the fragile coupling as the probe SCHEMA (fields, visibility semantics, priority ordering), re-pin instructions shrink (a binary bump no longer re-pins catalog values); new live probe joins the `LAB_COMPAT_LIVE=1` re-verification set. ## Verification - `CGO_ENABLED=0 go test ./...` green; `golangci-lint run` 0 issues; `gofmt` clean. - Web: 776 tests green (14 new), eslint clean, `tsc --noEmit && vite build` green. - Live against the real codex-cli 0.144.1 on this host: `TestCompat_Live_debugModelsProbe` PASS — probe yields 4 models `[gpt-5.6-terra gpt-5.6-luna gpt-5.5 gpt-5.4-mini]`, 6-effort union incl. Max/Ultra. Closes #156 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WKhcbE8KTiAN7LA8xnSS6m
feat: codex dynamic model catalog from codex debug models + per-model efforts seam
All checks were successful
ci / native (pull_request) Successful in 5m13s
43904418f4
Codex model/effort catalogs were compiled-in pins from 0.133.0; the
deployed 0.144.1 binary has drifted (gpt-5.6-terra/-luna, max/ultra
tiers, per-model effort lists) and codex does not clamp — an
unsupported model+effort combo 400s at the API.

Probe: New runs `codex debug models` once at construction (10s
timeout, 8 MiB cap), trusts the binary fully (visibility=="list"
filter, priority-ascending order with first entry as spawn default,
display_name labels, lab-side effort-label map with title-case
fallback), and caches for the process lifetime — the binary is
nix-pinned, so restart is the only refresh event. Any probe failure
serves the compiled-in 0.133.0 catalog as the fallback with one loud
structured warning; the boot log records catalog source and binary
version. ProbeModels is exported for the live compat probe; a trimmed
real 0.144.1 capture pins the probe schema.

Seam: provider.ModelOption enriches Models() with per-model effort
lists + reported default effort. Explicit spawn efforts validate
against the RESOLVED model's list (unsupported combo -> 400); stored
defaults keep skip-layer semantics against that list; the
all-layers-unset fallback becomes the model's reported default
(codex: medium — was first-entry low), first-entry rule kept for
providers reporting none. Efforts() stays the union catalog for the
repo/global defaults pickers. Claude-code stays pinned: uniform
per-model lists (it clamps itself), no reported default. Conformance
grows the per-model obligations (non-empty/unique lists, default
membership, union coverage, nested-slice aliasing).

Composer: the effort chip catalogs the selected model; a model switch
that invalidates the pick snaps to the new model's default, valid
picks are kept; settings pickers keep the flat union.

Docs: ADR-0043 (amends ADR-0037's pinned-catalog decision), compat.md
§1 re-pins the coupling as the probe schema, re-pin instructions
shrink accordingly.

Closes #156

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WKhcbE8KTiAN7LA8xnSS6m
Merge branch 'main' into afk/156
All checks were successful
ci / native (pull_request) Successful in 5m16s
0998e4b4ff
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab!158
No description provided.