Skip to content

Catalog: a provider-advertised "video" input modality makes Codex reject the whole catalog #759

Closed

Description

@lidge-jun

Summary

A provider that advertises "video" in a model's input_modalities makes the
Codex config loader reject the entire opencodex-catalog.json. Codex parses
that field as a closed enum (text | image | audio), so one out-of-enum value
takes down every plugin, app and MCP server, not just the offending model.

Observed on zenmux/meta-muse-spark-1.1:

failed to reload config: failed to parse model_catalog_json path
`/Users/…/.codex/opencodex-catalog.json` as JSON:
unknown variant `video`, expected one of `text`, `image`, `audio` at line 2013 column 15

The Codex app surface shows "Unable to load apps" with 0 apps and 0 MCPs.
Nothing in the message points at the model or at opencodex, so the failure reads
as a Codex problem rather than a catalog one.

Why it happened

modelInputModalities() in src/codex/catalog/provider-fetch.ts accepted
"video" in its filter, so provider metadata carried it straight into the
serialized catalog. There was also no normalization at the point where every
entry is finalized, so jawcode metadata and effort sync could reintroduce an
out-of-enum value independently of the provider path.

Note that "video" is legitimate internally: the xAI video bridge
(images.videoBridgeEnabled, src/images/xai-video-client.ts) and the
vision-sidecar modality plumbing both reason about video. The defect is
specifically that the value escapes into the Codex-facing catalog file, whose
schema does not have it.

Scope of the fix

  1. Filter "video" at the provider-metadata source.
  2. Normalize input_modalities to the enum at the single choke point every entry
    passes through (ensureStrictCatalogFields), falling back to ["text"] if
    nothing acceptable survives — an entry with no modality at all is worse than
    a text-only one.
  3. Regression test that an out-of-enum value cannot reach a written catalog by
    any path, including preserveExactInputModalities.
  4. Sweep the remaining serialization boundaries (aggregation/combos, custom rows,
    the management API's catalog write path) for other unvalidated enum fields, and
    repair an already-poisoned catalog on read instead of failing closed.

Item 4 is the part that still needs work: the current fix proves the two known
write paths are clean, not that every field Codex treats as an enum is validated.

Verification

  • tests/catalog-input-modality-enum.test.ts — 5 pass.
  • tests/catalog-vision-sidecar-modalities.test.ts — 12 pass (internal video
    reasoning intentionally unchanged).
  • A user catalog that previously failed to parse now loads with 0 models outside
    the enum.

Activity

  1. github-actions commented on Jul 30, 2026

    @github-actions
    Contributor

    Automated translation bookkeeping — detected language: unknown.

  2. lidge-jun commented on Jul 30, 2026

    @lidge-jun
    OwnerAuthor

    Closing the loop on scope. The two landed fixes cover entry construction: "video" is dropped from the provider-metadata filter, and ensureStrictCatalogFields normalizes the field at the single point every constructed entry passes through.

    An adversarial review of the follow-up plan removed two items I had proposed, both on evidence rather than effort:

    Read-side auto-repair — rejected. readCatalog also serves pristine backups and models_cache.json, so repairing in place there would mutate recovery evidence and user-owned files during mere inspection, and it still could not fix the Codex app until some opencodex command ran. ocx sync is the explicit repair route; it already normalizes every outgoing entry.

    A blanket enum sanitizer — rejected. There are other closed enums (visibility, shell_type, web_search_tool_type), but truncation_policy.mode legitimately carries both bytes and tokens in the upstream snapshot, so a sanitizer hardcoding one value would corrupt valid entries. Those fields also never receive provider data: their only writer is parsing.ts with hardcoded valid values. input_modalities is the one enum field that carries a provider-supplied string through to disk.

    I also had one claim wrong and it is worth recording: I thought the backup-restore path could rewrite the poisoned value. It cannot — writePristineCatalogBackup refuses to back up any catalog containing routed slugs, and the offending model is routed, so it never enters a backup.

    What was actually missing is now covered: a row already poisoned on disk. With no provider configured, sync deliberately preserves stale routed entries, so that row is read back, merged and rewritten — it has to be repaired on the way out or the same rejection repeats. The new test seeds zenmux/meta-muse-spark-1.1 with ["text","image","video"], runs real syncCatalogModels() against an isolated CODEX_HOME, and asserts the slug survives with modalities exactly ["text","image"]. Asserting only "no video in the output" would pass just as happily if a future sync dropped the row instead of cleaning it, silently deleting a provider model.

    Detection power was verified, not assumed: removing the enum filter makes the test fail, restoring it makes it pass.

    Verification: 142 pass across the five catalog suites, including the 12 vision-sidecar cases that keep internal video reasoning for the xAI bridge intact. The reporting user's catalog now parses with 31 models, zero out-of-enum values, and the offending model at ["text","image","audio"].

    Remaining known gap, deliberately not closed here: Codex's parse error does not name ocx sync, so a user hitting this has no recovery hint. That is a CLI/docs diagnostic decision.

  3. Wibias commented on Jul 30, 2026

    @Wibias
    Contributor

    [shipping-github] Research review

    Claim: A provider-advertised video input_modalities value makes Codex reject the entire opencodex-catalog.json.
    Checked against: dev@be177ea5

    Field Finding
    Still an issue on latest development? yes
    Exact issue modelInputModalities still accepts video; ensureStrictCatalogFields does not strip out-of-enum modalities
    Fixed on development? no on tip — despite an owner comment describing landed construction-path filters, those changes are not present on origin/dev
    Open PR covering this? none found
    Duplicate of? none
    Security relevance none — n/a
    Priority high — one bad modality takes down apps/MCP for the whole catalog
    Verdict Needs fix on development

    Evidence: src/codex/catalog/provider-fetch.ts:327 still filters text|image|audio|video. ensureStrictCatalogFields (parsing.ts ~274–276) only default-fills when the field is missing, does not sanitize enum members. No tests/catalog-input-modality-enum.test.ts on tip. Vision-sidecar tests still expect internal video reasoning.
    Gaps: Owner comment may describe local/unpushed work; reconcile before closing.

  4. Wibias commented on Jul 30, 2026

    @Wibias
    Contributor

    [shipping-github] Opened PR #777 to address this.

  5. lidge-jun commented on Jul 31, 2026

    @lidge-jun
    OwnerAuthor

    Fixed on dev (verified at bf9bc1ac8).

    The video modality can no longer reach the catalog. Three write paths filter it independently:

    • src/codex/catalog/provider-fetch.ts:327-333 — filters provider-advertised modalities to text | image | audio at fetch time, with the reason recorded inline: Codex parses input_modalities as a closed enum, so one model's stray value rejected the whole file.
    • src/codex/catalog/parsing.ts:281-285 — sanitizes again on parse.
    • src/codex/catalog/sync.ts:451-458 — applies the same filter to rows preserved from disk, so an entry written by an older build cannot survive a sync.

    That last one matters: filtering only at fetch would have left already-written catalogs broken.

    Closing as fixed. If a provider advertises a modality that still breaks loading, please reopen with the provider and the raw /v1/models row.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions