diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index f2d5f813a86..a0c7e5d9cd7 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -195,6 +195,11 @@ Two exceptions are worth knowing because you can hit them: | OrcaRouter | `ocx login orcarouter-oauth` — consent mints a user-owned, long-lived `sk-orca-…` key, and the request then carries a key | `orcarouter` — the same key pasted by hand | | Meta Muse | `ocx login meta-muse` can import a local Muse Code CLI key or start device login. Meta scopes that credential to its own CLI, so this is an unsupported use: how the calls settle is not observable from the API, and you should treat every call as billable against your account | `meta-model` is the supported path — every call is metered per token, and a Muse Code subscription does not work there | +When the Grok subscription catalog publishes selectable context windows, opencodex uses the +widest published rung: `[256000, 500000]` exposes 500k for Grok 4.5–4.7, including the 4.7 Fast +lane. Your configured model window and enabled Context cap still lower that value. If your plan +serves a shorter window, configure that limit; the catalog does not verify account entitlement. + Cursor, Kiro, Zed Hosted AI and Nous Portal are login-only and have no API-key equivalent. Google Antigravity is login-only too: `ocx login google-antigravity` signs in with your Google account over the Cloud Code Assist wire, and the `google` preset beside it is the AI Studio Gemini API — a different product diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index d712337eacb..e024311e547 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -335,7 +335,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/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index c0ec0cbd94f..096d0b4527f 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/gather-capture.ts b/src/codex/catalog/gather-capture.ts index e60d18d882a..f01219f56a4 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 073bf5ddef6..bd5866616e2 100644 --- a/src/codex/catalog/model-hints.ts +++ b/src/codex/catalog/model-hints.ts @@ -95,6 +95,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, @@ -110,8 +111,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: @@ -134,10 +135,14 @@ 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] = 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+$/; @@ -562,6 +567,25 @@ export function positiveSafeInteger(...values: unknown[]): number | undefined { return values.find(value => typeof value === "number" && Number.isSafeInteger(value) && value > 0) as number | undefined; } +/** Bound inspection of untrusted upstream context ladders. */ +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); @@ -771,6 +795,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 555085a0cdc..4bfbf1b1271 100644 --- a/structure/catalog-vendor-context.md +++ b/structure/catalog-vendor-context.md @@ -11,6 +11,15 @@ 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 +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`, +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). @@ -45,3 +54,28 @@ window, including retained lower soft budgets. Native capacity remains defined b `tests/codex-integration/catalog-native-max-context.test.ts`. The first-party DeepSeek catalog retains `deepseek-v4-pro`: its retirement plan was withdrawn, and the official models endpoint still advertises it. `src/codex/catalog/parsing.ts` does not exclude that first-party row; third-party exclusions remain provider-specific. `src/providers/registry/entries-core.ts` restores its text-only capability, context window, reasoning controls and inbound-scoped Responses route while retaining Flash as the default. Evidence: [official service continuation notice](https://api-docs.deepseek.com/updates/), checked 2026-10-23. + +## 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`; PR #7701 reports a 2026-10-24 observation in which 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`. + +Independent subscription overflow probes recorded a 500000-token rejection ceiling for 4.7 and +build-fast (`devlog/_fin/260930_grok47_build_unify/010_probe-evidence.md`); this is not a successful +near-limit generation test for every model or account. The [official API model table](https://docs.x.ai/developers/models.md) +also lists 500k context tokens for 4.5, 4.6 and 4.7 (checked 2026-10-26), independently of subscription entitlement. +No equivalent 4.5/4.6 boundary probe is recorded here. Effort and Fast/Flex policy projection remain +independent of context parsing; xAI rows retain `supportsFlexTier: false` unless explicitly eligible. diff --git a/structure/catalog.md b/structure/catalog.md index 2a53d3fab81..e0faab25764 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -24,7 +24,7 @@ validation-pending. Import alone supplies no entitlement evidence for the model ## Shared catalog `src/server/models-capabilities.ts` preserves the legacy `/v1/models` negative reasoning signal for an explicit empty effort ladder when no reasoning, thinking-toggle or effort capability is declared. Explicit capability axes take precedence; a declared thinking toggle or reasoning support remains available with no effort rungs. This compatibility projection does not change Codex ladders, Claude thinking discovery, client exports or request wire policy. 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 new file mode 100644 index 00000000000..90a9532f303 --- /dev/null +++ b/tests/codex-integration/catalog-context-window-ladder.test.ts @@ -0,0 +1,192 @@ +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"; + +/** + * 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`. PR #7701 reports that on 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. Independent subscription-lane + * overflow probes confirm a 500k ceiling for Grok 4.7 and build-fast in + * devlog/_fin/260930_grok47_build_unify/010_probe-evidence.md. This is a catalog fixture, + * not a fresh entitlement or successful long-input inference probe for every model/account. + */ + +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("valid rungs survive malformed neighbors", () => { + expect(catalogHintsFromModelsApiItem("xai", { + ...GROK_LANE_ITEM, context_windows: [null, NaN, Infinity, "500000", 0, 500000, -1, 1.5], + }).contextWindow).toBe(500000); + }); + + for (const id of ["grok-4.5", "grok-4.6", "grok-4.7", "grok-4.7-build-fast"]) { + test(`${id} publishes the widest rung through config projection and Codex serialization`, () => { + const registry = getProviderRegistryEntry("xai")!; + const provider: OcxProviderConfig = { ...registry, authMode: "oauth" }; + const hints = catalogHintsFromModelsApiItem("xai", { ...GROK_LANE_ITEM, id }); + const model = applyProviderConfigHints("xai", provider, { provider: "xai", id, ...hints }); + expect(model.contextWindow).toBe(500000); + expect(model.reasoningEfforts).toEqual(id === "grok-4.5" + ? ["low", "medium", "high"] : ["low", "medium", "high", "xhigh"]); + expect(model.supportsFlexTier).toBe(false); + const entry = deriveEntry(null, `xai/${id}`, "Grok", 1, model); + expect(entry.context_window).toBe(500000); + expect(entry.max_context_window).toBe(500000); + expect(entry.opencodex_capability_provenance.context_window).toBe(500000); + expect(entry.service_tiers?.some((tier: { id: string }) => tier.id === "flex") ?? false).toBe(false); + }); + } + + test("configured windows and Context caps still lower the widest observed rung", () => { + const id = GROK_LANE_ITEM.id; + const provider: OcxProviderConfig = { ...getProviderRegistryEntry("xai")!, authMode: "oauth" }; + const model = { provider: "xai", id, ...catalogHintsFromModelsApiItem("xai", GROK_LANE_ITEM) }; + const lower = applyProviderConfigHints("xai", { + ...provider, modelContextWindows: { [id]: 300000 }, modelMaxInputTokens: { [id]: 450000 }, + }, model); + expect(lower.contextWindow).toBe(300000); + expect(lower.maxInputTokens).toBe(300000); + const capped = applyProviderConfigHints("xai", provider, model, 200000); + expect(capped.contextWindow).toBe(200000); + expect(capped.contextCapped).toBe(true); + expect(applyProviderConfigHints("xai", { + ...provider, modelContextWindows: { [id]: 900000 }, + }, 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: "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 }, + 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("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", { + ...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 d1f3dad7a49..6959fb3a702 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -308,7 +308,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",