Skip to content

fix(model_fallbacks): use dotted slugs for codex subscription catalog - #4015

Closed
ColeMatthewBienek wants to merge 17 commits into
omnigent-ai:mainfrom
ColeMatthewBienek:fix/codex-dotted-model-aliases
Closed

fix(model_fallbacks): use dotted slugs for codex subscription catalog#4015
ColeMatthewBienek wants to merge 17 commits into
omnigent-ai:mainfrom
ColeMatthewBienek:fix/codex-dotted-model-aliases

Conversation

@ColeMatthewBienek

Copy link
Copy Markdown

Related issue

Closes #

Summary

Codex sub-agents were failing with HTTP 400 codex_turn_error: "The 'gpt-5-6-terra' model is not supported when using Codex with a ChatGPT account." This started when Codex CLI auth switched to a ChatGPT-subscription login (auth_mode=chatgpt, no OPENAI_API_KEY).

Root cause: _CODEX_MODELS in omnigent/model_fallbacks.py used the dashed vendor ids (gpt-5-6-sol, gpt-5-6-luna, gpt-5-6-terra, gpt-5-5). This tuple backs the (SUBSCRIPTION_KIND, "codex") and (CLI_CONFIG_KIND, "codex") static fallbacks — the ChatGPT-subscription path. Codex's ChatGPT-subscription backend only accepts the dotted marketing slugs (gpt-5.6-sol, gpt-5.6-luna, gpt-5.6-terra, gpt-5.5), which is what a user's ~/.codex/config.toml / ~/.codex/models_cache.json actually use under ChatGPT auth. When an omnigent codex launch applied a model override from this catalog, _pin_codex_config_model wrote the dashed id into the session config.toml and ChatGPT-auth Codex 400'd.

  • Updated _CODEX_MODELS to the dotted slugs.
  • Left the databricks-gateway model catalog (model_catalog.resolve_catalog_model("databricks", ...) and _databricks_* code) untouched — dashed ids are correct there.
  • Verified _MECHANICAL_VENDOR_ID_RE in omnigent/model_override.py already permits . in the tail, so normalize_model_for_provider / canonical_model_spelling pass the dotted ids through unchanged for vendor-direct/subscription kinds — no change needed there.
  • Updated the one test asserting the old dashed subscription/CLI-config catalog (tests/test_model_catalog.py).

Test Plan

  • uv run python -m pytest tests/test_model_catalog.py tests/dev/lint/test_lint_no_hardcoded_models.py -q → 75 passed
  • uv run python -m pytest tests/test_model_override.py tests/repl/test_repl_pending_model_override.py tests/runtime/test_model_override.py tests/server/integration/test_sessions_model_override.py tests/server/test_smart_routing.py tests/test_codex_native_app_server.py -q → 218 passed
  • uv run pre-commit run --files omnigent/model_fallbacks.py tests/test_model_catalog.py → ruff format/check pass; pre-existing pyrefly and no-hardcoded-models failures reproduce identically on main (unrelated files: cursor_executor.py, kubernetes.py, examples/polly/*.yaml) — confirmed via git stash diff, not introduced by this change
  • uv run pyrefly check 2>&1 | grep -i "model_fallbacks\|test_model_catalog" → no output (no errors touching either changed file)

Demo

N/A — non-visual, backend model-id fix.

Type of change

  • Bug fix
  • Feature
  • UI / frontend change
  • Refactor / chore
  • Docs
  • Test / CI
  • Breaking change

Test coverage

  • Unit tests added / updated
  • Integration tests added / updated
  • E2E tests added / updated
  • Manual verification completed
  • Existing tests cover this change
  • Not applicable

Coverage notes

N/A — updated the existing unit test asserting the codex subscription/CLI-config static catalog.

Changelog

Fixed Codex sub-agents 400'ing with codex_turn_error under ChatGPT-subscription auth by using the dotted model slugs (gpt-5.6-sol, etc.) Codex-on-ChatGPT expects

ColeMatthewBienek and others added 17 commits August 3, 2026 07:15
Adds a nullable scheduled_tasks.project_id, plumbed through the store,
REST routes, and sys_scheduled_task_* MCP tools. Create/update reject a
project_id that doesn't exist or isn't owned by the caller; the fire
path files the session after create but soft-fails (logs, keeps the
run alive) if the project has since vanished.
Projects are owned by the RAW user_id (None in single-user/local
mode), never RESERVED_USER_LOCAL — that folding is specific to the
LEVEL_OWNER session grant. The fire path's project lookup was folding
task.user_id to "local" before calling project_store.get, so every
local-mode project looked owner-mismatched and was treated as
vanished, leaving fired sessions unfiled even with a valid project.
Also warns when set_conversation_project() reports no metadata row to
file into, and hardens the fake ProjectStore in tests to enforce owner
equality like the real store.
Round 2 fix: the previous round's raw-user_id lookup only worked for
auth-disabled (owner None) and multi-user (real ids) — it broke the
actual production posture, OMNIGENT_LOCAL_SINGLE_USER=1, where the
auth provider resolves a headerless request to the literal "local"
identity and project CRUD stores that raw value. Scheduled tasks
normalize "the single local user" to user_id=None regardless of which
of the two local modes produced it, so a bare None can't tell them
apart at fire time.

Adds AuthProvider.is_local_single_user() and a single shared
omnigent.server.auth.resolve_project_owner(user_id, local_single_user)
that reconstructs the correct project-owner scope from a normalized
owner value. Both the scheduled-tasks route validation (create AND
update, which share one helper) and the fire path call this same
function, sourced from the same auth_provider instance at wiring time
in app.py, so the two call sites cannot drift apart again.
…ing it at fire time

Round 3 fix: the round-2 fix reconstructed a task's project-owner
scope from the CURRENT server's auth mode every time it fired, but
scheduled_tasks.user_id=None carries no provenance about which mode
was active when the task was created. A restart that flips
OMNIGENT_LOCAL_SINGLE_USER between a task's creation and a later fire
made a still-valid project's owner scope disagree with what the
project was actually created under, and the project would look
vanished.

Adds a nullable scheduled_tasks.project_owner column, written once at
create/update validation time (create/update already resolved and
confirmed the project under the true owner scope — this just persists
that result instead of discarding it). The fire path reads it directly
via a new decode_scheduled_task_project_owner, removing the unsafe
re-resolution entirely for any task created under this column.

A NULL project_owner (a row with project_id set from before this
column existed) is the one accepted gap: decode falls back to the
old current-mode resolution, explicitly documented as a one-time
migration-window limitation, not silently masked. A resolved-to-None
owner (auth fully disabled) is persisted as the "" sentinel so it is
never confused with that NULL/legacy case.

Also narrows UnifiedAuthProvider.is_local_single_user() to be
source-aware — it no longer reports True for an oidc/accounts
provider that happens to be constructed with local_single_user=True,
since only header mode ever consults that flag.
…on-fire

Round 4 fix: a legacy row (project_id set, project_owner NULL) only
got project_owner written when a PATCH itself touched project_id.
Any other PATCH (rename, RRULE change, ...) left it NULL forever, so
the row stayed mode-dependent and could still soft-fail after an
auth-mode switch indefinitely.

Adds two deterministic healing paths so the live fallback is bounded
to "until the row is next touched":

- Heal-on-update: any successful PATCH on a task with project_id set
  and project_owner NULL resolves the owner under the current mode and
  persists it, regardless of which fields the PATCH actually changed.
- Heal-on-fire: when a fire takes the legacy NULL fallback and the
  project lookup succeeds, the resolved owner is written back onto the
  row so the fallback (and its live re-resolution) happens at most once
  per legacy row.

Also documents the cross-restart test's unit-boundary choice (exercises
build_on_fire directly rather than a full two-app restart flow), per
review request to fold in or document.
Round 5 fix: heal-on-update wrote the current-mode-resolved owner
unconditionally on any unrelated PATCH, without confirming the project
actually exists under that scope. A legacy project owned by None
(created auth-disabled), touched by an unrelated rename while the
server runs local-single-user, would get "local" written even though
it doesn't match -- permanently converting a recoverable legacy row
(project_owner NULL, still falls back) into a wrong non-legacy one
(decode no longer falls back), breaking filing even after the mode
reverted.

Adds _heal_legacy_project_owner, mirroring heal-on-fire's existing
discipline: resolve, then a scoped project_store.get, and persist
project_owner ONLY on a confirmed hit. On a miss, the row is left
untouched (project_owner stays NULL) so the live fallback remains
available for a later heal or fire.

Also documents (rather than guards) the inert heal-on-fire race with a
concurrent project_id-clearing PATCH, per review's fold-in-if-cheap
guidance -- a conditional/CAS update isn't worth it for a single stray
column value with no observable effect.

Regression tests: a route-level miss (project_owner stays NULL across
the mismatched-mode PATCH) and its fire-level companion (mismatched-
mode fire also leaves the row legacy, and a later fire in the correct
mode still resolves and files).
Round 6 fix: _heal_legacy_project_owner's scoped project_store.get()
was uncaught, so a transient project-store error during an unrelated
PATCH of a legacy task (rename, RRULE change, ...) would 500 and block
the user's own update -- the heal is optional and must never do that.

Wraps the resolve+lookup in try/except, logging and treating any
failure exactly like a miss (project_owner stays NULL, still
recoverable). Mirrors heal-on-fire, which already never lets a
healing failure break the run.

Also corrects the heal-on-fire race comment: an unrelated PATCH does
NOT self-correct a stale project_owner left by the race (heal-on-update
only touches project_owner when it's NULL) -- only a PATCH that itself
sets project_id again, or deleting the task, does.

Regression test: project_store.get raising during an unrelated PATCH
of a legacy row -> PATCH still succeeds (200, rename applied),
project_owner stays NULL, the failure is logged.
Codex's ChatGPT-subscription backend only accepts dotted marketing
slugs (gpt-5.6-sol, gpt-5.6-luna, gpt-5.6-terra, gpt-5.5), not the
dashed vendor ids. The subscription/CLI-config static fallback catalog
was pinning the dashed spelling, so overriding to one of these models
under ChatGPT auth 400'd with codex_turn_error.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the size/XL Pull request size: XL label Aug 4, 2026
@github-actions
github-actions Bot requested a review from serena-ruan August 4, 2026 04:49
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

@ColeMatthewBienek This PR is a Bug fix, Feature, or UI / frontend change but the Demo section is missing or only contains a placeholder.

These change types require a screenshot or screen recording so reviewers can see the new behaviour without checking out the branch. Please update the Demo section with:

  • A screenshot or screen recording of the change, or
  • A link to a hosted video or GIF showing the new behaviour.

Use N/A only when the change has no user-visible effect whatsoever (e.g. a pure refactor or test-only change). If that's the case, uncheck the relevant type box and check Refactor / chore or Test / CI instead.

@github-actions github-actions Bot added the needs-demo PR needs a demo screenshot or recording label Aug 4, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Closed. If you want to pick this back up, comment /reopen. GitHub only lets maintainers press the Reopen button, so this command does it for you. It needs the source branch to still exist.

@ColeMatthewBienek
ColeMatthewBienek deleted the fix/codex-dotted-model-aliases branch August 10, 2026 23:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-demo PR needs a demo screenshot or recording size/XL Pull request size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants