-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(catalog): per-provider retainModels opt-in (#1690, carries #2860) #3206
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| # wp4 — #1690 retainModels allowlist (two rival PRs) | ||
|
|
||
| Issue #1690 (score 58, labels enhancement/catalog). Two open drafts implement it: | ||
|
|
||
| | | PR #2860 (rrmlima) | PR #2122 (chilung-cgu) | | ||
| | --- | --- | --- | | ||
| | size | +132/-2, 3 files | +726/-25, 15 files | | ||
| | CI at head | fully green (test 1-4, macos, ci) | only hygiene/label/target ran | | ||
| | base | e546c160b, 310 behind dev; cherry-picks cleanly onto `fcf0da257` | same base | | ||
| | retention | `shouldRetainConfiguredProviderModel(name, id, prov)` + `modelInList` | new `providerRetainModels` set inside the merge loop | | ||
| | ids must also be in `models`? | yes (purely retentive) | no (`retainModels` folded into `configuredIds`) | | ||
| | config validation | none (relies on `.passthrough()`) | zod `retainModels` + `nonBlankStringArrayConfigError` | | ||
| | management API / DTO | none | none (neither exposes it via PATCH) | | ||
| | rename migration | none | adds `retainModels` to `MODEL_ID_LISTS` | | ||
| | 404 diagnostic | none | new module state + `warnRetainedModel404Once` wired into 4 request handlers, `CatalogModel.retainedWithoutDiscovery` | | ||
| | docs | none | 5 locales, one table row each | | ||
|
|
||
| ## Decision | ||
|
|
||
| Adopt **#2860 as the base commit** (cherry-picked as `2be9d505d` on | ||
| `codex/retain-models-1690`), then add the pieces that make the opt-in | ||
| discoverable and safe to hand-edit. #2122 is closed as superseded with credit for | ||
| the config/migration design. | ||
|
|
||
| Why #2860 over #2122: the retention decision belongs in the one predicate the | ||
| merge loop already consults; #2122 adds a second set beside it. #2122's 404 | ||
| diagnostic requires module-level maps keyed by provider, a new `CatalogModel` | ||
| field, and edits to `core.ts`, `chat-completions.ts`, `chat-native.ts`, | ||
| `claude-messages.ts` — four hot request paths — to print one warning that the | ||
| upstream error body already carries (`model_not_found`). That is the wrong | ||
| trade for this cycle; the existing `warnDroppedConfiguredIdsOnce` stays as the | ||
| diagnostic for ids that are *not* retained. | ||
|
|
||
| ## What this cycle adds on top of #2860 | ||
|
|
||
| 1. **`retainModels` alone is enough.** `configuredIds` in | ||
| `fetchProviderModelsWithAuth` becomes the ordered union of the Vertex seed, | ||
| `prov.models`, and `prov.retainModels`. An operator who writes | ||
| `"retainModels": ["gemini-3.7-flash"]` should not have to repeat the id in | ||
| `models`; requiring both is the footgun #2122 correctly avoided. #2860's | ||
| "does not invent ids" test flips to assert the union. | ||
| 2. **Schema + load normalization** (`src/config.ts`): `retainModels: | ||
| z.array(z.string().min(1)).transform(normalizeNonBlankStringArray).optional()` | ||
| next to `noStructuredOutputModels`, plus the same `superRefine` entry so a | ||
| hand-edited `"retainModels": "x"` fails with a path instead of being silently | ||
| passed through. | ||
| 3. **Management PATCH + DTO** (`src/server/management/provider-routes.ts`): | ||
| `retainModels` accepted like `noStructuredOutputModels` (`null` clears, | ||
| empty array clears, validated with `nonBlankStringArrayConfigError`), and | ||
| returned in the safe provider DTO so the dashboard/API round-trips it. | ||
| 4. **CLI opt-in** (`src/cli/provider-runtime.ts`): | ||
| `ocx provider edit <name> --retain-models <id,id|->`. This is the easy | ||
| switch: one flag, no JSON editing, `-` clears. Usage string and | ||
| `skills/ocx` surface are regenerated if the capability registry changes | ||
| (it does not — `provider edit` already exists; only the flag list grows). | ||
| 5. **Rename migration** (`src/providers/model-rename-migration.ts`): | ||
| `"retainModels"` added to `MODEL_ID_LISTS` so a retired id is renamed | ||
| rather than resurrected as a ghost row. | ||
| 6. **Docs** (`docs-site/.../reference/configuration/providers.md`): one table | ||
| row after `selectedModels`, and a short paragraph in "Static model | ||
| allowlists" contrasting `selectedModels` (narrows) with `retainModels` | ||
| (preserves). English only this cycle; locales already lag on | ||
| `noStructuredOutputModels` and a missing row does not contradict. | ||
|
|
||
| ## Explicitly not in this cycle | ||
|
|
||
| - Seeding `CALLABLE_CONFIGURED_COMPATIBILITY_MODELS` with antigravity | ||
| `gemini-3.7-flash` (issue step 4). That is a product default, and #1683 is a | ||
| separate issue; with the config key available it no longer needs a release. | ||
| - GUI field. The dashboard provider editor is untouched; the PATCH contract is | ||
| ready for it, and a later PR can add the input with its screenshot. | ||
| - 404-time warning. See "Decision". | ||
|
|
||
| ## Acceptance criteria | ||
|
|
||
| - `retainModels` absent/empty → catalog identical to today (existing | ||
| `tests/codex-catalog.test.ts` retention tests untouched and green). | ||
| - `retainModels: ["x"]`, live omits `x`, `x` not in `models` → `x` present | ||
| with provider hints applied; `droppedConfiguredIds` excludes it. | ||
| - `retainModels: ["x"]`, live returns `x` → single row, no duplicate. | ||
| - `liveModels: false` → `retainModels` ids are part of the static list. | ||
| - Config load rejects `retainModels: "x"` / `[""]` with a | ||
| `providers.<name>.retainModels` path; trims and dedupes valid input. | ||
| - Management PATCH sets/clears; DTO echoes; CLI flag round-trips through PATCH. | ||
| - A test through `fetchProviderModels` (not only `mergeConfiguredModelsIntoLiveCatalog`) proves a retain-only id survives both live discovery and `liveModels: false` (audit r1 blocker 2). | ||
| - CLI treats `-` before `csv` so `--retain-models -` clears (audit r1 blocker 3). | ||
| - `providerCatalogFingerprint` includes `retainModels`. | ||
| - Migration renames a retired id inside `retainModels`. | ||
| - `bun x tsc --noEmit` clean, `bun run privacy:scan` clean, focused files: | ||
| `tests/catalog-retain-models.test.ts`, `tests/codex-catalog.test.ts`, | ||
| `tests/management-provider-validation.test.ts`, | ||
| `tests/model-rename-migration.test.ts` (if present), provider-runtime CLI test. | ||
|
|
||
| ## Files | ||
|
|
||
| - `src/codex/catalog/provider-fetch.ts` — configuredIds union (on top of #2860). | ||
| - `src/config.ts` — schema + superRefine. | ||
| - `src/server/management/provider-routes.ts` — PATCH + DTO. | ||
| - `src/server/auth-cors.ts` — `providerManagementConfigError` validation + safe-config DTO key list (audit r1 blocker 1). | ||
| - `src/cli/provider-runtime.ts` — `--retain-models`. | ||
| - `src/providers/model-rename-migration.ts` — list entry. | ||
| - `src/types/provider.ts` — already added by #2860 (doc comment adjusted for union). | ||
| - `docs-site/src/content/docs/reference/configuration/providers.md`. | ||
| - `tests/catalog-retain-models.test.ts` (extend), `tests/management-provider-validation.test.ts` (extend). | ||
|
|
||
| ## Closure | ||
|
|
||
| PR targets `dev`, `Closes #1690`, description names #2860 as the carried | ||
| source commit (`12e69c200`) and #2122 as design input. After landing: close | ||
| #1690 with the landing SHA, close #2860 as landed-via-carry (author credited in | ||
| the squash trailer), close #2122 as superseded with the reasoning above. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # wp4 audit r1 — synthesis | ||
|
|
||
| Reviewer: grok-4.6 subagent (Feynman), read-only, 7 questions on 040. Verdict: **near-pass / GO-WITH-FIXES** (3 blockers, 3 suggestions). | ||
|
|
||
| ## Answers that confirm the plan | ||
|
|
||
| - Union (Q1): safe only if written as an ordered dedupe set `[vertexDefault?, ...models, ...retainModels]`, replacing the current `seed ? [default] : models` ternary. `configured` is the single seed for static `liveModels:false`, the Cursor filter, the degraded fallback, `droppedConfiguredIds`, and hints, so a retain-only id gets the same context/effort maps as a `models[]` entry. The Vertex seed predicate (`models.length === 0`) stays untouched. | ||
| - Family match (Q2): acceptable, same semantics as `noVisionModels`. `retainModels: ["gpt-oss"]` keeps `gpt-oss:120b` and also invents a bare `gpt-oss` row through the union — extra row, never a drop. | ||
| - selectedModels precedence (Q3): `filterCatalogVisibleModels` and `sync.ts` still hide a retained id when `selectedModels` is non-empty and omits it. Keep that; document "retain ≠ visible". | ||
| - PATCH (Q4): copy `provider-routes.ts:386` verbatim; also GET list (:540). | ||
| - CLI (Q5): no `takeListOption`; use `csv(takeOption)` and special-case `"-"` **before** `csv` (`csv("-")` yields `["-"]`). | ||
| - #2122 (Q6): union, schema, `MODEL_ID_LISTS` are the necessary parts; the 404 module maps and `retainedWithoutDiscovery` are not. The `withConfiguredRetention(live→forCache)` change is incidental — the double merge is the OCX-111 combo-cache contract. Do not copy. | ||
| - No-regression (Q7): absent/empty is a no-op; kimi/xai tables unchanged. | ||
|
|
||
| ## Blockers folded into 040 | ||
|
|
||
| 1. `src/server/auth-cors.ts` was missing from the file list: `providerManagementConfigError` (~693) must validate `retainModels` with `nonBlankStringArrayConfigError`, and the safe-config DTO key list (~794) must include it, otherwise PATCH validation and DTO echo silently miss. | ||
| 2. The "liveModels:false / retain-only present" criterion cannot be proven through `mergeConfiguredModelsIntoLiveCatalog` alone. Add a test that goes through `fetchProviderModels`/the gather path so the union at :1307 is actually exercised. | ||
| 3. CLI: handle `-` before `csv`. | ||
|
|
||
| ## Suggestions taken | ||
|
|
||
| - Docs state that `selectedModels` still narrows what is visible even for retained ids. | ||
| - `retainModels` added to `providerCatalogFingerprint` (:573) so two providers differing only in that list do not share a discovery flight. | ||
| - Flip #2860's "does not invent ids" test to assert the union. | ||
|
|
||
| ## Disposition | ||
|
|
||
| All three blockers are additive edits inside the already-planned files plus one file (`auth-cors.ts`). No scope change. Proceed to B. | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -396,6 +396,19 @@ function applyProviderPatchFields( | |
| } | ||
| touched = true; | ||
| } | ||
| if (Object.hasOwn(rawBody, "retainModels")) { | ||
| const value = rawBody.retainModels; | ||
|
Comment on lines
+399
to
+400
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an operator sets this field with AGENTS.md reference: src/AGENTS.md:L10-L10 Useful? React with 👍 / 👎. |
||
| if (value === null) { | ||
| delete next.retainModels; | ||
| } else { | ||
| const error = nonBlankStringArrayConfigError(value, "retainModels"); | ||
| if (error) return { error }; | ||
| const models = normalizeNonBlankStringArray(value as string[]); | ||
| if (models.length > 0) next.retainModels = models; | ||
| else delete next.retainModels; | ||
| } | ||
| touched = true; | ||
| } | ||
| if (Object.hasOwn(rawBody, "omitReasoningEffortWithToolsModels")) { | ||
| const value = rawBody.omitReasoningEffortWithToolsModels; | ||
| if (value === null) { | ||
|
|
@@ -538,6 +551,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp | |
| modelAutoCompactTokenLimits: p.modelAutoCompactTokenLimits, | ||
| modelSupportsServiceTier: p.modelSupportsServiceTier, | ||
| noStructuredOutputModels: p.noStructuredOutputModels, | ||
| retainModels: p.retainModels, | ||
| omitReasoningEffortWithToolsModels: p.omitReasoningEffortWithToolsModels, | ||
| upstreamHttpVersion: p.upstreamHttpVersion, | ||
| authMode: p.authMode, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This paragraph correctly says retained IDs survive
liveModels: false, but lines 570–571 in the same section still state that this mode exposes onlymodelsand exposes nothing whenmodelsis absent. AretainModels-only configuration now exposes retained rows, as the new runtime path and regression test demonstrate, so the contradictory earlier statement can mislead operators about which models remain visible; describe the static result as the union ofmodelsandretainModels.AGENTS.md reference: docs-site/AGENTS.md:L7-L10
Useful? React with 👍 / 👎.