From 85cd63775eb5650d67294e413677ef677d32994f Mon Sep 17 00:00:00 2001 From: Andrew Wang Date: Sat, 24 Oct 2026 14:53:36 -0400 Subject: [PATCH 1/3] fix(xai): publish the subscription lane's widest context rung for Grok 4.5-4.7 Grok's OAuth subscription lane (cli-chat-proxy.grok.com) reports its default rung in `context_window` and the selectable ladder in `context_windows`. Probed live 2026-10-24, all four rows read `context_window: 256000` with `context_windows: [256000, 500000]`. `catalogHintsFromModelsApiItem` read only the default rung, so grok-4.5, grok-4.6 and grok-4.7 reached Codex and every client exporter at 256k while the registry table, the generated metadata and the api.x.ai lane all say 500k (`context_length: 500000` there). The 500k the registry already carried could not correct it: `clampObservedModelLimits` lets a configured window LOWER an observation and never raise it, so the lane-reported short window was sticky. Read the ladder and use its widest valid rung, placed immediately above the `context_window` fallback: a ladder outranks the default rung printed beside it, every other recognized field keeps the precedence it had, and a lane that publishes no ladder is unaffected. Inspection is bounded to eight rungs. Verified: tests/codex-integration/catalog-context-window-ladder.test.ts (6 cases), the other catalog-hints fixtures, `bun run typecheck`, and `bun run structure:check` for this doc. --- scripts/test-layout/layout.json | 2 +- src/codex/catalog/model-hints.ts | 27 ++++++ structure/catalog-vendor-context.md | 18 ++++ .../catalog-context-window-ladder.test.ts | 93 +++++++++++++++++++ tests/fixtures/test-layout-expected.json | 2 +- 5 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 tests/codex-integration/catalog-context-window-ladder.test.ts diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 2fdded5a374..aaed7f2a5d6 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -296,7 +296,7 @@ "bun-stream-caps.test.ts": "lib", "cache-diagnostic.test.ts": "usage", "cancel-body-on-abort.test.ts": "server", - "catalog-auto-refresh-client-fanout.test.ts": "codex-integration", "catalog-auto-refresh-scheduler.test.ts": "codex-integration", + "catalog-auto-refresh-client-fanout.test.ts": "codex-integration", "catalog-auto-refresh-scheduler.test.ts": "codex-integration", "catalog-context-window-ladder.test.ts": "codex-integration", "catalog-cursor-search.test.ts": "codex-integration", "catalog-custom-ordering.test.ts": "codex-integration", "catalog-duplicate-slug-dedup.test.ts": "codex-integration", diff --git a/src/codex/catalog/model-hints.ts b/src/codex/catalog/model-hints.ts index 6d9a13d060f..439a7155517 100644 --- a/src/codex/catalog/model-hints.ts +++ b/src/codex/catalog/model-hints.ts @@ -524,6 +524,25 @@ export function positiveSafeInteger(...values: unknown[]): number | undefined { return values.find(value => typeof value === "number" && Number.isSafeInteger(value) && value > 0) as number | undefined; } +/** Bounded inspection of an upstream-published context ladder; no catalogue publishes more. */ +const MAX_CONTEXT_WINDOW_RUNGS = 8; + +/** + * The widest rung of a selectable context ladder an upstream published, if it published one. + * + * A lane that offers a long-context rung announces the whole ladder and reports its default + * rung as the effective window. The capacity is the widest rung, not the default one. + */ +function widestContextWindowRung(value: unknown): number | undefined { + if (!Array.isArray(value)) return undefined; + let widest: number | undefined; + for (const rung of value.slice(0, MAX_CONTEXT_WINDOW_RUNGS)) { + const window = positiveSafeInteger(rung); + if (window !== undefined && (widest === undefined || window > widest)) widest = window; + } + return widest; +} + function normalizedMetadataString(raw: string, maxLength: number): string | undefined { if (raw.length > maxLength * 4 || MODEL_DISCOVERY_METADATA_CONTROL_CHARS.test(raw)) return undefined; const normalized = raw.trim().toLowerCase().replace(/\s+/g, "-").slice(0, maxLength); @@ -724,6 +743,14 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid // real values. Appended after the recognized fields for the same reason as the // llama.cpp entries above: no provider that already resolves changes behavior. capabilityRecord?.context_length, + // The Grok CLI subscription lane publishes its default rung in `context_window` and the + // selectable ladder in `context_windows` (`[256000, 500000]` for grok-4.5/4.6/4.7 on + // 2026-10-24). Reading only the default rung published half of what the lane offers, and + // because a configured or registered window may only LOWER the observed one, the 500k the + // registry already carries could not raise it back. The widest rung is the model's capacity, + // so it outranks the default rung beside it; every other recognized field keeps the + // precedence it already had, and a lane that publishes no ladder is unaffected. + widestContextWindowRung(item.context_windows), // OpenGateway publishes a top-level context_window. Keep this last so catalogs // already resolving through any recognized field retain their existing window. item.context_window, diff --git a/structure/catalog-vendor-context.md b/structure/catalog-vendor-context.md index 2481d17e29f..6006e5603a8 100644 --- a/structure/catalog-vendor-context.md +++ b/structure/catalog-vendor-context.md @@ -43,3 +43,21 @@ Compaction and max input still follow the effective default window, including retained lower soft budgets. Native capacity remains defined by the [native passthrough contract](catalog.md#native-passthrough). Covered by `tests/codex-integration/catalog-native-max-context.test.ts`. + +## Published context ladders + +A lane may publish a selectable window ladder instead of one number. The Grok CLI subscription +lane (`cli-chat-proxy.grok.com`) reports its default rung in `context_window` and the ladder in +`context_windows`; probed live 2026-10-24, all four rows (grok-4.5, grok-4.6, grok-4.7, +grok-4.7-build-fast) read `context_window: 256000` with `context_windows: [256000, 500000]`. +`catalogHintsFromModelsApiItem` in `src/codex/catalog/model-hints.ts` reads the widest valid +rung, so the 500k the registry already carries survives a lane that reports its default rung as +the effective window. Configuration cannot restore that: `clampObservedModelLimits` only lowers +an observation, so a lane-reported short window was previously sticky. + +The read sits immediately above the `context_window` fallback — a ladder outranks the default rung +beside it, while every other recognized field keeps the precedence it already had, and a lane that +publishes no ladder is unaffected. Inspection is bounded to eight rungs. A ladder is the model's +capacity, not evidence that the configured tier serves it; the same rule as above applies, and an +operator who is entitled to only the short rung sets a lower window or an enabled Context cap. +Covered by `tests/codex-integration/catalog-context-window-ladder.test.ts`. diff --git a/tests/codex-integration/catalog-context-window-ladder.test.ts b/tests/codex-integration/catalog-context-window-ladder.test.ts new file mode 100644 index 00000000000..be1693052b6 --- /dev/null +++ b/tests/codex-integration/catalog-context-window-ladder.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, test } from "bun:test"; +import { catalogHintsFromModelsApiItem } from "../../src/codex/catalog/provider-fetch"; + +/** + * Regression coverage for the Grok CLI subscription lane (cli-chat-proxy.grok.com). + * + * That lane's `/v1/models` reports the DEFAULT rung of a selectable context ladder in + * `context_window` and the whole ladder in `context_windows`. Probed live 2026-10-24, all + * four rows (grok-4.7, grok-4.7-build-fast, grok-4.6, grok-4.5) read: + * + * `context_window: 256000, context_windows: [256000, 500000]` + * + * Reading only `context_window` published 256k for models the registry already carries at + * 500k, and it could not be corrected from configuration: `clampObservedModelLimits` lets a + * configured window LOWER the observed one and never raise it. The api.x.ai lane agrees the + * capacity is 500k (`context_length: 500000`); it is only the subscription lane's default rung + * that is 256k. The CLI picks a rung by declaring `context_window` in its own model config, so + * the widest rung is what the model can be asked for. + */ + +const GROK_LANE_ITEM = { + id: "grok-4.7", + object: "model", + owned_by: "xAI", + model: "grok-4.7", + model_family: "xai", + context_window: 256000, + context_windows: [256000, 500000], + auto_compact_threshold_percent: 80, + api_backend: "responses", +}; + +describe("models-API context ladder", () => { + test("a published ladder outranks the default rung beside it", () => { + const hints = catalogHintsFromModelsApiItem("xai", GROK_LANE_ITEM); + expect(hints.contextWindow).toBe(500000); + }); + + test("every rung is read, in any order, and the widest wins", () => { + expect(catalogHintsFromModelsApiItem("xai", { + ...GROK_LANE_ITEM, context_windows: [500000, 256000], + }).contextWindow).toBe(500000); + expect(catalogHintsFromModelsApiItem("xai", { + ...GROK_LANE_ITEM, context_windows: [256000, 400000, 500000], + }).contextWindow).toBe(500000); + }); + + test("a lane that publishes no ladder is unchanged", () => { + const { context_windows: _ladder, ...withoutLadder } = GROK_LANE_ITEM; + expect(catalogHintsFromModelsApiItem("xai", withoutLadder).contextWindow).toBe(256000); + }); + + test("the ladder does not outrank a recognized capability or metadata field", () => { + // Precedence is unchanged for every field that already resolved a window: the ladder only + // fills the slot the default rung used to fill. + expect(catalogHintsFromModelsApiItem("hub", { + id: "hub-row", + capabilities: { context_length: 922000 }, + context_window: 256000, + context_windows: [256000, 500000], + }).contextWindow).toBe(922000); + expect(catalogHintsFromModelsApiItem("zed", { + id: "zed-row", + context_length: 32768, + context_window: 256000, + context_windows: [256000, 500000], + }).contextWindow).toBe(32768); + }); + + test("invalid rungs are ignored, and a ladder with none falls back to the default rung", () => { + for (const context_windows of [ + [0, -1, 1.5, "500000", Number.MAX_SAFE_INTEGER + 1], + [], + "500000", + null, + { 0: 500000 }, + ]) { + expect(catalogHintsFromModelsApiItem("xai", { + ...GROK_LANE_ITEM, context_windows, + }).contextWindow).toBe(256000); + } + }); + + test("inspection is bounded, so an oversized ladder cannot pad the read", () => { + const padded = [...Array.from({ length: 8 }, () => 256000), 500000]; + expect(catalogHintsFromModelsApiItem("xai", { + ...GROK_LANE_ITEM, context_windows: padded, + }).contextWindow).toBe(256000); + expect(catalogHintsFromModelsApiItem("xai", { + ...GROK_LANE_ITEM, context_windows: [500000, ...Array.from({ length: 8 }, () => 256000)], + }).contextWindow).toBe(500000); + }); +}); diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index 32e61e52725..66c422f67aa 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -300,7 +300,7 @@ "bump-dev-version.test.ts": "ci-workflows", "bun-runtime.test.ts": "ci-workflows", "bun-transpiler-cache-scope.test.ts": "cli", "bun-stream-caps.test.ts": "lib", "cache-diagnostic.test.ts": "usage", - "cancel-body-on-abort.test.ts": "server", "catalog-auto-refresh-client-fanout.test.ts": "codex-integration", "catalog-auto-refresh-scheduler.test.ts": "codex-integration", + "cancel-body-on-abort.test.ts": "server", "catalog-auto-refresh-client-fanout.test.ts": "codex-integration", "catalog-auto-refresh-scheduler.test.ts": "codex-integration", "catalog-context-window-ladder.test.ts": "codex-integration", "catalog-cursor-search.test.ts": "codex-integration", "catalog-custom-ordering.test.ts": "codex-integration", "catalog-duplicate-slug-dedup.test.ts": "codex-integration", "catalog-free-pricing-status.test.ts": "codex-integration", "catalog-full-picker-order.test.ts": "codex-integration", From 5e2b329e737627dcb299c7b371e877d2fa1f67e4 Mon Sep 17 00:00:00 2001 From: JUN <243035832+lidge-jun@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:24:33 +0900 Subject: [PATCH 2/3] fix(catalog): preserve provider context limits during capture --- .../docs/reference/configuration/providers.md | 2 +- src/codex/catalog/gather-capture.ts | 3 ++ src/codex/catalog/model-hints.ts | 10 +++-- structure/catalog-vendor-context.md | 8 ++++ structure/catalog.md | 2 +- .../catalog-context-window-ladder.test.ts | 40 +++++++++++++++++++ 6 files changed, 60 insertions(+), 5 deletions(-) diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index d4d626e0c7a..308f9edf5a5 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -325,7 +325,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity | `selectedModels?` | `string[]` | Catalog allowlist after discovery. Non-empty exposes only those ids; empty or omitted exposes all discovered models. | | `retainModels?` | `string[]` | Ids kept in the catalog even when live discovery omits them. They need not be repeated in `models`. Empty or omitted keeps today's behavior. | | `modelDisplayNames?` | `Record` | Durable labels used only for display, keyed by this provider's exact upstream model id. Labels win over provider catalog metadata, survive discovery refreshes and provider edits, and never change authentication, adapter behavior, routing, billing, upstream request construction, the routed `provider/model` selector, or the upstream wire model. Keys are exact and case sensitive. Unknown model ids are kept so a temporarily missing model receives its label when it returns. The map accepts at most 2,000 entries, matching the discovery limit. | -| `contextWindow?` | `number` | Provider-wide context fallback when upstream metadata is absent; otherwise a cap that retains smaller live metadata. The Models dashboard exposes this separately from `providerContextCaps`. | +| `contextWindow?` | `number` | Provider-wide context fallback when upstream metadata is absent; otherwise a cap that retains smaller live metadata. Registry defaults cannot override a lower configured limit; explicit `modelContextWindows` entries take priority. The Models dashboard exposes this separately from `providerContextCaps`. | | `modelContextWindows?` | `Record` | Per-model context fallbacks/caps. These override `contextWindow`: an unknown window uses the configured value, while smaller live metadata remains authoritative. | | `inferContextWindow?` | `boolean` | Custom providers only, default off. `true` fills a missing window from bundled model-ID metadata ([details](#context-windows-on-custom-providers)); the value is an estimate, not verified relay capacity. | | `modelContextTiers?` | `Record` | Per-model upstream tier for `github-copilot`. `long_context` sends `contextTier: "long_context"`. Catalog metadata grows only with an exact per-model `modelContextWindows` value that documents the supported window; that value wins before the provider cap. An unknown model keeps its observed window. Unconfigured passthrough requests retain a caller-supplied `contextTier` field on any provider. | diff --git a/src/codex/catalog/gather-capture.ts b/src/codex/catalog/gather-capture.ts index f193fd7a3d9..0422e3dd120 100644 --- a/src/codex/catalog/gather-capture.ts +++ b/src/codex/catalog/gather-capture.ts @@ -39,6 +39,7 @@ import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, mo import { isModelVisionSidecarConsumer } from "../../vision/eligibility"; import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider, type ModelMetadata } from "../../generated/model-metadata"; import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive"; +import { applyRegistryCapabilitySeedFill } from "./model-hints"; import { captureFastPolicyAuthority, fastPolicyForModel, @@ -360,6 +361,8 @@ export function captureProviderGather( ): CapturedProviderGather { const metadataConfigDir = getConfigDir(); const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured)); + // Bound seed windows while the clone still distinguishes operator settings from defaults. + applyRegistryCapabilitySeedFill(name, enriched); enrichProviderFromRegistry(name, enriched); const registryTransportMatch = providerMatchesRegistryTransport(name, enriched); const registryEntry = registryTransportMatch ? getProviderRegistryEntry(name) : undefined; diff --git a/src/codex/catalog/model-hints.ts b/src/codex/catalog/model-hints.ts index 3618af4ff3a..6eb08778ee9 100644 --- a/src/codex/catalog/model-hints.ts +++ b/src/codex/catalog/model-hints.ts @@ -109,8 +109,8 @@ import type { /** * Fill the registry seed's per-model numeric capability maps beneath the provider's own * values, mutating `prov` in place. The merge is per key — an operator's entry always - * wins; a model the persisted map never mentions picks up its seed value — matching - * `mergeRecordFill` in src/router.ts exactly. + * wins; a model the persisted map never mentions picks up its seed value, bounded + * by the operator's provider-wide context limit before enrichment can hide its origin. * * Routing already performs this fill at resolve time (routedProviderConfig in * src/router.ts) and the catalog did not, and that divergence is #4570: @@ -133,7 +133,11 @@ export function applyRegistryCapabilitySeedFill(name: string, prov: OcxProviderC const entry = getProviderRegistryEntry(name); if (!entry) return; if (entry.modelContextWindows || prov.modelContextWindows) { - prov.modelContextWindows = { ...(entry.modelContextWindows ?? {}), ...(prov.modelContextWindows ?? {}) }; + const providerWindow = typeof prov.contextWindow === "number" && prov.contextWindow > 0 + ? prov.contextWindow : undefined; + const seeds = Object.fromEntries(Object.entries(entry.modelContextWindows ?? {}).map(([id, window]) => + [id, providerWindow !== undefined ? Math.min(window, providerWindow) : window])); + prov.modelContextWindows = { ...seeds, ...(prov.modelContextWindows ?? {}) }; } if (entry.modelMaxOutputTokens || prov.modelMaxOutputTokens) { prov.modelMaxOutputTokens = { ...(entry.modelMaxOutputTokens ?? {}), ...(prov.modelMaxOutputTokens ?? {}) }; diff --git a/structure/catalog-vendor-context.md b/structure/catalog-vendor-context.md index 5f485ad00d2..f52c4e2fd08 100644 --- a/structure/catalog-vendor-context.md +++ b/structure/catalog-vendor-context.md @@ -11,6 +11,14 @@ then Azure metadata, then this estimate, then an enabled context cap as the wind retain their existing policies; OpenAI API metadata used for a custom relay is not Codex subscription capacity. The lookup changes no configuration, credentials, pricing, or request model identity. +`src/codex/catalog/gather-capture.ts` fills registry numeric capability maps on the detached +provider before enrichment. `applyRegistryCapabilitySeedFill` bounds registry context seeds by +an explicitly configured positive provider-wide window, then overlays explicit per-model windows. +Thus enrichment cannot promote a registry default above the operator's limit; explicit per-model +settings still win and smaller observed windows remain authoritative. Capture, hints and Codex +serialization are covered by `tests/codex-integration/catalog-context-window-ladder.test.ts`, +including OAuth xAI's 500k ladder with a 300k provider limit and smaller key-auth limits. + Management reads, strict boolean/null PATCH updates, and same-name overwrite preservation of the optional flag follow the [provider API contract](gui-and-management-api.md#api-ownership). diff --git a/structure/catalog.md b/structure/catalog.md index 0b7be713aa7..49f6e0e5bd6 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -23,7 +23,7 @@ validation-pending. Import alone supplies no entitlement evidence for the model ## Shared catalog Vision-sidecar attachment metadata is projected after provider discovery/cache reads in `src/codex/catalog/routed-gather.ts`. The gather captures `visionSidecar.enabled !== false` before awaiting discovery, and `src/codex/catalog/gather-capture.ts` includes that effective flag in the flight key. Provider TTL/stale rows exclude synthetic sidecar image input; custom rows and synthesized combo members use the same captured switch. Explicit/native image modalities remain intact, and enabled advertisement retains the existing config-based consumer predicate without probing credential availability. An effective Vision switch change through `src/server/management/config-routes.ts` invokes the catalog-only management funnel after persistence to publish both catalog and cache; incomplete work is exposed as `catalogRefreshPending` without claiming client reload. Coverage: `tests/codex-integration/catalog-vision-sidecar-disabled.test.ts` and `tests/vision/sidecar-settings-vision-catalog.test.ts`. -Custom-provider missing windows use [source-labelled vendor estimates](catalog-vendor-context.md) only when the provider opts in with `inferContextWindow: true`; these remain distinct from asserted routing capacity. +Custom-provider missing windows use [source-labelled vendor estimates](catalog-vendor-context.md) only when the provider opts in with `inferContextWindow: true`; these remain distinct from asserted routing capacity. Registry context seed capture preserves [configured window precedence](catalog-vendor-context.md): provider-wide limits bound seed defaults, explicit per-model windows win, and smaller observed limits remain authoritative. Static policy and observed catalog evidence are separate authorities. Antigravity grouping and selection projection follow the [discovered effort-family contract](providers-and-adapters.md#antigravity-effort-families). `src/providers/resolved-model-policy.ts` resolves and freezes only registry/operator static facts, hard wire pins, aliases, and explicit false/empty declarations. Discovery responses, generated diff --git a/tests/codex-integration/catalog-context-window-ladder.test.ts b/tests/codex-integration/catalog-context-window-ladder.test.ts index a83d4310d53..39f2f9b4175 100644 --- a/tests/codex-integration/catalog-context-window-ladder.test.ts +++ b/tests/codex-integration/catalog-context-window-ladder.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; import { applyProviderConfigHints, catalogHintsFromModelsApiItem } from "../../src/codex/catalog/provider-fetch"; import { deriveEntry } from "../../src/codex/catalog/sync"; +import { captureProviderGather } from "../../src/codex/catalog/gather-capture"; import { getProviderRegistryEntry } from "../../src/providers/registry"; import type { OcxProviderConfig } from "../../src/types"; @@ -125,6 +126,45 @@ describe("models-API context ladder", () => { }, model).contextWindow).toBe(500000); }); + for (const { label, authMode, item, overrides, window, compact } of [ + { label: "provider-wide limit", authMode: "oauth", item: GROK_LANE_ITEM, + overrides: { contextWindow: 300000 }, window: 300000, compact: 270000 }, + { label: "partial model map", authMode: "oauth", item: GROK_LANE_ITEM, + overrides: { contextWindow: 300000, modelContextWindows: { "grok-4.6": 200000 } }, window: 300000, compact: 270000 }, + { label: "higher explicit model limit", authMode: "oauth", item: GROK_LANE_ITEM, + overrides: { contextWindow: 300000, modelContextWindows: { "grok-4.7": 400000 } }, window: 400000, compact: 360000 }, + { label: "lower explicit model limit", authMode: "oauth", item: GROK_LANE_ITEM, + overrides: { contextWindow: 300000, modelContextWindows: { "grok-4.7": 200000 } }, window: 200000, compact: 180000 }, + { label: "unconfigured OAuth ladder", authMode: "oauth", item: GROK_LANE_ITEM, + overrides: {}, window: 500000, compact: 450000 }, + { label: "key-auth observed limit", authMode: "key", item: { id: "grok-4.7", context_window: 256000 }, + overrides: { contextWindow: 300000 }, window: 256000, compact: 230400 }, + { label: "key-auth smaller model limit", authMode: "key", item: { id: "grok-4.3", context_window: 131072 }, + overrides: { contextWindow: 300000 }, window: 131072, compact: 117964 }, + ] satisfies { label: string; authMode: "oauth" | "key"; item: typeof GROK_LANE_ITEM | { id: string; context_window: number }; + overrides: Partial; window: number; compact: number }[]) { + test(`capture preserves ${label} through hints and serialization`, () => { + const registry = getProviderRegistryEntry("xai")!; + const configured: OcxProviderConfig = { + adapter: registry.adapter, baseUrl: registry.baseUrl, authMode, ...overrides, + }; + const original = structuredClone(configured); + const captured = captureProviderGather("xai", configured, { + kind: "observed", resolve: () => ({ apiKey: "fixture-token", observed: true }), + }); + const model = applyProviderConfigHints("xai", captured.provider, { + provider: "xai", id: item.id, ...catalogHintsFromModelsApiItem("xai", item), + }); + const entry = deriveEntry(null, `xai/${item.id}`, "Grok", 1, model); + expect(entry).toMatchObject({ + context_window: window, max_context_window: window, auto_compact_token_limit: compact, + opencodex_capability_provenance: { context_window: window }, + }); + expect(configured).toEqual(original); + expect(Object.isFrozen(captured.provider)).toBe(true); + }); + } + test("inspection is bounded, so an oversized ladder cannot pad the read", () => { const padded = [...Array.from({ length: 8 }, () => 256000), 500000]; expect(catalogHintsFromModelsApiItem("xai", { From 1e01307c27cd552483cedc73319bbc00d33a1cd3 Mon Sep 17 00:00:00 2001 From: JUN <243035832+lidge-jun@users.noreply.github.com> Date: Tue, 27 Oct 2026 01:29:20 +0900 Subject: [PATCH 3/3] fix(catalog): preserve case-varied model limit overrides --- src/codex/catalog/model-hints.ts | 5 +++-- structure/catalog-vendor-context.md | 3 ++- .../catalog-context-window-ladder.test.ts | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/codex/catalog/model-hints.ts b/src/codex/catalog/model-hints.ts index 6eb08778ee9..f4978693770 100644 --- a/src/codex/catalog/model-hints.ts +++ b/src/codex/catalog/model-hints.ts @@ -94,6 +94,7 @@ import { disabledNativeSlugs, hasComboTargets, hasNativeOpenAiCapabilityMetadata import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation"; import type { ComboCatalogOmission } from "./aggregation"; import { clampObservedModelLimits, resolveModelPolicy } from "../../providers/resolved-model-policy"; +import { mapFill } from "../../providers/resolved-model-policy-merge"; import type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence"; import type { CatalogAdmissionSnapshot, @@ -137,10 +138,10 @@ export function applyRegistryCapabilitySeedFill(name: string, prov: OcxProviderC ? prov.contextWindow : undefined; const seeds = Object.fromEntries(Object.entries(entry.modelContextWindows ?? {}).map(([id, window]) => [id, providerWindow !== undefined ? Math.min(window, providerWindow) : window])); - prov.modelContextWindows = { ...seeds, ...(prov.modelContextWindows ?? {}) }; + [prov.modelContextWindows] = mapFill(seeds, prov.modelContextWindows); } if (entry.modelMaxOutputTokens || prov.modelMaxOutputTokens) { - prov.modelMaxOutputTokens = { ...(entry.modelMaxOutputTokens ?? {}), ...(prov.modelMaxOutputTokens ?? {}) }; + [prov.modelMaxOutputTokens] = mapFill(entry.modelMaxOutputTokens, prov.modelMaxOutputTokens); } } const NUMERIC_MODEL_ID_SEGMENT = /^\d+$/; diff --git a/structure/catalog-vendor-context.md b/structure/catalog-vendor-context.md index f52c4e2fd08..4bfbf1b1271 100644 --- a/structure/catalog-vendor-context.md +++ b/structure/catalog-vendor-context.md @@ -13,7 +13,8 @@ capacity. The lookup changes no configuration, credentials, pricing, or request `src/codex/catalog/gather-capture.ts` fills registry numeric capability maps on the detached provider before enrichment. `applyRegistryCapabilitySeedFill` bounds registry context seeds by -an explicitly configured positive provider-wide window, then overlays explicit per-model windows. +an explicitly configured positive provider-wide window, then overlays explicit per-model windows +using the static resolver's case-aware map merge for both context and output limits. Thus enrichment cannot promote a registry default above the operator's limit; explicit per-model settings still win and smaller observed windows remain authoritative. Capture, hints and Codex serialization are covered by `tests/codex-integration/catalog-context-window-ladder.test.ts`, diff --git a/tests/codex-integration/catalog-context-window-ladder.test.ts b/tests/codex-integration/catalog-context-window-ladder.test.ts index 39f2f9b4175..90a9532f303 100644 --- a/tests/codex-integration/catalog-context-window-ladder.test.ts +++ b/tests/codex-integration/catalog-context-window-ladder.test.ts @@ -135,6 +135,8 @@ describe("models-API context ladder", () => { overrides: { contextWindow: 300000, modelContextWindows: { "grok-4.7": 400000 } }, window: 400000, compact: 360000 }, { label: "lower explicit model limit", authMode: "oauth", item: GROK_LANE_ITEM, overrides: { contextWindow: 300000, modelContextWindows: { "grok-4.7": 200000 } }, window: 200000, compact: 180000 }, + { label: "case-varied explicit model limit", authMode: "oauth", item: GROK_LANE_ITEM, + overrides: { contextWindow: 300000, modelContextWindows: { "GROK-4.7": 200000 } }, window: 200000, compact: 180000 }, { label: "unconfigured OAuth ladder", authMode: "oauth", item: GROK_LANE_ITEM, overrides: {}, window: 500000, compact: 450000 }, { label: "key-auth observed limit", authMode: "key", item: { id: "grok-4.7", context_window: 256000 }, @@ -165,6 +167,19 @@ describe("models-API context ladder", () => { }); } + test("capture preserves a case-varied explicit output limit beneath registry enrichment", () => { + const registry = getProviderRegistryEntry("xai")!; + const captured = captureProviderGather("xai", { + adapter: registry.adapter, baseUrl: registry.baseUrl, authMode: "oauth", + modelMaxOutputTokens: { "GROK-4.7": 12345 }, + }, { kind: "observed", resolve: () => ({ apiKey: "fixture-token", observed: true }) }); + const model = applyProviderConfigHints("xai", captured.provider, { + provider: "xai", id: GROK_LANE_ITEM.id, ...catalogHintsFromModelsApiItem("xai", GROK_LANE_ITEM), + }); + expect(model.maxOutputTokens).toBe(12345); + expect(captured.provider.modelMaxOutputTokens).not.toHaveProperty("grok-4.7"); + }); + test("inspection is bounded, so an oversized ladder cannot pad the read", () => { const padded = [...Array.from({ length: 8 }, () => 256000), 500000]; expect(catalogHintsFromModelsApiItem("xai", {