diff --git a/src/codex/catalog.ts b/src/codex/catalog.ts index fe73d48263..e32d5216a5 100644 --- a/src/codex/catalog.ts +++ b/src/codex/catalog.ts @@ -2,7 +2,7 @@ // Public surface preserved exactly; importers keep using "src/codex/catalog". export { isMediaGenerationModelId, shouldExposeRoutedModel, readCodexCatalogPath, readCatalog, normalizeRoutedCatalogEntry, catalogModelSlug, filterSupportedNativeSlugs, catalogModelSupportsReasoningSummaries } from "./catalog/parsing"; export type { CatalogModel, MultiAgentMode } from "./catalog/parsing"; -export { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, CODEX_NATIVE_ALIAS_CATALOG_KIND, NATIVE_DAYBREAK_BLUE_MODEL, NATIVE_GPT56_CONTEXT_WINDOW, NATIVE_GPT56_MAX_INPUT_TOKENS, NATIVE_GPT56_OPT_IN_CONTEXT_WINDOW, NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS, NATIVE_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, isNativeOpenAiCapabilityAliasModel, nativeContextLimits, nativeOpenAiCapabilitySourceSlug, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiContextTier, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, observedAccountBoundNativeEntries, observedAccountBoundNativeOpenAiSlugs, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeInputModalities, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, type NativeContextLimits, type NativeContextLimitsInput } from "./catalog/metadata"; +export { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, CODEX_NATIVE_ALIAS_CATALOG_KIND, NATIVE_DAYBREAK_BLUE_MODEL, NATIVE_GPT56_CONTEXT_WINDOW, NATIVE_GPT56_MAX_INPUT_TOKENS, NATIVE_GPT56_OPT_IN_CONTEXT_WINDOW, NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS, NATIVE_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, isNativeOpenAiCapabilityAliasModel, nativeContextLimits, nativeOpenAiCapabilitySourceSlug, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiMaxOutputTokens, nativeOpenAiContextTier, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, observedAccountBoundNativeEntries, observedAccountBoundNativeOpenAiSlugs, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeInputModalities, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, type NativeContextLimits, type NativeContextLimitsInput } from "./catalog/metadata"; export { isSpawnableCodexCandidate, codexExecInvocation, loadBundledCodexCatalog, materializeBundledCodexCatalog, loadCatalogTemplate } from "./catalog/bundled"; export { nativeEffortClamp, shouldApplyNativeEffortClamp, catalogModelEfforts, codexSupportedReasoningEfforts, clampedDefaultEffort, clampEntryToCodexSupportedEfforts, clampCatalogModelsToCodexSupport } from "./catalog/effort"; export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithMetadata, resolveComboCatalogMember, configuredComboTargetModelsByProvider } from "./catalog/provider-fetch"; diff --git a/src/codex/catalog/aggregation.ts b/src/codex/catalog/aggregation.ts index e43fddbf1d..f97b8295fc 100644 --- a/src/codex/catalog/aggregation.ts +++ b/src/codex/catalog/aggregation.ts @@ -162,6 +162,12 @@ export function deriveComboCatalogModel( contextWindow, ...members.map(member => member.maxInputTokens ?? member.contextWindow!), ); + const knownMaxOutputTokens = members + .map(member => member.maxOutputTokens) + .filter((value): value is number => typeof value === "number" && value > 0); + const maxOutputTokens = knownMaxOutputTokens.length === members.length + ? Math.min(...knownMaxOutputTokens) + : undefined; const autoCompactTokenLimit = Math.min( ...members.map(member => clampAutoCompactTokenLimit( member.contextWindow!, @@ -180,6 +186,7 @@ export function deriveComboCatalogModel( owned_by: COMBO_NAMESPACE, contextWindow, maxInputTokens, + ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}), autoCompactTokenLimit, ...(hasLimitingContextCapMetadata ? { contextCapped } : {}), inputModalities, @@ -320,6 +327,7 @@ export function normalizedOpenAiApiSignature(model: CatalogModel): string { id: model.id, contextWindow: model.contextWindow ?? null, maxInputTokens: model.maxInputTokens ?? null, + maxOutputTokens: model.maxOutputTokens ?? null, autoCompactTokenLimit: model.autoCompactTokenLimit ?? null, inputModalities: [...new Set(model.inputModalities ?? [])].sort(), reasoningEfforts: [...new Set(model.reasoningEfforts ?? [])].sort(), diff --git a/src/codex/catalog/metadata.ts b/src/codex/catalog/metadata.ts index f0bbe6348c..6b134934a9 100644 --- a/src/codex/catalog/metadata.ts +++ b/src/codex/catalog/metadata.ts @@ -271,6 +271,11 @@ export function nativeOpenAiContextWindow(slug: string, limits?: NativeContextLi return narrowToLimits(raw, slug, limits); } +export function nativeOpenAiMaxOutputTokens(slug: string): number | undefined { + const sourceSlug = nativeOpenAiCapabilitySourceSlug(slug); + return positiveInt(getModelMetadata("openai", sourceSlug)?.maxTokens); +} + /** * Long-context tier for a native slug as a (default, long) pair, for clients that let the user * pick a window per request (Cursor's local-agent "Context" selector). The pair is the family's diff --git a/src/codex/catalog/parsing.ts b/src/codex/catalog/parsing.ts index de195b9abc..d9ec821646 100644 --- a/src/codex/catalog/parsing.ts +++ b/src/codex/catalog/parsing.ts @@ -112,6 +112,8 @@ export interface CatalogModel { defaultReasoningEffort?: string; contextWindow?: number; maxInputTokens?: number; + /** Model-scoped output-token ceiling; omitted when no authoritative value is known. */ + maxOutputTokens?: number; /** Soft client compaction threshold; hard context/input limits remain authoritative. */ autoCompactTokenLimit?: number; contextCap?: number; diff --git a/src/codex/catalog/provider-fetch.ts b/src/codex/catalog/provider-fetch.ts index 8025ad9471..f48edcd03c 100644 --- a/src/codex/catalog/provider-fetch.ts +++ b/src/codex/catalog/provider-fetch.ts @@ -81,7 +81,7 @@ import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } fr import { CODEX_CUSTOM_MODEL_CATALOG_KIND, JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing"; import type { CatalogModel } from "./parsing"; -import { disabledNativeSlugs, hasComboTargets, isNativeOpenAiCapabilityAliasModel, NATIVE_GPT56_MAX_INPUT_TOKENS, nativeContextLimits, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiAutoCompactTokenLimit, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata"; +import { disabledNativeSlugs, hasComboTargets, isNativeOpenAiCapabilityAliasModel, NATIVE_GPT56_MAX_INPUT_TOKENS, nativeContextLimits, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiAutoCompactTokenLimit, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiMaxOutputTokens, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata"; import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation"; import type { ComboCatalogOmission } from "./aggregation"; import type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence"; @@ -163,6 +163,7 @@ interface CapturedProviderGather { readonly policy: CatalogProviderDiscoveryPolicySnapshot; readonly request: CapturedModelsRequest; readonly fastPolicyAuthority: FastPolicyAuthority; + readonly metadataModelIdCaseFold: boolean; readonly observedAuth?: ModelsAuthResolution; /** * Configured model ids this provider must keep even when live discovery omits @@ -379,6 +380,7 @@ function captureTrustedOpenAiApiPolicy( models: entry.models, ...(entry.modelContextWindows ? { modelContextWindows: entry.modelContextWindows } : {}), ...(entry.modelMaxInputTokens ? { modelMaxInputTokens: entry.modelMaxInputTokens } : {}), + ...(entry.virtualModels ? { virtualModels: entry.virtualModels } : {}), ...(entry.modelInputModalities ? { modelInputModalities: entry.modelInputModalities } : {}), ...(entry.modelReasoningEfforts ? { modelReasoningEfforts: entry.modelReasoningEfforts } : {}), }); @@ -422,6 +424,7 @@ function captureProviderGather( registryTransportMatch, configured, ); + const metadataModelIdCaseFold = shouldCaseFoldMetadataModelId(name); const observedAuth = authResolver.kind === "observed" && provider.authMode !== "forward" && provider.liveModels !== false @@ -458,6 +461,7 @@ function captureProviderGather( policy, request, fastPolicyAuthority, + metadataModelIdCaseFold, ...(observedAuth ? { observedAuth: Object.freeze({ ...observedAuth }) } : {}), ...(retainConfiguredModelIds && retainConfiguredModelIds.size > 0 ? { retainConfiguredModelIds } @@ -579,6 +583,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco ctx: prov.contextWindow ?? null, ctxW: prov.modelContextWindows ?? null, maxIn: prov.modelMaxInputTokens ?? null, + maxOut: prov.modelMaxOutputTokens ?? null, autoCompact: prov.modelAutoCompactTokenLimits ?? null, inMod: prov.modelInputModalities ?? null, re: prov.modelReasoningEfforts ?? null, @@ -645,6 +650,44 @@ export function configuredMaxInputTokens(prov: OcxProviderConfig, id: string): n return typeof configured === "number" && configured > 0 ? configured : undefined; } +function generatedMaxOutputTokens( + providerName: string, + id: string, + metadataId = id, + metadataModelIdCaseFold?: boolean, +): number | undefined { + const metadataProvider = providerName === OPENAI_API_PROVIDER_ID || providerName === OPENAI_CODEX_PROVIDER_ID + ? "openai" + : resolveMetadataProvider(providerName); + if (!metadataProvider) return undefined; + const metadata = getModelMetadata(metadataProvider, metadataId) + ?? ((metadataModelIdCaseFold ?? (providerName === OPENAI_API_PROVIDER_ID || providerName === OPENAI_CODEX_PROVIDER_ID + ? false + : shouldCaseFoldMetadataModelId(providerName))) + ? getModelMetadataCaseInsensitive(metadataProvider, metadataId) + : undefined); + return positiveSafeInteger(metadata?.maxTokens); +} + +function routedMaxOutputTokens( + providerName: string, + provider: OcxProviderConfig, + model: CatalogModel, + metadataId = model.id, + metadataModelIdCaseFold?: boolean, +): number | undefined { + const discovered = positiveSafeInteger(model.maxOutputTokens); + const generated = generatedMaxOutputTokens(providerName, model.id, metadataId, metadataModelIdCaseFold); + const configured = positiveSafeInteger( + modelRecordValue(provider.modelMaxOutputTokens, model.id), + ); + const authoritative = discovered ?? generated; + if (configured === undefined) return authoritative; + return authoritative === undefined + ? configured + : Math.min(authoritative, configured); +} + export function configuredAutoCompactTokenLimit( prov: OcxProviderConfig | undefined, id: string, @@ -679,10 +722,17 @@ function configuredVerbositySupport(name: string, prov: OcxProviderConfig | unde return prov.supportsVerbosity; } -export function applyProviderConfigHints(name: string, prov: OcxProviderConfig, model: CatalogModel, providerCap?: number): CatalogModel { +export function applyProviderConfigHints( + name: string, + prov: OcxProviderConfig, + model: CatalogModel, + providerCap?: number, + metadataModelIdCaseFold?: boolean, +): CatalogModel { const displayName = configuredModelDisplayName(prov, model.id); const configuredCap = configuredContextWindow(prov, model.id); const configuredMaxInput = configuredMaxInputTokens(prov, model.id); + const maxOutputTokens = routedMaxOutputTokens(name, prov, model, model.id, metadataModelIdCaseFold); const configuredAutoCompact = configuredAutoCompactTokenLimit(prov, model.id); let inputModalities = configuredInputModalities(prov, model.id); // The shared vision-sidecar consumer predicate keeps catalog advertisement and request-time @@ -727,6 +777,7 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig, : configuredMaxInput, } : {}), + ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}), ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}), ...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}), ...(typeof supportsVerbosity === "boolean" ? { supportsVerbosity } : {}), @@ -769,14 +820,26 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig, }; } -export function catalogHintsFromProviderConfig(name: string, prov: OcxProviderConfig, id: string, contextCap?: number): Partial { - const hinted = applyProviderConfigHints(name, prov, { id, provider: name }, contextCap); +export function catalogHintsFromProviderConfig( + name: string, + prov: OcxProviderConfig, + id: string, + contextCap?: number, + metadataModelIdCaseFold?: boolean, +): Partial { + const hinted = applyProviderConfigHints(name, prov, { id, provider: name }, contextCap, metadataModelIdCaseFold); const { provider: _provider, id: _id, ...hints } = hinted; return hints; } -export function applyConfigHintsToCachedModels(name: string, prov: OcxProviderConfig, models: CatalogModel[], contextCap?: number): CatalogModel[] { - return models.map(model => applyProviderConfigHints(name, prov, model, contextCap)); +export function applyConfigHintsToCachedModels( + name: string, + prov: OcxProviderConfig, + models: CatalogModel[], + contextCap?: number, + metadataModelIdCaseFold?: boolean, +): CatalogModel[] { + return models.map(model => applyProviderConfigHints(name, prov, model, contextCap, metadataModelIdCaseFold)); } @@ -793,6 +856,7 @@ interface ComboCatalogMemberFallback { readonly contextWindow?: number; /** Input ceiling when it is lower than the window (native GPT-5.6: 922k under 1.05M). */ readonly maxInputTokens?: number; + readonly maxOutputTokens?: number; readonly autoCompactTokenLimit?: number; readonly inputModalities?: readonly string[]; readonly reasoningEfforts?: readonly string[]; @@ -814,6 +878,7 @@ export function resolveComboCatalogMember( providers: ReadonlyMap, contextCap?: number, fallback?: ComboCatalogMemberFallback, + metadataModelIdCaseFold?: boolean, ): CatalogModel | undefined { const existing = memberByKey.get(targetKey(target)); const prov = providers.get(target.provider); @@ -827,6 +892,10 @@ export function resolveComboCatalogMember( : undefined; const addMaxInput = fallback !== undefined && contextWindow !== undefined && !(typeof member.maxInputTokens === "number" && member.maxInputTokens > 0); + const addMaxOutput = fallback !== undefined + && typeof fallback.maxOutputTokens === "number" + && fallback.maxOutputTokens > 0 + && !(typeof member.maxOutputTokens === "number" && member.maxOutputTokens > 0); const effectiveMaxInput = addMaxInput ? Math.min(fallback?.maxInputTokens ?? contextWindow!, contextWindow!) : member.maxInputTokens; @@ -840,12 +909,13 @@ export function resolveComboCatalogMember( && fallback?.inputModalities !== undefined; const addReasoning = member.reasoningEfforts === undefined && fallback?.reasoningEfforts !== undefined; - if (!addMaxInput && !adjustAutoCompact && !addModalities && !addReasoning) return member; + if (!addMaxInput && !addMaxOutput && !adjustAutoCompact && !addModalities && !addReasoning) return member; return { ...member, // Never claim a larger input budget than the window, and prefer the model's own // measured ceiling when the fallback carries one. ...(addMaxInput ? { maxInputTokens: effectiveMaxInput } : {}), + ...(addMaxOutput ? { maxOutputTokens: fallback!.maxOutputTokens } : {}), ...(adjustAutoCompact && autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}), ...(addModalities ? { inputModalities: [...fallback!.inputModalities!] } : {}), ...(addReasoning ? { reasoningEfforts: [...fallback!.reasoningEfforts!] } : {}), @@ -882,7 +952,7 @@ export function resolveComboCatalogMember( provider: target.provider, }; const hinted = prov - ? applyProviderConfigHints(target.provider, prov, base, contextCap) + ? applyProviderConfigHints(target.provider, prov, base, contextCap, metadataModelIdCaseFold) : base; const hintedContext = typeof hinted.contextWindow === "number" && hinted.contextWindow > 0 ? hinted.contextWindow @@ -923,6 +993,8 @@ export function resolveComboCatalogMember( ?? (prov ? configuredReasoningEfforts(prov, target.model) : undefined) ?? base.reasoningEfforts ?? (fallback?.reasoningEfforts ? [...fallback.reasoningEfforts] : undefined); + const maxOutputTokens = positiveSafeInteger(hinted.maxOutputTokens, base.maxOutputTokens) + ?? (existing || prov ? positiveSafeInteger(fallback?.maxOutputTokens) : undefined); // The model's own measured input ceiling still applies when discovery gave us nothing: // GPT-5.6 advertises a 1.05M window but refuses input past 922k. const effectiveMaxInput = knownMaxInput ?? fallbackMaxInput; @@ -950,6 +1022,7 @@ export function resolveComboCatalogMember( ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}), contextWindow, maxInputTokens, + ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}), ...(autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}), ...(fallbackCapped ? { contextCap, contextCapped: true as const } : {}), }; @@ -1233,6 +1306,12 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid plainRecord(item.meta)?.n_ctx_train, ); const maxInputTokens = positiveSafeInteger(limits?.max_input_tokens, item.max_input_tokens); + const maxOutputTokens = positiveSafeInteger( + capabilityRecord?.max_output_tokens, + limits?.max_output_tokens, + metadata?.max_output_tokens, + item.max_output_tokens, + ); // Some OpenAI-compatible catalogs expose the selectable ladder under // `reasoning_parameters.efforts` instead of the older `reasoning_efforts` key. // Treat both as model metadata: otherwise a valid upstream capability disappears @@ -1260,6 +1339,7 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid return { ...(contextWindow && contextWindow > 0 ? { contextWindow } : {}), ...(maxInputTokens && maxInputTokens > 0 ? { maxInputTokens } : {}), + ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}), ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}), ...(inputModalities ? { inputModalities } : {}), ...(capabilities ? { capabilities } : {}), @@ -1305,7 +1385,7 @@ async function fetchProviderModelsWithAuth( contextCap: number | undefined, resolveAuth: ModelsAuthResolver, ): Promise { - const { name, provider: prov, discovery, request } = captured; + const { name, provider: prov, discovery, request, metadataModelIdCaseFold } = captured; const observed = ( models: CatalogModel[], state: CatalogGatherProviderModelOutcome["state"], @@ -1330,7 +1410,7 @@ async function fetchProviderModelsWithAuth( const configured: CatalogModel[] = configuredIds.map(id => ({ id, provider: name, - ...catalogHintsFromProviderConfig(name, prov, id, contextCap), + ...catalogHintsFromProviderConfig(name, prov, id, contextCap, metadataModelIdCaseFold), })); const withConfiguredRetention = ( models: CatalogModel[], @@ -1345,6 +1425,7 @@ async function fetchProviderModelsWithAuth( contextCap, seedVertexDefault, retainComboTargets: options?.retainComboTargets, + metadataModelIdCaseFold, }); if ( options?.warnDrops === true @@ -1383,7 +1464,7 @@ async function fetchProviderModelsWithAuth( : [{ id: prov.defaultModel, provider: name, - ...catalogHintsFromProviderConfig(name, prov, prov.defaultModel, contextCap), + ...catalogHintsFromProviderConfig(name, prov, prov.defaultModel, contextCap, metadataModelIdCaseFold), }]; const vertexDefaultSeed = seedVertexDefault ? configured[0] : undefined; const withVertexDefaultSeed = (models: CatalogModel[]): CatalogModel[] => ( @@ -1400,7 +1481,7 @@ async function fetchProviderModelsWithAuth( const cachedCursor = getFreshCached(name, ttlMs); if (cachedCursor) { return observed( - withConfiguredRetention(applyConfigHintsToCachedModels(name, prov, cachedCursor)), + withConfiguredRetention(applyConfigHintsToCachedModels(name, prov, cachedCursor, undefined, metadataModelIdCaseFold)), "authoritative", ); } @@ -1408,7 +1489,7 @@ async function fetchProviderModelsWithAuth( const cooling = getStaleCached(name); return observed( withConfiguredRetention( - cooling ? applyConfigHintsToCachedModels(name, prov, cooling) : configured, + cooling ? applyConfigHintsToCachedModels(name, prov, cooling, undefined, metadataModelIdCaseFold) : configured, ), "degraded", ); @@ -1449,7 +1530,7 @@ async function fetchProviderModelsWithAuth( const staleCursor = getStaleCached(name); return observed( withConfiguredRetention( - staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor) : configured, + staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor, undefined, metadataModelIdCaseFold) : configured, ), "degraded", ); @@ -1467,7 +1548,7 @@ async function fetchProviderModelsWithAuth( if (fresh) { return observed( withConfiguredRetention( - withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap)), + withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap, metadataModelIdCaseFold)), ), "authoritative", ); // dedups Codex's frequent /v1/models polling within the TTL @@ -1479,7 +1560,7 @@ async function fetchProviderModelsWithAuth( return observed( withConfiguredRetention( stale - ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap)) + ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap, metadataModelIdCaseFold)) : failedDiscoveryConfigured, ), "degraded", @@ -1519,7 +1600,7 @@ async function fetchProviderModelsWithAuth( return { models: withConfiguredRetention( stale - ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap)) + ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap, metadataModelIdCaseFold)) : failedDiscoveryConfigured, ), fallback: stale ? "stale" : "configured", @@ -1596,7 +1677,7 @@ async function fetchProviderModelsWithAuth( reasoningEfforts: [], ...(model.contextWindow ? { contextWindow: model.contextWindow } : {}), ...(model.inputModalities ? { inputModalities: model.inputModalities } : {}), - }, contextCap)); + }, contextCap, metadataModelIdCaseFold)); const forCache = withConfiguredRetention(live, { retainComboTargets: false }); if (!setCached(name, forCache, Date.now(), cacheGeneration)) { return observed(withConfiguredRetention(configured), "degraded"); @@ -1659,7 +1740,7 @@ async function fetchProviderModelsWithAuth( provider: name, ...(ownedBy ? { owned_by: ownedBy } : {}), ...discoveredHints, - }, contextCap); + }, contextCap, metadataModelIdCaseFold); }) .filter(m => shouldExposeProviderModel(name, m.id)); // Capture the count BEFORE the alias/configured augmentation below pushes extra rows into @@ -1756,6 +1837,7 @@ export function mergeConfiguredModelsIntoLiveCatalog(opts: { contextCap?: number; seedVertexDefault?: boolean; retainComboTargets?: boolean; + metadataModelIdCaseFold?: boolean; }): { models: CatalogModel[]; droppedConfiguredIds: string[] } { const { name, @@ -1765,6 +1847,7 @@ export function mergeConfiguredModelsIntoLiveCatalog(opts: { contextCap, seedVertexDefault, retainComboTargets = true, + metadataModelIdCaseFold, } = opts; const out = [...opts.models]; const present = new Set(out.map(model => model.id)); @@ -1773,7 +1856,7 @@ export function mergeConfiguredModelsIntoLiveCatalog(opts: { if (present.has(candidate.id)) continue; const dated = out.find(live => isDatedVariantId(live.id, candidate.id)); if (dated) { - out.push(applyProviderConfigHints(name, prov, { ...dated, id: candidate.id }, contextCap)); + out.push(applyProviderConfigHints(name, prov, { ...dated, id: candidate.id }, contextCap, metadataModelIdCaseFold)); present.add(candidate.id); continue; } @@ -1959,7 +2042,16 @@ async function gatherRoutedModelsUncached( config, capture.openAiApiPolicy, ); - const all = augmentRoutedModelsWithMetadata(apiAugmented, activeProviders.map(provider => provider.name), config.providers, config) + const metadataModelIdCaseFoldByProvider = new Map( + activeProviders.map(provider => [provider.name, provider.metadataModelIdCaseFold]), + ); + const all = augmentRoutedModelsWithMetadata( + apiAugmented, + activeProviders.map(provider => provider.name), + config.providers, + config, + metadataModelIdCaseFoldByProvider, + ) // Drop image/video generation models (e.g. Grok image/video) by default. Cursor's static catalog // intentionally mirrors Cursor's public model table, including Gemini image preview, so the // exposure decision goes through shouldExposeRoutedModel (single choke point). @@ -2013,6 +2105,9 @@ async function gatherRoutedModelsUncached( // stay separate fields because routed/API rows of the same family run a wider window. // Falls back to the window for slugs with no separate ceiling. maxInputTokens: Math.min(nativeOpenAiMaxInputTokens(slug, openaiContextCap) ?? contextWindow, contextWindow), + ...(nativeOpenAiMaxOutputTokens(slug) !== undefined + ? { maxOutputTokens: nativeOpenAiMaxOutputTokens(slug) } + : {}), autoCompactTokenLimit: nativeOpenAiAutoCompactTokenLimit(slug, openaiContextCap), inputModalities: nativeInputModalities(slug), reasoningEfforts: nativeReasoningEfforts(slug), @@ -2046,6 +2141,9 @@ async function gatherRoutedModelsUncached( ? { contextWindow: nativeContextWindow, ...(nativeAliasMaxInput !== undefined ? { maxInputTokens: nativeAliasMaxInput } : {}), + ...(nativeOpenAiMaxOutputTokens(combo.alias) !== undefined + ? { maxOutputTokens: nativeOpenAiMaxOutputTokens(combo.alias) } + : {}), ...(nativeAliasAutoCompact !== undefined ? { autoCompactTokenLimit: nativeAliasAutoCompact } : {}), inputModalities: nativeInputModalities(combo.alias), reasoningEfforts: nativeReasoningEfforts(combo.alias), @@ -2058,6 +2156,7 @@ async function gatherRoutedModelsUncached( enrichedByName, providerContextCap(config, target.provider), nativeAliasFallback, + metadataModelIdCaseFoldByProvider.get(target.provider), )) .filter((member): member is CatalogModel => member !== undefined); const derived = deriveComboCatalogModel(id, combo, members); @@ -2109,6 +2208,9 @@ async function gatherRoutedModelsUncached( const nativeAliasMaxInputTokens = codexForwardNativeCapabilityAlias ? nativeOpenAiMaxInputTokens(cm.modelId, customNativeLimits) : undefined; + const nativeAliasMaxOutputTokens = codexForwardNativeCapabilityAlias + ? nativeOpenAiMaxOutputTokens(cm.modelId) + : undefined; const configuredMaxInput = rawProvider ? configuredMaxInputTokens(rawProvider, cm.modelId) : undefined; @@ -2120,6 +2222,13 @@ async function gatherRoutedModelsUncached( ...(customContextWindow !== undefined ? [customContextWindow] : []), ) : undefined; + const customMaxOutputTokens = rawProvider + ? routedMaxOutputTokens(cm.provider, rawProvider, { + id: cm.modelId, + provider: cm.provider, + ...(nativeAliasMaxOutputTokens !== undefined ? { maxOutputTokens: nativeAliasMaxOutputTokens } : {}), + }, cm.modelId, metadataModelIdCaseFoldByProvider.get(cm.provider)) + : nativeAliasMaxOutputTokens; const configuredAutoCompact = configuredAutoCompactTokenLimit(rawProvider, cm.modelId); const customAutoCompactTokenLimit = codexForwardNativeCapabilityAlias ? nativeOpenAiAutoCompactTokenLimit(cm.modelId, customNativeLimits) @@ -2146,6 +2255,7 @@ async function gatherRoutedModelsUncached( : codexForwardNativeCapabilityAlias ? { displayName: "Daybreak Blue" } : {}), ...(customContextWindow !== undefined ? { contextWindow: customContextWindow } : {}), ...(customMaxInputTokens !== undefined ? { maxInputTokens: customMaxInputTokens } : {}), + ...(customMaxOutputTokens !== undefined ? { maxOutputTokens: customMaxOutputTokens } : {}), ...(customAutoCompactTokenLimit !== undefined ? { autoCompactTokenLimit: customAutoCompactTokenLimit } : {}), ...(cm.inputModalities ? { inputModalities: cm.inputModalities } @@ -2197,10 +2307,16 @@ async function gatherRoutedModelsUncached( const mergedMaxInput = mergedMaxInputCandidates.length > 0 ? Math.min(...mergedMaxInputCandidates) : undefined; + const mergedMaxOutputCandidates = [base.maxOutputTokens, replaced?.maxOutputTokens] + .filter((value): value is number => typeof value === "number" && value > 0); + const mergedMaxOutput = mergedMaxOutputCandidates.length > 0 + ? Math.min(...mergedMaxOutputCandidates) + : undefined; const merged: CatalogModel = replaced ? { ...base, ...(base.contextWindow === undefined && replaced.contextWindow !== undefined ? { contextWindow: replaced.contextWindow } : {}), ...(mergedMaxInput !== undefined ? { maxInputTokens: mergedMaxInput } : {}), + ...(mergedMaxOutput !== undefined ? { maxOutputTokens: mergedMaxOutput } : {}), ...(base.autoCompactTokenLimit === undefined && replaced.autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit: replaced.autoCompactTokenLimit } : {}), @@ -2335,12 +2451,19 @@ function augmentRoutedModelsWithCapturedOpenAiApiRows( const autoCompactTokenLimit = contextWindow !== undefined && configuredAutoCompact !== undefined ? clampAutoCompactTokenLimit(contextWindow, maxInputTokens, configuredAutoCompact) : undefined; + const maxOutputTokens = routedMaxOutputTokens( + OPENAI_API_PROVIDER_ID, + configured, + existingById.get(id) ?? { provider: OPENAI_API_PROVIDER_ID, id }, + policy.virtualModels?.[id]?.wireModelId ?? id, + ); return { provider: OPENAI_API_PROVIDER_ID, id, owned_by: OPENAI_API_PROVIDER_ID, ...(contextWindow ? { contextWindow } : {}), ...(maxInputTokens ? { maxInputTokens } : {}), + ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}), ...(autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}), ...(policy.modelInputModalities?.[id] ? { inputModalities: [...policy.modelInputModalities[id]!] } : {}), ...(policy.modelReasoningEfforts?.[id] ? { reasoningEfforts: [...policy.modelReasoningEfforts[id]!] } : {}), @@ -2370,6 +2493,7 @@ export function augmentRoutedModelsWithMetadata( providerNames: string[], providers?: Record, caps?: Pick, + metadataModelIdCaseFoldByProvider?: ReadonlyMap, ): CatalogModel[] { const out = [...models]; const seen = new Set(out.map(m => `${m.provider}/${m.id}`)); @@ -2388,11 +2512,20 @@ export function augmentRoutedModelsWithMetadata( id: meta.id, owned_by: provider, ...(typeof meta.contextWindow === "number" && meta.contextWindow > 0 ? { contextWindow: meta.contextWindow } : {}), + ...(typeof meta.maxTokens === "number" && meta.maxTokens > 0 ? { maxOutputTokens: meta.maxTokens } : {}), ...(Array.isArray(meta.input) && meta.input.length > 0 ? { inputModalities: [...meta.input] } : {}), }; out.push({ ...model, - ...(providers?.[provider] ? applyProviderConfigHints(provider, providers[provider], model, contextCap) : {}), + ...(providers?.[provider] + ? applyProviderConfigHints( + provider, + providers[provider], + model, + contextCap, + metadataModelIdCaseFoldByProvider?.get(provider), + ) + : {}), }); } } diff --git a/src/codex/convergence-types.ts b/src/codex/convergence-types.ts index c5518a862d..5254285ab4 100644 --- a/src/codex/convergence-types.ts +++ b/src/codex/convergence-types.ts @@ -429,6 +429,7 @@ export interface CatalogTrustedOpenAiApiPolicySnapshot { readonly models?: readonly string[]; readonly modelContextWindows?: Readonly>; readonly modelMaxInputTokens?: Readonly>; + readonly virtualModels?: Readonly>>; readonly modelInputModalities?: Readonly>; readonly modelReasoningEfforts?: Readonly>; } diff --git a/src/server/index.ts b/src/server/index.ts index eab0ba1d7f..0e072634ce 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1354,7 +1354,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server 0 ? floored : undefined; + // Catalog limits are safe integers everywhere else; an unsafe finite value is a bad row. + return floored > 0 && Number.isSafeInteger(floored) ? floored : undefined; } export function modelCapabilityFields(input: ModelCapabilityInput): ModelCapabilityFields { const efforts = (input.reasoningEfforts ?? []).filter(effort => typeof effort === "string" && effort.length > 0); const contextLength = positiveInt(input.contextWindow); const longContextLength = positiveInt(input.longContextWindow); + const maxOutputTokens = positiveInt(input.maxOutputTokens); const hasLongTier = contextLength !== undefined && longContextLength !== undefined && longContextLength > contextLength; const modalities = Array.isArray(input.inputModalities) ? input.inputModalities.filter(modality => typeof modality === "string" && modality.length > 0) @@ -159,6 +163,7 @@ export function modelCapabilityFields(input: ModelCapabilityInput): ModelCapabil ...(hasLongTier ? { context_length: longContextLength } : contextLength !== undefined ? { context_length: contextLength } : {}), + ...(maxOutputTokens !== undefined ? { max_output_tokens: maxOutputTokens } : {}), // Once a gateway advertises api_types, Cursor keeps only rows whose output_modalities // include "text"; omitting the key drops the row from the extended catalog. output_modalities: ["text"], diff --git a/tests/codex-catalog.test.ts b/tests/codex-catalog.test.ts index 7b8015041e..993887c645 100644 --- a/tests/codex-catalog.test.ts +++ b/tests/codex-catalog.test.ts @@ -226,6 +226,20 @@ describe("combo catalog capability intersection", () => { }); }); + test("combo output ceiling is the smallest known member ceiling and stays unknown if any member is unknown", () => { + const known = deriveComboCatalogModel("known-output", normalizedCombo(), [ + { provider: "a", id: "m1", contextWindow: 128_000, maxOutputTokens: 64_000 }, + { provider: "b", id: "m2", contextWindow: 128_000, maxOutputTokens: 32_000 }, + ]); + expect(known?.maxOutputTokens).toBe(32_000); + + const partial = deriveComboCatalogModel("partial-output", normalizedCombo(), [ + { provider: "a", id: "m1", contextWindow: 128_000, maxOutputTokens: 64_000 }, + { provider: "b", id: "m2", contextWindow: 128_000 }, + ]); + expect(partial).not.toHaveProperty("maxOutputTokens"); + }); + test("handles vision, missing modalities, reasoning defaults, and parallel tools conservatively", () => { expect(deriveComboCatalogModel("vision", normalizedCombo({ defaultEffort: "low" }), [ memberA, @@ -1825,7 +1839,7 @@ describe("provider discovered model display names", () => { modelDisplayNames: { "grok-4.6": "Grok 4.6" }, }; - test("an exact provider model id receives only the configured display name", () => { + test("an exact provider model id receives the configured display name without losing catalog metadata", () => { const discovered = { provider: "xai", id: "grok-4.6", @@ -1848,10 +1862,40 @@ describe("provider discovered model display names", () => { const { displayName: _afterDisplayName, ...afterIdentity } = output; expect(output.displayName).toBe("Grok 4.6"); - expect(afterIdentity).toEqual({ ...beforeIdentity, supportsServiceTier: false }); + expect(afterIdentity).toEqual({ + ...beforeIdentity, + maxOutputTokens: 500_000, + supportsServiceTier: false, + }); expect(catalogModelSlug(output)).toBe("xai/grok-4.6"); }); + test("output ceilings prefer live metadata and only model-scoped config may narrow", () => { + const generated = applyProviderConfigHints("xai", { + ...provider, + defaultMaxOutputTokens: 1, + }, { provider: "xai", id: "grok-4.6" }); + expect(generated.maxOutputTokens).toBe(500_000); + + const narrowed = applyProviderConfigHints("xai", { + ...provider, + modelMaxOutputTokens: { "grok-4.6": 64_000 }, + }, { provider: "xai", id: "grok-4.6", maxOutputTokens: 128_000 }); + expect(narrowed.maxOutputTokens).toBe(64_000); + + const discoveredSmaller = applyProviderConfigHints("xai", { + ...provider, + modelMaxOutputTokens: { "grok-4.6": 64_000 }, + }, { provider: "xai", id: "grok-4.6", maxOutputTokens: 32_000 }); + expect(discoveredSmaller.maxOutputTokens).toBe(32_000); + + const defaultOnly = applyProviderConfigHints("unknown", { + ...provider, + defaultMaxOutputTokens: 1, + }, { provider: "unknown", id: "unknown-model" }); + expect(defaultOnly.maxOutputTokens).toBeUndefined(); + }); + test("display names use exact case-sensitive ids and stay provider scoped", () => { const wrongCase = applyProviderConfigHints("xai", provider, { provider: "xai", id: "GROK-4.6" }); const otherProvider = applyProviderConfigHints("other", { @@ -4715,6 +4759,8 @@ describe("Codex catalog routed normalization", () => { expect(slugs.has("deepseek/deepseek-v4-flash")).toBe(true); expect(slugs.has("deepseek/deepseek-v4-pro")).toBe(true); + expect(models.find(model => model.id === "deepseek-v4-flash")?.maxOutputTokens) + .toBe(384_000); for (const model of models) { expect(model.contextWindow).toBe(1_048_576); expect(model.inputModalities).toEqual(["text"]); @@ -5887,6 +5933,7 @@ describe("OpenAI API trusted catalog augmentation", () => { expect(rows.find(row => row.provider === "openai-apikey" && row.id === "gpt-5.6-sol")).toMatchObject({ contextWindow: 1_050_000, maxInputTokens: 922_000, + maxOutputTokens: 128_000, inputModalities: ["text", "image"], reasoningEfforts: ["low", "medium", "high", "xhigh", "max"], }); @@ -5920,6 +5967,7 @@ describe("OpenAI API trusted catalog augmentation", () => { expect(row).toMatchObject({ contextWindow: 1_050_000, maxInputTokens: 922_000, + maxOutputTokens: 128_000, inputModalities: ["text", "image"], reasoningEfforts: ["low", "medium", "high", "xhigh", "max"], }); @@ -6022,7 +6070,8 @@ describe("OpenAI API trusted catalog augmentation", () => { try { const equalDifferentOrder = { provider: "openai-apikey", id: "gpt-5.6-sol", contextWindow: 1_050_000, maxInputTokens: 922_000, - inputModalities: ["image", "text", "image"], reasoningEfforts: ["max", "low", "xhigh", "medium", "high", "low"], owned_by: "openai-apikey", + maxOutputTokens: 128_000, inputModalities: ["image", "text", "image"], + reasoningEfforts: ["max", "low", "xhigh", "medium", "high", "low"], owned_by: "openai-apikey", }; augmentRoutedModelsWithRegistryOpenAiApiRows([equalDifferentOrder], openAiApiCatalogConfig()); expect(warn).not.toHaveBeenCalled(); diff --git a/tests/cursor-local-models-schema.test.ts b/tests/cursor-local-models-schema.test.ts index 5b03ea576f..2b0c9759c0 100644 --- a/tests/cursor-local-models-schema.test.ts +++ b/tests/cursor-local-models-schema.test.ts @@ -43,6 +43,7 @@ function capabilityConfig(): OcxConfig { }, modelDefaultReasoningEfforts: { k3: "high" }, modelContextWindows: { k3: 200000 }, + modelMaxOutputTokens: { k3: 64_000 }, modelInputModalities: { k3: ["text", "image"] }, }, openai: { @@ -103,12 +104,25 @@ describe("modelCapabilityFields", () => { const tiered = modelCapabilityFields({ contextWindow: 272000, longContextWindow: 922000 }); expect(tiered.capabilities.context_length).toBe(922000); expect(tiered.pricing).toEqual({ overrides: [{ min_prompt_tokens: 272000 }] }); + expect("long_context_threshold_tokens" in tiered).toBe(false); // Equal or smaller opt-in window: plain context_length, no pricing block. const flat = modelCapabilityFields({ contextWindow: 272000, longContextWindow: 272000 }); expect(flat.capabilities.context_length).toBe(272000); expect("pricing" in flat).toBe(false); + expect("long_context_threshold_tokens" in flat).toBe(false); expect("pricing" in modelCapabilityFields({ longContextWindow: 922000 })).toBe(false); }); + + test("max output tokens are sanitized independently of reasoning", () => { + expect(modelCapabilityFields({ maxOutputTokens: 128000 }).capabilities.max_output_tokens) + .toBe(128000); + expect("max_output_tokens" in modelCapabilityFields({ maxOutputTokens: 0 }).capabilities) + .toBe(false); + expect("max_output_tokens" in modelCapabilityFields({ maxOutputTokens: Number.MAX_SAFE_INTEGER + 2 }).capabilities) + .toBe(false); + expect(modelCapabilityFields({ maxOutputTokens: 1.9 }).capabilities.supports_reasoning) + .toBe(false); + }); }); describe("nativeOpenAiContextTier", () => { @@ -142,6 +156,7 @@ describe("raw /v1/models list advertises Cursor local-agent capabilities", () => expect(k3!.api_types).toEqual(["chat_completions", "responses", "anthropic_messages"]); expect(k3!.capabilities).toEqual({ context_length: 200000, + max_output_tokens: 64_000, output_modalities: ["text"], input_modalities: ["text", "image"], supports_tool_use: true, @@ -160,6 +175,7 @@ describe("raw /v1/models list advertises Cursor local-agent capabilities", () => const plainCaps = plain!.capabilities as Record; expect(plainCaps.supports_reasoning).toBe(false); expect("reasoning_effort" in plainCaps).toBe(false); + expect("max_output_tokens" in plainCaps).toBe(false); const sol = body.data.find(m => m.id === "gpt-5.6-sol"); expect(sol).toBeDefined(); @@ -169,7 +185,9 @@ describe("raw /v1/models list advertises Cursor local-agent capabilities", () => expect(solCaps.reasoning_effort).toEqual(nativeReasoningEfforts("gpt-5.6-sol")); // Native GPT-5.6: 272k default window, 922k opt-in ceiling → Cursor Context selector. expect(solCaps.context_length).toBe(922000); + expect(solCaps.max_output_tokens).toBe(128_000); expect(sol!.pricing).toEqual({ overrides: [{ min_prompt_tokens: 272000 }] }); + expect("long_context_threshold_tokens" in sol!).toBe(false); expect(solCaps.supports_vision).toBe(true); // Routed rows have no separate opt-in tier, so no pricing block. expect("pricing" in k3!).toBe(false); diff --git a/tests/grok-models-effort-list.test.ts b/tests/grok-models-effort-list.test.ts index 863bb8c754..c64964cef7 100644 --- a/tests/grok-models-effort-list.test.ts +++ b/tests/grok-models-effort-list.test.ts @@ -123,6 +123,9 @@ describe("raw /v1/models list reasoning-effort advertisement (Grok Build discove expect("supports_reasoning_effort" in plain!).toBe(false); expect("reasoning_effort" in plain!).toBe(false); expect("reasoning_efforts" in plain!).toBe(false); + const capabilities = plain!.capabilities as Record; + expect(capabilities.supports_reasoning).toBe(false); + expect("reasoning_effort" in capabilities).toBe(false); } finally { await server.stop(true); } diff --git a/tests/provider-model-discovery-contract.test.ts b/tests/provider-model-discovery-contract.test.ts index f4a088b730..a78977d161 100644 --- a/tests/provider-model-discovery-contract.test.ts +++ b/tests/provider-model-discovery-contract.test.ts @@ -246,16 +246,23 @@ describe("registry-owned provider model discovery", () => { }); test("accepts only positive safe-integer token limits from live metadata", () => { + expect(catalogHintsFromModelsApiItem("example", { + id: "valid-output", + capabilities: { max_output_tokens: 8192 }, + })).toEqual({ maxOutputTokens: 8192 }); + expect(catalogHintsFromModelsApiItem("example", { id: "fractional", context_size: 1_000, max_input_tokens: 0.5, + capabilities: { max_output_tokens: 0.5 }, })).toEqual({ contextWindow: 1_000 }); expect(catalogHintsFromModelsApiItem("example", { id: "unsafe", context_size: Number.MAX_SAFE_INTEGER + 1, max_input_tokens: Number.MAX_SAFE_INTEGER + 1, + max_output_tokens: Number.MAX_SAFE_INTEGER + 1, })).toEqual({}); }); diff --git a/tests/server-combo-failover-e2e.test.ts b/tests/server-combo-failover-e2e.test.ts index ce6fbacc77..40b60a7807 100644 --- a/tests/server-combo-failover-e2e.test.ts +++ b/tests/server-combo-failover-e2e.test.ts @@ -844,6 +844,7 @@ describe("server combo failover 030 activation matrix", () => { liveModels: false, models: ["deepseek-chat"], modelContextWindows: { "deepseek-chat": 128_000 }, + modelMaxOutputTokens: { "deepseek-chat": 64_000 }, }), }, combo.targets, { alias: combo.alias }); saveConfig(config); @@ -853,7 +854,12 @@ describe("server combo failover 030 activation matrix", () => { const response = await fetch(new URL("/v1/models", server.url)); expect(response.status).toBe(200); const payload = await response.json() as { - data: Array<{ id: string; owned_by: string; is_combo?: boolean }>; + data: Array<{ + id: string; + owned_by: string; + is_combo?: boolean; + capabilities?: { max_output_tokens?: number }; + }>; }; return payload.data; }; @@ -868,6 +874,7 @@ describe("server combo failover 030 activation matrix", () => { const initialRows = (await publicRows()).filter(model => model.id === selector); expect(initialRows).toHaveLength(1); expect(initialRows[0]).toMatchObject({ id: selector, object: "model", created: 0, owned_by: "openai", is_combo: true }); + expect(initialRows[0]!.capabilities?.max_output_tokens).toBe(64_000); const renamed = await updateAlias("fast-chat"); expect(renamed.status).toBe(200); @@ -875,6 +882,7 @@ describe("server combo failover 030 activation matrix", () => { const renamedSelectorRows = renamedRows.filter(model => model.id === selector); expect(renamedSelectorRows).toHaveLength(1); expect(renamedSelectorRows[0]).toMatchObject({ id: selector, object: "model", created: 0, owned_by: "deepseek" }); + expect(renamedSelectorRows[0]!.capabilities?.max_output_tokens).toBe(64_000); expect(renamedSelectorRows[0].is_combo).toBeUndefined(); const renamedAliasRows = renamedRows.filter(model => model.id === "fast-chat"); expect(renamedAliasRows).toHaveLength(1); @@ -888,6 +896,7 @@ describe("server combo failover 030 activation matrix", () => { const deletedSelectorRows = deletedRows.filter(model => model.id === selector); expect(deletedSelectorRows).toHaveLength(1); expect(deletedSelectorRows[0]).toMatchObject({ id: selector, object: "model", created: 0, owned_by: "deepseek" }); + expect(deletedSelectorRows[0]!.capabilities?.max_output_tokens).toBe(64_000); expect(deletedSelectorRows[0].is_combo).toBeUndefined(); expect(deletedRows.some(model => model.is_combo === true)).toBe(false); } finally {