From 7a5fafdfacb5d8a6d85ac84ef3bd448543d9d2d2 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Tue, 11 Aug 2026 11:12:43 +0800 Subject: [PATCH 1/2] fix(catalog): apply providerContextCaps.openai to native OpenAI rows Native OpenAI catalog rows ignored providerContextCaps.openai: the fixed native context overrides in metadata.ts stayed at 372k while routed models were capped. Thread the openai provider cap through the catalog entry builders (finishUpstreamNativeEntry, deriveEntry, buildCatalogEntries, mergeCatalogEntriesForSync), the runtime native metadata accessors (nativeOpenAiContextWindow, nativeModelRows), and routing capability evidence so the catalog, management rows, and proxy routing agree. Preserved native rows without a hardcoded override (e.g. gpt-5.4-mini) are capped the same way, and auto-compaction follows the capped window. The 372k native value remains the default when no cap is configured. Closes #1430 --- src/codex/catalog/metadata.ts | 10 ++- src/codex/catalog/parsing.ts | 30 +++++-- src/codex/catalog/provider-fetch.ts | 3 +- src/codex/catalog/sync.ts | 33 ++++++-- src/providers/registry.ts | 6 -- src/routing/capability.ts | 12 ++- tests/codex-catalog.test.ts | 119 +++++++++++++++++++++++++++- tests/native-model-toggle.test.ts | 14 ++++ tests/route-explainability.test.ts | 32 ++++++++ 9 files changed, 229 insertions(+), 30 deletions(-) diff --git a/src/codex/catalog/metadata.ts b/src/codex/catalog/metadata.ts index 80ba299fc0..0c3fd0913f 100644 --- a/src/codex/catalog/metadata.ts +++ b/src/codex/catalog/metadata.ts @@ -107,11 +107,12 @@ const PINNED_NATIVE_CAPABILITY_ENTRIES: Map = new Map( .map(m => [m.slug as string, m]), ); -export function nativeOpenAiContextWindow(slug: string): number | undefined { - return NATIVE_OPENAI_CONTEXT_OVERRIDES[slug]?.contextWindow +export function nativeOpenAiContextWindow(slug: string, contextCap?: number): number | undefined { + const raw = NATIVE_OPENAI_CONTEXT_OVERRIDES[slug]?.contextWindow ?? (typeof PINNED_NATIVE_CAPABILITY_ENTRIES.get(slug)?.context_window === "number" ? PINNED_NATIVE_CAPABILITY_ENTRIES.get(slug)!.context_window as number : undefined); + return applyProviderContextCap(raw, contextCap) ?? raw; } export function nativeInputModalities(slug: string): string[] { @@ -199,11 +200,12 @@ export function desktopVisibleNativeSlugs(config: Pick): Array<{ slug: string; disabled: boolean; contextWindow?: number }> { +export function nativeModelRows(config: Pick): Array<{ slug: string; disabled: boolean; contextWindow?: number }> { const disabled = disabledNativeSlugs(config); const shadowed = configuredNativeAliasSlugs(config); + const openaiContextCap = providerContextCap(config, OPENAI_CODEX_PROVIDER_ID); return NATIVE_OPENAI_MODELS.filter(slug => !shadowed.has(slug)).map(slug => { - const contextWindow = nativeOpenAiContextWindow(slug); + const contextWindow = nativeOpenAiContextWindow(slug, openaiContextCap); return { slug, disabled: disabled.has(slug), ...(contextWindow !== undefined ? { contextWindow } : {}) }; }); } diff --git a/src/codex/catalog/parsing.ts b/src/codex/catalog/parsing.ts index 74a93d7ca9..7d69b811d5 100644 --- a/src/codex/catalog/parsing.ts +++ b/src/codex/catalog/parsing.ts @@ -259,18 +259,34 @@ export function isNativeOpenAiEntry(entry: RawEntry): boolean { return typeof entry.slug === "string" && !entry.slug.includes("/"); } -export function applyNativeOpenAiContextOverride(entry: RawEntry): void { +export function applyNativeOpenAiContextOverride(entry: RawEntry, contextCap?: number): void { const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry) ?? (isNativeOpenAiEntry(entry) ? entry.slug as string : undefined); if (!nativeSlug) return; const override = NATIVE_OPENAI_CONTEXT_OVERRIDES[nativeSlug]; - if (!override) return; - if (typeof override.contextWindow === "number") { - entry.context_window = override.contextWindow; - entry.auto_compact_token_limit = Math.floor(override.contextWindow * 0.9); + if (override) { + if (typeof override.contextWindow === "number") { + const contextWindow = applyProviderContextCap(override.contextWindow, contextCap) ?? override.contextWindow; + entry.context_window = contextWindow; + entry.auto_compact_token_limit = Math.floor(contextWindow * 0.9); + } + if (typeof override.maxContextWindow === "number") { + entry.max_context_window = applyProviderContextCap(override.maxContextWindow, contextCap) ?? override.maxContextWindow; + } + } + // providerContextCaps.openai is a ceiling for native OpenAI rows regardless of where the + // advertised window came from (#1430): preserved rows without a hardcoded override (e.g. + // gpt-5.4-mini) must stay under the cap too, and auto-compaction follows the capped window. + const currentContext = typeof entry.context_window === "number" ? entry.context_window : undefined; + const cappedContext = applyProviderContextCap(currentContext, contextCap); + if (cappedContext !== currentContext && typeof cappedContext === "number") { + entry.context_window = cappedContext; + entry.auto_compact_token_limit = Math.floor(cappedContext * 0.9); } - if (typeof override.maxContextWindow === "number") { - entry.max_context_window = override.maxContextWindow; + const currentMax = typeof entry.max_context_window === "number" ? entry.max_context_window : undefined; + const cappedMax = applyProviderContextCap(currentMax, contextCap); + if (cappedMax !== currentMax) { + entry.max_context_window = cappedMax; } } diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts index 1327197a60..a723fe0878 100644 --- a/src/codex/catalog/provider-fetch.ts +++ b/src/codex/catalog/provider-fetch.ts @@ -1639,6 +1639,7 @@ async function gatherRoutedModelsUncached( // configs that will never need it. } else { const disabled = disabledNativeSlugs(config); + const openaiContextCap = providerContextCap(config, OPENAI_CODEX_PROVIDER_ID); const requiredNativeComboTargets = new Set(listComboIds(config).flatMap(id => { const combo = getCombo(config, id); return combo?.targets.flatMap(target => ( @@ -1649,7 +1650,7 @@ async function gatherRoutedModelsUncached( // A bare native disable key hides the native row, not a combo that targets it. // Keep synthetic native metadata available to those combos. if (disabled.has(slug) && !requiredNativeComboTargets.has(slug)) continue; - const contextWindow = nativeOpenAiContextWindow(slug); + const contextWindow = nativeOpenAiContextWindow(slug, openaiContextCap); if (contextWindow === undefined) continue; const synthetic: CatalogModel = { provider: "openai", diff --git a/src/codex/catalog/sync.ts b/src/codex/catalog/sync.ts index f48c215b10..12866bcb13 100644 --- a/src/codex/catalog/sync.ts +++ b/src/codex/catalog/sync.ts @@ -29,6 +29,7 @@ import type { NormalizedComboConfig } from "../../combos/types"; import { providerDestinationResolvedError } from "../../lib/destination-policy"; import { redactSecretString } from "../../lib/redact"; import upstreamModelsSnapshot from "../data/upstream-models.json"; +import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers"; import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readNativeBaseline } from "./parsing"; @@ -190,9 +191,9 @@ export function effectiveSubagentRoster( return { candidates, advertised, excluded }; } -export function finishUpstreamNativeEntry(clone: RawEntry, priority: number): RawEntry { +export function finishUpstreamNativeEntry(clone: RawEntry, priority: number, contextCap?: number): RawEntry { if (priority !== 9) clone.priority = priority; - applyNativeOpenAiContextOverride(clone); + applyNativeOpenAiContextOverride(clone, contextCap); // GPT-5.6 natives keep their exact upstream ladders (e.g. luna has max but no ultra). // Older natives (gpt-5.5 / 5.4 / 5.4-mini / 5.3-codex-spark) get mock max + ultra // (wire-clamped to xhigh). Ultra is always advertised regardless of v2 toggle. @@ -243,6 +244,7 @@ export function deriveEntry( priority: number, model?: CatalogModel, exactComboSlugs: ReadonlySet = new Set(), + contextCap?: number, ): RawEntry { const preserveExact = isExactComboCatalogModel(model, exactComboSlugs); const isRouted = model !== undefined; @@ -251,7 +253,7 @@ export function deriveEntry( // reasoning ladder — e.g. luna has no ultra — default effort, identity, model_messages) // instead of cloning an older template. const upstream = upstreamNativeEntry(slug); - if (upstream) return finishUpstreamNativeEntry(upstream, priority); + if (upstream) return finishUpstreamNativeEntry(upstream, priority, contextCap); } if (template) { const e = JSON.parse(JSON.stringify(template)) as RawEntry; @@ -286,7 +288,7 @@ export function deriveEntry( applyCatalogModelMetadata(e, model); if (model?.catalogKind) e.opencodex_catalog_kind = model.catalogKind; } else { - applyNativeOpenAiContextOverride(e); + applyNativeOpenAiContextOverride(e, contextCap); if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(e); else ensureUltraReasoningLevel(e); // Non-5.6 natives (5.5, 5.4, 5.4-mini, spark) do not support responses-lite; @@ -324,7 +326,7 @@ export function deriveEntry( if (model && isRouted) applyCatalogMetadata(entry, model.provider, model.id, model.contextCap); applyCatalogModelMetadata(entry, model); if (model?.catalogKind) entry.opencodex_catalog_kind = model.catalogKind; - if (!isRouted) applyNativeOpenAiContextOverride(entry); + if (!isRouted) applyNativeOpenAiContextOverride(entry, contextCap); return ensureStrictCatalogFields(normalizeServiceTiers(entry), { preserveExactInputModalities: preserveExact, isRouted, @@ -343,6 +345,7 @@ export interface ObservedCatalogEntryBuildInput { readonly suppressedBareNativeSlugs: ReadonlySet; readonly disabledNativeAccountSlugs: ReadonlySet; readonly multiAgentV2Enabled: boolean; + readonly openaiContextCap?: number; } /** Build entries with the process-observed Codex feature state. */ @@ -357,6 +360,7 @@ export function buildCatalogEntries( accountSelectors: readonly string[] = [], suppressedBareNativeSlugs: ReadonlySet = new Set(), disabledNativeAccountSlugs: ReadonlySet = new Set(), + contextCap?: number, ): RawEntry[] { return buildCatalogEntriesFromObservedState({ template, @@ -370,6 +374,7 @@ export function buildCatalogEntries( suppressedBareNativeSlugs, disabledNativeAccountSlugs, multiAgentV2Enabled: isMultiAgentV2Enabled(), + openaiContextCap: contextCap, }); } @@ -386,6 +391,7 @@ export function buildCatalogEntriesFromObservedState({ suppressedBareNativeSlugs, disabledNativeAccountSlugs, multiAgentV2Enabled, + openaiContextCap, }: ObservedCatalogEntryBuildInput): RawEntry[] { // Codex's models-manager sorts by `priority` ASC and advertises the first 5 picker-visible // models to spawn_agent (sort_by_key(priority) + MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT=5). Catalog @@ -420,7 +426,7 @@ export function buildCatalogEntriesFromObservedState({ .filter(model => model.provider === COMBO_NAMESPACE) .map(catalogModelSlug)); for (const slug of gptSlugs) { - const native = deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9); + const native = deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap); if (rank.has(slug)) native.priority = rank.get(slug)!; nativeEntries.push(native); const nativeAlias = nativeAliasesBySlug.get(slug); @@ -623,6 +629,7 @@ export interface ObservedCatalogMergeInput { readonly accountBoundEntries: readonly RawEntry[]; readonly suppressedBareNativeSlugs?: ReadonlySet; readonly policy: ObservedCatalogMergePolicy; + readonly openaiContextCap?: number; } /** @@ -652,6 +659,7 @@ export function mergeCatalogEntriesFromObservedState({ accountBoundEntries, suppressedBareNativeSlugs = new Set(), policy, + openaiContextCap, }: ObservedCatalogMergeInput): RawEntry[] { // Raw catalog rows contain nested arrays/objects that normalization mutates. Detach every row at // the observed-core boundary so callers can safely retain evidence objects or repeat the merge. @@ -786,7 +794,7 @@ export function mergeCatalogEntriesFromObservedState({ // genuine catalog entry (real display name) is preserved untouched. if (shouldUpgradeToUpstreamEntry(m)) { const upstream = upstreamNativeEntry(slug)!; - const finished = finishUpstreamNativeEntry(upstream, 9); + const finished = finishUpstreamNativeEntry(upstream, 9, openaiContextCap); finished.priority = nativePriority(slug, upstream.priority); return finished; } @@ -815,6 +823,9 @@ export function mergeCatalogEntriesFromObservedState({ slug, "OpenAI native model (Codex OAuth passthrough).", nativePriority(slug, upstreamNativeEntry(slug)?.priority), + undefined, + new Set(), + openaiContextCap, ); entry.priority = nativePriority(slug, upstreamNativeEntry(slug)?.priority); native.push(entry); @@ -927,7 +938,7 @@ export function mergeCatalogEntriesFromObservedState({ const managedEntries = [...finalRoutedEntries, ...alignedAccountBoundEntries]; const mergedEntries = [...native, ...managedEntries].map(m => { const normalized = normalizeServiceTiers(m); - if (!isNativeAliasCatalogEntry(normalized)) applyNativeOpenAiContextOverride(normalized); + if (!isNativeAliasCatalogEntry(normalized)) applyNativeOpenAiContextOverride(normalized, openaiContextCap); const exactCombo = isExactComboCatalogEntry(m, exactComboSlugs); const e = ensureStrictCatalogFields(normalized, { preserveExactInputModalities: exactCombo, @@ -997,6 +1008,7 @@ export function mergeCatalogEntriesForSync( isNativeAliasCatalogEntry(entry) && typeof entry.slug === "string" ? [entry.slug] : [] )), ), + openaiContextCap?: number, ): RawEntry[] { // Retained for source compatibility with the original helper contract. Raw provider ids must // not suppress same-named native rows; actual admitted combo entries own that decision now. @@ -1031,6 +1043,7 @@ export function mergeCatalogEntriesForSync( includeNativeOpenAi, accountBoundEntries, suppressedBareNativeSlugs, + openaiContextCap, policy: { ...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, warningPolicy: "emit", @@ -1248,6 +1261,7 @@ function writeRetainedCatalogSync({ const hasPhysicalComboProvider = Object.hasOwn(config.providers, COMBO_NAMESPACE); const includeNativeOpenAi = shouldIncludeNativeOpenAi(config); const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config); + const openaiContextCap = providerContextCap(config, OPENAI_CODEX_PROVIDER_ID); const accountSelectors = includeAccountBoundNativeOpenAi ? visibleCodexAccountSelectors(config) : []; @@ -1265,6 +1279,7 @@ function writeRetainedCatalogSync({ suppressedBareNativeSlugs, disabledNativeAccountSlugs: new Set(), multiAgentV2Enabled, + openaiContextCap, }); // Keep genuine native entries (gpt-*, codex-*) with their real per-model fields and append // routed providers as namespaced slugs. Cursor and other adopted providers can expose model ids @@ -1309,6 +1324,7 @@ function writeRetainedCatalogSync({ suppressedBareNativeSlugs, disabledNativeAccountSlugs: new Set([...disabledNativeSlugs(config)].filter(slug => suppressedBareNativeSlugs.has(slug))), multiAgentV2Enabled, + openaiContextCap, }).filter(entry => trustedAccountBoundNativeCatalogSlug(entry) !== undefined) : []; catalog.models = mergeCatalogEntriesFromObservedState({ @@ -1331,6 +1347,7 @@ function writeRetainedCatalogSync({ includeNativeOpenAi, accountBoundEntries, suppressedBareNativeSlugs, + openaiContextCap, policy: { ...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, warningPolicy: "emit", diff --git a/src/providers/registry.ts b/src/providers/registry.ts index bd56aca150..54c6230274 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -300,12 +300,6 @@ const MINIMAX_M3_REASONING_EFFORT_MAP: Record = { const OPENAI_GPT56_MODELS = ["gpt-5.6", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]; const OPENAI_GPT56_PRO_MODELS = ["gpt-5.6-sol-pro", "gpt-5.6-terra-pro", "gpt-5.6-luna-pro"]; const OPENAI_API_GPT56_CONTEXT_WINDOW = 1_050_000; -const OPENAI_CODEX_GPT56_CONTEXT_WINDOW = 372_000; -const OPENAI_GPT56_CONTEXT_WINDOWS = { - "gpt-5.6-sol": OPENAI_CODEX_GPT56_CONTEXT_WINDOW, - "gpt-5.6-terra": OPENAI_CODEX_GPT56_CONTEXT_WINDOW, - "gpt-5.6-luna": OPENAI_CODEX_GPT56_CONTEXT_WINDOW, -}; const OPENAI_API_GPT56_CONTEXT_WINDOWS: Record = { ...Object.fromEntries([...OPENAI_GPT56_MODELS, ...OPENAI_GPT56_PRO_MODELS].map(id => [id, OPENAI_API_GPT56_CONTEXT_WINDOW])), "gpt-5.5": OPENAI_API_GPT56_CONTEXT_WINDOW, diff --git a/src/routing/capability.ts b/src/routing/capability.ts index 157e0e6f9e..b02083f4fd 100644 --- a/src/routing/capability.ts +++ b/src/routing/capability.ts @@ -11,7 +11,8 @@ */ import type { OcxConfig } from "../types"; -import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers"; +import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers"; +import { applyProviderContextCap, providerContextCap } from "../providers/context-cap"; import { PROVIDER_REGISTRY } from "../providers/registry"; import { nativeInputModalities, @@ -145,13 +146,18 @@ export function candidateCapabilityEvidence( const provider = config.providers[providerName]; const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName); const catalogRow = cachedCatalogModels().find(model => model.provider === providerName && model.id === modelId); - const isNative = providerName === "openai" && !modelId.includes("/"); + const isNative = providerName === OPENAI_CODEX_PROVIDER_ID && !modelId.includes("/"); - const contextWindow = provider?.modelContextWindows?.[modelId] + const rawContextWindow = provider?.modelContextWindows?.[modelId] ?? provider?.contextWindow ?? registryEntry?.modelContextWindows?.[modelId] ?? catalogRow?.contextWindow ?? (isNative ? nativeOpenAiContextWindow(modelId) : undefined); + // providerContextCaps.openai also ceilings native OpenAI rows (#1430), so routing + // evidence never contradicts a capped catalog entry. + const contextWindow = isNative + ? applyProviderContextCap(rawContextWindow, providerContextCap(config, OPENAI_CODEX_PROVIDER_ID)) ?? rawContextWindow + : rawContextWindow; const modalities = provider?.modelInputModalities?.[modelId] ?? registryEntry?.modelInputModalities?.[modelId] diff --git a/tests/codex-catalog.test.ts b/tests/codex-catalog.test.ts index 59f9e4aada..e57b1481dc 100644 --- a/tests/codex-catalog.test.ts +++ b/tests/codex-catalog.test.ts @@ -2,7 +2,7 @@ import { afterEach, describe, expect, spyOn, test } from "bun:test"; import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { applyNativeVisibility, augmentRoutedModelsWithMetadata, augmentRoutedModelsWithRegistryOpenAiApiRows, buildCatalogEntries, buildComboCatalogOmission, catalogModelSlug, clampCatalogModelsToCodexSupport, clampEntryToCodexSupportedEfforts, clampedDefaultEffort, CODEX_ACCOUNT_BOUND_CATALOG_KIND, CODEX_NATIVE_ALIAS_CATALOG_KIND, comboCatalogOmissionReason, deriveComboCatalogModel, exactComboCatalogSlugs, filterCatalogVisibleModels, filterSupportedNativeSlugs, gatherRoutedModels as gatherRoutedModelsDirect, isDatedVariantId, isMediaGenerationModelId, loadBundledCodexCatalog, materializeBundledCodexCatalog, mergeCatalogEntriesForSync, NATIVE_OPENAI_MODELS, normalizeRoutedCatalogEntry, resetCatalogRuntimeStateForTests, resetOpenAiApiCatalogWarningStateForTests, resolveComboCatalogMember, shouldExposeRoutedModel } from "../src/codex/catalog"; +import { applyNativeVisibility, augmentRoutedModelsWithMetadata, augmentRoutedModelsWithRegistryOpenAiApiRows, buildCatalogEntries, buildComboCatalogOmission, catalogModelSlug, clampCatalogModelsToCodexSupport, clampEntryToCodexSupportedEfforts, clampedDefaultEffort, CODEX_ACCOUNT_BOUND_CATALOG_KIND, CODEX_NATIVE_ALIAS_CATALOG_KIND, comboCatalogOmissionReason, deriveComboCatalogModel, exactComboCatalogSlugs, filterCatalogVisibleModels, filterSupportedNativeSlugs, gatherRoutedModels as gatherRoutedModelsDirect, isDatedVariantId, isMediaGenerationModelId, loadBundledCodexCatalog, materializeBundledCodexCatalog, mergeCatalogEntriesForSync, NATIVE_OPENAI_MODELS, nativeOpenAiContextWindow, normalizeRoutedCatalogEntry, resetCatalogRuntimeStateForTests, resetOpenAiApiCatalogWarningStateForTests, resolveComboCatalogMember, shouldExposeRoutedModel } from "../src/codex/catalog"; import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, @@ -2402,6 +2402,123 @@ describe("Codex catalog routed normalization", () => { expect(sol?.supports_websockets).toBe(true); }); + test("providerContextCaps.openai ceilings native GPT-5.6 catalog rows (#1430)", () => { + const cap = 272_000; + const entries = buildCatalogEntries( + nativeTemplate(), + ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"], + [], + undefined, + false, + "default", + new Set(), + [], + new Set(), + new Set(), + cap, + ); + for (const slug of ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]) { + const entry = entries.find(e => e.slug === slug); + expect(entry?.context_window).toBe(cap); + expect(entry?.max_context_window).toBe(cap); + expect(entry?.auto_compact_token_limit).toBe(244_800); + } + }); + + test("mergeCatalogEntriesForSync re-applies the openai cap to preserved and upgraded native rows (#1430)", () => { + const cap = 272_000; + const template = nativeTemplate(); + // A preserved genuine row and a fallback-quality row (display_name stamped with + // the bare slug) both pass through the final native-override pass on merge. + const genuineSol = { + ...template, + slug: "gpt-5.6-sol", + display_name: "GPT-5.6-Sol", + context_window: 372_000, + max_context_window: 372_000, + auto_compact_token_limit: 334_800, + supported_reasoning_levels: [ + { effort: "low", description: "l" }, { effort: "high", description: "h" }, + { effort: "max", description: "m" }, { effort: "ultra", description: "u" }, + ], + }; + const merged = mergeCatalogEntriesForSync( + [genuineSol], + [], + new Map(), + [], + false, + new Set(), + template, + new Set(), + new Set(), + "default", + new Set(), + false, + true, + [], + new Set(), + new Set(), + cap, + ); + const sol = merged.find(e => e.slug === "gpt-5.6-sol"); + expect(sol?.context_window).toBe(cap); + expect(sol?.max_context_window).toBe(cap); + expect(sol?.auto_compact_token_limit).toBe(244_800); + // The backfilled luna row (upstream snapshot) is capped the same way. + const luna = merged.find(e => e.slug === "gpt-5.6-luna"); + expect(luna?.context_window).toBe(cap); + expect(luna?.max_context_window).toBe(cap); + expect(luna?.auto_compact_token_limit).toBe(244_800); + }); + + test("preserved gpt-5.4-mini rows get the openai cap without a hardcoded override (#1430)", () => { + const cap = 200_000; + const template = nativeTemplate(); + // gpt-5.4-mini has no NATIVE_OPENAI_CONTEXT_OVERRIDES entry; its windows come + // from the preserved disk row and must still be capped on merge. + const genuine54Mini = { + ...template, + slug: "gpt-5.4-mini", + display_name: "GPT-5.4-Mini", + context_window: 272_000, + max_context_window: 272_000, + auto_compact_token_limit: 244_800, + }; + const merged = mergeCatalogEntriesForSync( + [genuine54Mini], + [], + new Map(), + [], + false, + new Set(), + template, + new Set(), + new Set(), + "default", + new Set(), + false, + true, + [], + new Set(), + new Set(), + cap, + ); + const mini = merged.find(e => e.slug === "gpt-5.4-mini"); + expect(mini?.context_window).toBe(cap); + expect(mini?.max_context_window).toBe(cap); + expect(mini?.auto_compact_token_limit).toBe(180_000); + }); + + test("nativeOpenAiContextWindow applies the openai cap as a ceiling only when provided", () => { + expect(nativeOpenAiContextWindow("gpt-5.6-sol")).toBe(372_000); + expect(nativeOpenAiContextWindow("gpt-5.6-sol", 272_000)).toBe(272_000); + // A cap above the native value is a ceiling, not a floor. + expect(nativeOpenAiContextWindow("gpt-5.6-sol", 500_000)).toBe(372_000); + // Non-5.6 natives are capped the same way. + expect(nativeOpenAiContextWindow("gpt-5.4", 272_000)).toBe(272_000); + }); + test("catalog sync upgrades fallback-quality gpt-5.6 entries but preserves genuine ones", () => { // Fallback-quality: display_name stamped with the bare slug (ocx synthesis signature), // wrong ladder (ultra on luna) left by an older ocx version. diff --git a/tests/native-model-toggle.test.ts b/tests/native-model-toggle.test.ts index b294ba6f3f..8cde1866c4 100644 --- a/tests/native-model-toggle.test.ts +++ b/tests/native-model-toggle.test.ts @@ -66,6 +66,20 @@ describe("native GPT model toggles (bare slugs in disabledModels)", () => { expect(rows.find(r => r.slug === "gpt-5.6-sol")?.contextWindow).toBe(372_000); }); + test("nativeModelRows applies providerContextCaps.openai as a ceiling (#1430)", () => { + const rows = nativeModelRows({ + disabledModels: [], + providerContextCaps: { openai: 272_000 }, + }); + expect(rows.find(r => r.slug === "gpt-5.6-sol")?.contextWindow).toBe(272_000); + expect(rows.find(r => r.slug === "gpt-5.6-luna")?.contextWindow).toBe(272_000); + // gpt-5.5 (272k native) is unchanged by the same cap. + expect(rows.find(r => r.slug === "gpt-5.5")?.contextWindow).toBe(272_000); + // A cap for another provider leaves natives untouched. + const other = nativeModelRows({ providerContextCaps: { "openai-apikey": 128_000 } }); + expect(other.find(r => r.slug === "gpt-5.6-sol")?.contextWindow).toBe(372_000); + }); + test("native aliases suppress their native dashboard row and activate Desktop allowlist pruning", () => { const config = makeConfig({ disabledModels: ["gpt-5.6-sol", "gpt-5.5"], diff --git a/tests/route-explainability.test.ts b/tests/route-explainability.test.ts index db84463294..cf0cc4574a 100644 --- a/tests/route-explainability.test.ts +++ b/tests/route-explainability.test.ts @@ -200,6 +200,38 @@ describe("route explainability (RI-09)", () => { expect(Object.prototype.hasOwnProperty.call(evidence, "encryptedCodexTasks")).toBe(false); }); + test("providerContextCaps.openai ceilings native openai capability evidence (#1430)", () => { + const evidence = candidateCapabilityEvidence({ + ...config(), + providers: { + openai: { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + authMode: "forward", + codexAccountMode: "pool", + }, + }, + providerContextCaps: { openai: 272_000 }, + }, "openai", "gpt-5.6-sol"); + expect(evidence.contextWindow).toBe(272_000); + expect(evidence.encryptedCodexTasks).toBe(true); + }); + + test("native openai capability evidence keeps the 372k default without a cap", () => { + const evidence = candidateCapabilityEvidence({ + ...config(), + providers: { + openai: { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + authMode: "forward", + codexAccountMode: "pool", + }, + }, + }, "openai", "gpt-5.6-sol"); + expect(evidence.contextWindow).toBe(372_000); + }); + test("CLI logs explain encodes request ids and supports --json", async () => { const { handleObserveCommand } = await import("../src/cli/observe"); const calls: Array<{ path: string; init?: RequestInit }> = []; From e266f799a617e7dd02e52cf6abaa84139dfb92b3 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:21:48 +0800 Subject: [PATCH 2/2] fix(catalog): cap live Codex discovery rows --- src/server/index.ts | 17 +++++++++++++-- tests/claude-models-discovery.test.ts | 31 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/server/index.ts b/src/server/index.ts index 60b27973c0..ed761bb401 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -43,7 +43,8 @@ import { import { acquireServerBackgroundLifecycle } from "./background-lifecycle"; import { runOpenAiTierStartupMigration } from "../providers/openai-tier-startup"; import { runAlibabaRegionStartupMigration } from "../providers/alibaba-region-startup"; -import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers"; +import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers"; +import { providerContextCap } from "../providers/context-cap"; import { providerCodexAccountMode } from "../providers/registry"; import type { StorageCleanupPolicy } from "../types"; import { @@ -911,7 +912,19 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server 0 ? NATIVE_OPENAI_MODELS : nativeSlugs; - const entries = buildCatalogEntries(loadCatalogTemplate(), catalogNativeSlugs, goOrdered, config.subagentModels, websocketsEnabled(config), maMode as "v1" | "default" | "v2", exactComboCatalogSlugs(config), accountSelectors, suppressedBareNativeSlugs); + const entries = buildCatalogEntries( + loadCatalogTemplate(), + catalogNativeSlugs, + goOrdered, + config.subagentModels, + websocketsEnabled(config), + maMode as "v1" | "default" | "v2", + exactComboCatalogSlugs(config), + accountSelectors, + suppressedBareNativeSlugs, + new Set(), + providerContextCap(config, OPENAI_CODEX_PROVIDER_ID), + ); return jsonResponse({ models: applyNativeVisibility( entries, diff --git a/tests/claude-models-discovery.test.ts b/tests/claude-models-discovery.test.ts index f5046fffde..07acc676df 100644 --- a/tests/claude-models-discovery.test.ts +++ b/tests/claude-models-discovery.test.ts @@ -156,6 +156,37 @@ test("OpenAI list shape and Codex catalog shape stay unchanged", async () => { } }); +test("Codex discovery applies the OpenAI context cap to native rows (#1430)", async () => { + const config = configWithStaticModels(); + config.providers.openai = { + adapter: "openai-responses", + baseUrl: "https://chatgpt.com/backend-api/codex", + liveModels: false, + }; + config.providerContextCaps = { openai: 272_000 }; + saveConfig(config); + const server = startServer(0); + try { + const response = await fetch(new URL("/v1/models?client_version=1.0.0", server.url)); + expect(response.status).toBe(200); + const json = await response.json() as { + models: Array<{ + slug: string; + context_window?: number; + max_context_window?: number; + auto_compact_token_limit?: number; + }>; + }; + expect(json.models.find(model => model.slug === "gpt-5.6-sol")).toMatchObject({ + context_window: 272_000, + max_context_window: 272_000, + auto_compact_token_limit: 244_800, + }); + } finally { + await server.stop(true); + } +}); + test("exact account disables affect only the matching OpenAI and Codex discovery row", async () => { const config = configWithStaticModels(); config.providers.openai = {