Skip to content

feat(models): newly discovered provider models arrive disabled (#2464) - #2609

Merged
lidge-jun merged 2 commits into
devfrom
codex/2464-new-models-off
Aug 25, 2026
Merged

lidge-jun merged 2 commits into
devfrom
codex/2464-new-models-off

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Aug 25, 2026 •

Copy link
Copy Markdown
Owner

Summary

Closes #2464. A provider with liveModels: true re-fetches its /models endpoint on every catalog sync, so any model the vendor publishes overnight appears in the Codex picker silently. For a proxy that routes real spend, the user should opt models IN as they arrive.

The mechanism is a persisted per-provider baseline: a model is new iff it is discovered now and absent from the last successful baseline. New arrivals get a disabledModels row instead of a selectedModels seed — the blocklist composes with an empty allowlist, whereas seeding the allowlist with the full current roster would convert "all on" into a frozen snapshot and silently change what the user's existing state means.

Three properties carry the design:

Each id can be auto-disabled at most once, ever. A model that disappears moves to a tombstone after three consecutive successful fetches without it, and newness is discovered − ids − removed. Since ids ∪ removed grows monotonically, the flapping case holds: a user enables a new arrival, the vendor drops it long enough to prune, the vendor restores it — it comes back enabled. A rename still counts as new, which is the safe reading since pricing may have changed with it.

A degraded fetch can never shrink the baseline. Reconciliation runs only for providers whose gather outcome is authoritative. A provider outage would otherwise mark its entire catalog "new" on recovery.

Bootstrap hides nothing. The first run with no baseline marks everything known. Existing installs have no modelDiscovery key at all, which means policy "on" — today's behavior exactly, with zero change until the user opts in.

Preset mode (#2465) is a deliberate, tested no-op: a non-empty allowlist already excludes arrivals, so the blocklist branch would be redundant state to reconcile later. That interaction is the reason #2464 had to land after #2465 rather than beside it.

Verification

bun x tsc --noEmit                                exit 0
bun test tests/new-model-policy.test.ts \
         tests/model-discovery-management-api.test.ts \
         tests/codex-convergence-contract.test.ts \
         tests/model-presets.test.ts               34 pass / 0 fail
bun run build:gui                                 ok
bun run lint:gui                                  no errors

Falsified per hunk: bootstrap reporting the initial catalog as new, auto-disable slugs suppressed, preset-mode providers auto-disabled, degraded providers reconciled, and each API route hunk removed — every one turned its test red, and all were restored before the final run.

The GUI was rendered against a stubbed management API rather than trusted to typecheck; the first render exposed header crowding, which moved the per-provider override into the expanded body.

Review fix included: reconciliation originally reported changed = true for every authoritative provider, so convergence rewrote config.json on every catalog write even when the roster was byte-identical. That is write amplification that also moves the config generation other writers revalidate against. Now only meaningful fields are compared — updatedAt cannot participate, since it moves on every fetch and would report a change every time. Falsified: forcing changed = true fails the steady-state test while the genuine-arrival test stays green, which pins that the guard narrows the write rather than disabling the policy.

The convergence-call count in codex-convergence-contract.test.ts goes 8 → 10 with a companion assertion naming the two new model-discovery write paths, following the pattern the preset routes established — a bare count that only ever rises stops being a contract.

Checklist

  • Targets dev
  • Focused regression tests, including the full design scenario table
  • docs-site/ updated
  • Strings added to all nine locales
  • No credential, auth, workflow or release-automation surface touched

Summary by CodeRabbit

  • New Features

    • Added controls for automatically handling newly discovered models globally or per provider.
    • Newly discovered models can be marked NEW, disabled by default, enabled, or acknowledged.
    • Added model-arrival counts and badges in the Models screen.
    • Added CLI commands to view or update policies and inspect recent arrivals.
    • Added management API support for policy updates and acknowledging arrivals.
  • Documentation

    • Documented model-discovery behavior, policy options, baseline handling, and CLI commands.
    • Added translations for the new controls and status indicators.

reconcileSuccessfulModelDiscoveries reported changed=true for every
authoritative provider, so convergence persisted config.json on every catalog
write - including the common case where a provider's roster is byte-identical
to the stored baseline. That is write amplification with no user-visible cause,
and it moves the config generation other writers revalidate against.

Compare only the fields that carry meaning. updatedAt cannot participate: it
moves on every successful fetch, so including it would report a change every
time. When nothing else moved, the previous timestamp is kept too, or the very
next comparison would look dirty and reintroduce the rewrite.

Falsified: forcing changed=true unconditionally fails the new steady-state
test; the genuine-arrival test stays green either way, which is what pins that
the guard narrows rather than disables the policy.
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner August 25, 2026 19:45
@lidge-jun
lidge-jun merged commit 9593b24 into dev Aug 25, 2026
5 of 7 checks passed
@lidge-jun
lidge-jun deleted the codex/2464-new-models-off branch August 25, 2026 19:46
@github-actions github-actions Bot added the enhancement New feature or request label Aug 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@coderabbitai

coderabbitai Bot commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 592c49cc-029d-40fd-9f4e-c70bde915eb0

📥 Commits

Reviewing files that changed from the base of the PR and between 6b508d5 and c4cb64f.

📒 Files selected for processing (21)
  • docs-site/src/content/docs/guides/model-routing.md
  • gui/src/i18n/de.ts
  • gui/src/i18n/en.ts
  • gui/src/i18n/fr.ts
  • gui/src/i18n/ja.ts
  • gui/src/i18n/ko.ts
  • gui/src/i18n/ru.ts
  • gui/src/i18n/tr.ts
  • gui/src/i18n/zh-TW.ts
  • gui/src/i18n/zh.ts
  • gui/src/pages/Models.tsx
  • src/cli/init.ts
  • src/cli/models-runtime.ts
  • src/codex/convergence.ts
  • src/providers/new-model-policy.ts
  • src/server/management/model-routes.ts
  • src/types/config.ts
  • src/types/provider.ts
  • tests/codex-convergence-contract.test.ts
  • tests/model-discovery-management-api.test.ts
  • tests/new-model-policy.test.ts

📝 Walkthrough

Walkthrough

The pull request adds persisted model-discovery baselines and configurable global or provider policies. It updates catalog convergence, management APIs, CLI commands, the Models GUI, localized strings, initialization defaults, documentation, and automated tests.

Changes

Model discovery policy

Layer / File(s) Summary
Discovery state and policy engine
src/types/config.ts, src/types/provider.ts, src/providers/new-model-policy.ts, tests/new-model-policy.test.ts
Adds persisted baselines, removal tracking, recent arrivals, global/provider policy resolution, overflow handling, three-fetch removal grace, and automatic disabling for eligible new models. Tests cover bootstrap, arrivals, degraded fetches, unchanged rosters, and timestamp behavior.
Catalog convergence persistence
src/codex/convergence.ts, tests/codex-convergence-contract.test.ts
Reconciles authoritative discoveries against cloned configuration and persists modelDiscovery and disabledModels after successful catalog commits. Contract tests verify convergence calls for model-discovery write paths.
Management API and CLI controls
src/server/management/model-routes.ts, src/cli/models-runtime.ts, src/cli/init.ts, tests/model-discovery-management-api.test.ts
Adds policy GET/PUT operations, arrival acknowledgement, catalog refresh integration, new-policy and new-arrivals CLI commands, and the generated configuration default. API tests cover baseline bootstrap, provider overrides, acknowledgement, and disabled-model preservation.
GUI presentation and documentation
gui/src/pages/Models.tsx, gui/src/i18n/*, docs-site/src/content/docs/guides/model-routing.md
Adds global and provider controls, recent-arrival counts, NEW badges, localized policy strings, and documentation for baseline behavior, failure handling, CLI commands, and curated providers.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: ingwannu

Sequence Diagram(s)

sequenceDiagram
  participant Provider
  participant CatalogConvergence
  participant ModelPolicy
  participant Config
  participant ModelsGUI
  Provider->>CatalogConvergence: return authoritative model roster
  CatalogConvergence->>ModelPolicy: reconcile successful discoveries
  ModelPolicy->>Config: update baseline and disabledModels
  CatalogConvergence->>Config: persist after successful commit
  ModelsGUI->>Config: load discovery state
  ModelsGUI->>Config: save global or provider policy
Loading
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/2464-new-models-off

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • UI screenshot required.

What to do

  • Add a screenshot of the UI change to the PR description.

Automatic draft conversion failed (token cannot change draft status). Please convert this pull request to a draft manually. The required enforce-target check will keep failing until every issue above is resolved.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c4cb64fcee

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +214 to +218
const models = await fetchAllModels(config);
const known = config.modelDiscovery.knownModels ??= {};
const at = new Date().toISOString();
for (const name of Object.keys(config.providers)) {
known[name] ??= { ids: [...new Set(models.filter(m => m.provider === name).map(m => m.id))].sort(), removed: [], updatedAt: at };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bootstrap only from authoritative discovery

When an existing install first switches the global policy from its absent/on-by-default state to off while provider discovery is degraded, fetchAllModels can return stale or static fallback rows, but this block records those rows as the successful baseline for every provider. On the next authoritative recovery, any live IDs omitted from that fallback are classified as new and appended to disabledModels, potentially hiding most of the provider's catalog. Defer baseline creation to convergence's authoritative provider outcomes rather than bootstrapping from the aggregate fallback result.

AGENTS.md reference: src/AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

removed.add(id);
} else missing[id] = count;
}
for (const id of discovered) active.add(id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count the actual baseline union

After a tombstoned model reappears, it is added back to active without being removed from removed, so the sets overlap and this sum counts that ID twice. For a large aggregator with enough restored/flapping IDs, the computed size can exceed 2,000 while the distinct union is still within the limit; reconcileSuccessfulModelDiscoveries then skips the entire result, including arrivals and auto-disables, allowing genuinely new models through. Compute the size of the actual set union or keep the sets disjoint.

Useful? React with 👍 / 👎.

Comment thread gui/src/pages/Models.tsx
<div className="row models-provider-hint">
<span className="muted text-label">{t("models.newPolicyProvider")}</span>
<div className="segmented models-segmented" role="radiogroup" aria-label={t("models.newPolicyProvider")}>
{(["off", "on"] as const).map(mode => (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add a way to restore inherited policy

Once a user selects an explicit per-provider on or off value, later global-policy changes no longer affect that provider, but this control renders only those two explicit modes even though the configuration and DTO include inherit; the PUT endpoint also rejects inherit. The override therefore cannot be cleared through the dashboard or CLI without manually editing config.json. Add the inherited option and normalize it by deleting the provider override.

AGENTS.md reference: gui/AGENTS.md:L7-L10

Useful? React with 👍 / 👎.

tarunravi pushed a commit to tarunravi/opencodex that referenced this pull request Sep 14, 2026
…-jun#2464) (lidge-jun#2609)

* feat(models): disable newly discovered models by policy

* fix(models): do not rewrite config when the roster is unchanged

reconcileSuccessfulModelDiscoveries reported changed=true for every
authoritative provider, so convergence persisted config.json on every catalog
write - including the common case where a provider's roster is byte-identical
to the stored baseline. That is write amplification with no user-visible cause,
and it moves the config generation other writers revalidate against.

Compare only the fields that carry meaning. updatedAt cannot participate: it
moves on every successful fetch, so including it would report a change every
time. When nothing else moved, the previous timestamp is kept too, or the very
next comparison would look dirty and reintroduce the rewrite.

Falsified: forcing changed=true unconditionally fails the new steady-state
test; the genuine-arrival test stays green either way, which is what pins that
the guard narrows rather than disables the policy.
agentHits pushed a commit to agentHits/opencodex that referenced this pull request Sep 17, 2026
…-jun#2464) (lidge-jun#2609)

* feat(models): disable newly discovered models by policy

* fix(models): do not rewrite config when the roster is unchanged

reconcileSuccessfulModelDiscoveries reported changed=true for every
authoritative provider, so convergence persisted config.json on every catalog
write - including the common case where a provider's roster is byte-identical
to the stored baseline. That is write amplification with no user-visible cause,
and it moves the config generation other writers revalidate against.

Compare only the fields that carry meaning. updatedAt cannot participate: it
moves on every successful fetch, so including it would report a change every
time. When nothing else moved, the previous timestamp is kept too, or the very
next comparison would look dirty and reintroduce the rewrite.

Falsified: forcing changed=true unconditionally fails the new steady-state
test; the genuine-arrival test stays green either way, which is what pins that
the guard narrows rather than disables the policy.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant