import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases"; import { initialModelSelectionPending } from "../../providers/initial-model-selection"; import { execFileSync } from "node:child_process"; import { createHash, createHmac, randomBytes } from "node:crypto"; import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs"; import { delimiter, dirname, join, resolve } from "node:path"; import { atomicWriteFile, expandUserPath, getConfigDir, websocketsEnabled } from "../../config"; import { resolveProviderApiKey } from "../../providers/key-store"; import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readRootTomlString, resolveCodexConfigPath } from "../paths"; import { clearModelCache, clearProviderDiscoveryStatus, captureModelCacheGeneration, DEFAULT_MODEL_CACHE_TTL_MS, getFreshCached, getStaleCached, isModelsFetchCoolingDown, isModelCacheGenerationCurrent, markModelsFetchFailure, markProviderDiscoveryFailed, markProviderDiscoveryOk, shouldLogDiscoveryFailure, setCached, type ProviderModelDiscoveryFailure, } from "../model-cache"; import { buildModelsRequest, getValidAccessTokenSnapshot, observeActiveOAuthAccessToken, resolveModelsAuthToken, type OAuthActiveTokenObservation, } from "../../oauth"; import type { OcxConfig, OcxProviderConfig } from "../../types"; import { modelInList } from "../../types"; import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort"; import { isModelVisionSidecarConsumer } from "../../vision/eligibility"; import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider, type ModelMetadata } from "../../generated/model-metadata"; import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive"; import { captureFastPolicyAuthority, fastPolicyForModel, serviceTierSupportFromPolicy, } from "../../providers/service-tier"; import type { FastPolicyAuthority } from "../../providers/fastwire"; import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../../providers/registry"; import { parseAntigravityAvailableModels, registerAntigravityDiscoveredWireModels } from "../../providers/antigravity-models"; import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap"; import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget"; import { effectiveModelAliases } from "../../providers/default-aliases"; import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec"; import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity"; import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery"; import { fetchCursorUsableModels } from "../../adapters/cursor/live-models"; import { recordLiveCursorClaudeModels, recordLiveCursorMaxModeModels } from "../../adapters/cursor/catalog"; import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers"; import { COMBO_NAMESPACE, comboModelId, getCombo, listComboIds, targetKey, } from "../../combos"; import type { NormalizedComboConfig } from "../../combos/types"; import { ProviderOutboundPolicyError, providerOutboundGet, providerOutboundPost, providerRedirectError, } from "../../lib/provider-outbound"; import { redactSecretString } from "../../lib/redact"; import { extractProviderModelItems, isRegistryModelDiscoveryUrl, readBoundedDiscoveryJson, resolveProviderModelDiscovery, type ModelDiscoveryResponseFailure, type ProviderModelsApiItem, type ResolvedProviderModelDiscovery, } from "../../providers/model-discovery"; import { applyConfiguredHeadersLast, fetchOllamaShowEnrichment, ollamaShowEnrichable } from "../../providers/ollama-show"; import upstreamModelsSnapshot from "../data/upstream-models.json"; import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } from "../../lib/admission"; import { CODEX_CUSTOM_MODEL_CATALOG_KIND, JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing"; import type { CatalogModel } from "./parsing"; import { disabledNativeSlugs, hasComboTargets, hasNativeOpenAiCapabilityMetadata, NATIVE_GPT56_MAX_INPUT_TOKENS, nativeContextLimits, nativeOpenAiCapabilityDisplayName, 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"; import type { CatalogAdmissionSnapshot, CatalogDiscoveryPolicyField, CatalogGatherAuthorityIdentity, CatalogProviderDiscoveryPolicySnapshot, CatalogProcessLocalEvidence, CatalogSourceEvidence, CatalogTrustedOpenAiApiPolicySnapshot, } from "../convergence-types"; export type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence"; /** Concurrent gatherRoutedModels callers with the same catalog identity share one live discovery. * Keyed by gatherFlightKey so a different config cannot join or evict the wrong flight. */ export interface CatalogGatherProviderAuthOutcome { readonly provider: string; readonly state: OAuthActiveTokenObservation["kind"]; } export interface CatalogGatherProviderModelOutcome { readonly provider: string; readonly state: "authoritative" | "degraded"; } export interface GatherRoutedModelsOptions { comboOmissions?: ComboCatalogOmission[]; providerAuthOutcomes?: CatalogGatherProviderAuthOutcome[]; /** Flight-local authority of each provider's returned model rows. */ providerModelOutcomes?: CatalogGatherProviderModelOutcome[]; /** Internal convergence sink for the immutable policy that produced the returned rows. */ discoveryPolicySnapshots?: CatalogProviderDiscoveryPolicySnapshot[]; } interface GatherFlightResult { models: CatalogModel[]; comboOmissions: ComboCatalogOmission[]; providerAuthOutcomes: readonly CatalogGatherProviderAuthOutcome[]; providerModelOutcomes: readonly CatalogGatherProviderModelOutcome[]; discoveryPolicySnapshots: readonly CatalogProviderDiscoveryPolicySnapshot[]; } interface ProviderModelsResult { readonly models: CatalogModel[]; readonly outcome: CatalogGatherProviderModelOutcome; } interface ModelsAuthResolution { readonly apiKey: string | undefined; readonly observed: boolean; readonly oauthApiBaseUrl?: string; readonly oauthProjectId?: string; } type ModelsAuthResolver = | { readonly kind: "refreshing" } | { readonly kind: "observed"; readonly resolve: (name: string, provider: OcxProviderConfig) => ModelsAuthResolution; }; type ModelsAuthResolverFactory = ( outcomes: CatalogGatherProviderAuthOutcome[], ) => ModelsAuthResolver; interface CapturedModelsRequest { readonly method: "GET" | "POST"; readonly url: string; readonly headersWithoutCredential: Readonly>; readonly headersWithCredential: Readonly>; } interface CapturedProviderGather { readonly name: string; readonly provider: OcxProviderConfig; readonly discovery: ResolvedProviderModelDiscovery; readonly policy: CatalogProviderDiscoveryPolicySnapshot; readonly request: CapturedModelsRequest; readonly fastPolicyAuthority: FastPolicyAuthority; readonly metadataModelIdCaseFold: boolean; readonly effectiveAlias?: string | null; readonly observedAuth?: ModelsAuthResolution; /** * Configured model ids this provider must keep even when live discovery omits * them — combo targets that are also listed in providers.*.models (OCX-111). * Combo-only ids (not in models[]) stay out of the public catalog and are * synthesized for combo derivation instead (#1305). */ readonly retainConfiguredModelIds?: ReadonlySet; } interface GatherFlightCapture { readonly discoveryPolicyIdentity: string; readonly authIdentity: string; readonly providerGraphIdentity: string; readonly discoveryPolicySnapshots: readonly CatalogProviderDiscoveryPolicySnapshot[]; readonly providers: readonly CapturedProviderGather[]; readonly authResolver: ModelsAuthResolver; readonly providerAuthOutcomes: readonly CatalogGatherProviderAuthOutcome[]; readonly openAiApiPolicy: CatalogTrustedOpenAiApiPolicySnapshot; } interface GatherInflightEntry { readonly discoveryPolicyIdentity: string; /** * The credential half of the join decision. * * `gatherFlightKey`'s fingerprint carries endpoints and model lists but no * `authMode`, key or headers, and discovery policy does not carry them either. * Two admissions differing ONLY in credential therefore produced the same key * and the same policy, so the second joined the first and published rows the * old key had fetched — reproduced against the real routes by rotating a key * through `/api/providers/keys` mid-flight. * * Now REDUNDANT with `providerGraphIdentity`, which hashes the whole provider * row and therefore covers `apiKey` too: removing this term alone leaves the * credential regression green. It is kept deliberately, for two reasons. It * covers what the graph cannot — the RESOLVED auth (`observedAuth`) and the * final materialized headers, which are derived rather than stored, so an * OAuth token that changes while the row is byte-identical still separates * admissions. And it states the credential rule where a reader looks for it, * instead of leaving it as an emergent property of hashing everything. */ readonly authIdentity: string; /** * The whole admitted provider graph, not a chosen subset. * * `providerCatalogFingerprint` is an ALLOW-LIST, so every field it forgot was * silently treated as equivalence: credentials leaked a flight until * `authIdentity` landed, and `reasoningEfforts` leaked one after that — both * reproduced against real routes. Enumerating fields cannot converge, because * the next field added to a provider row inherits the same defect. This * identity therefore covers the enriched, frozen provider objects the flight * actually gathered from, so a join is refused unless the admissions agree on * everything rather than on everything somebody remembered to list. */ readonly providerGraphIdentity: string; readonly promise: Promise; } function withCanonicalOpenAiForwardAuthDefault( name: string, provider: OcxProviderConfig, ): OcxProviderConfig { if (name !== OPENAI_CODEX_PROVIDER_ID || provider.authMode !== undefined) return provider; const candidate = { ...provider, authMode: "forward" as const }; return isCanonicalOpenAiForwardProvider(candidate) ? candidate : provider; } const gatherInflight = new Map(); const CATALOG_GATHER_AUTHORITY_KEY = randomBytes(32); const REQUEST_CREDENTIAL_SENTINEL = `ocx-catalog-credential-${randomBytes(16).toString("hex")}`; const MAX_CONCURRENT_CATALOG_GATHERS = 8; const gatherGate = createAdmissionGate("catalog_gathers", MAX_CONCURRENT_CATALOG_GATHERS); export class CatalogGatherBusyError extends ResourceAdmissionError { override readonly code = "catalog_busy"; readonly retryAfterSeconds = 1; constructor() { super("catalog_gathers", MAX_CONCURRENT_CATALOG_GATHERS); this.name = "CatalogGatherBusyError"; } } export function catalogGatherAdmissionMetrics(): AdmissionMetrics { return gatherGate.metrics(); } function stableJson(value: unknown): string { return JSON.stringify(value, (_key, nested) => { if (nested && typeof nested === "object" && !Array.isArray(nested)) { return Object.fromEntries(Object.entries(nested as Record).sort(([a], [b]) => a.localeCompare(b))); } return nested; }); } function framed(value: string): string { return `${Buffer.byteLength(value, "utf8")}:${value}`; } function canonicalAuthorityEncoding(value: unknown): string { if (value === null) return "null"; if (value === undefined) return "undefined"; if (typeof value === "string") return `string${framed(value)}`; if (typeof value === "boolean") return value ? "boolean1" : "boolean0"; if (typeof value === "number") { if (!Number.isFinite(value)) throw new TypeError("Catalog authority cannot encode a non-finite number."); const encoded = Object.is(value, -0) ? "-0" : String(value); return `number${framed(encoded)}`; } if (Array.isArray(value)) { return `array${value.length}:${value.map(item => framed(canonicalAuthorityEncoding(item))).join("")}`; } if (typeof value === "object") { const record = value as Record; const keys = Object.keys(record).sort((left, right) => left.localeCompare(right)); return `object${keys.length}:${keys.map(key => ( `${framed(key)}${framed(canonicalAuthorityEncoding(record[key]))}` )).join("")}`; } throw new TypeError(`Catalog authority cannot encode ${typeof value}.`); } function keyedGatherIdentity(domain: string, value: unknown): string { return createHmac("sha256", CATALOG_GATHER_AUTHORITY_KEY) .update(framed(domain)) .update(framed(canonicalAuthorityEncoding(value))) .digest("hex"); } function keyedGatherBytesIdentity(domain: string, value: Uint8Array): string { return createHmac("sha256", CATALOG_GATHER_AUTHORITY_KEY) .update(framed(domain)) .update(`${value.byteLength}:`) .update(value) .digest("hex"); } export function createCatalogGatherAuthorityIdentity( snapshot: CatalogAdmissionSnapshot, sourceEvidence: CatalogSourceEvidence, processLocal: CatalogProcessLocalEvidence, discoveryPolicies: readonly CatalogProviderDiscoveryPolicySnapshot[], ): CatalogGatherAuthorityIdentity { const sourceEvidenceIdentity = keyedGatherIdentity("catalog-source-evidence-v1", sourceEvidence); const processLocalEvidenceIdentity = keyedGatherIdentity("catalog-process-local-v1", processLocal); const discoveryPolicyIdentity = keyedGatherIdentity("catalog-discovery-policy-v1", discoveryPolicies); return Object.freeze({ version: 1 as const, authorityId: keyedGatherIdentity("catalog-authority-v1", { admittedConfig: snapshot.configIdentity, discoveryPolicyIdentity, sourceEvidenceIdentity, processLocalEvidenceIdentity, }), admittedConfig: Object.freeze({ ...snapshot.configIdentity, generation: Object.freeze({ ...snapshot.configIdentity.generation }), }), authSnapshotIdentity: keyedGatherIdentity( "catalog-auth-v1", sourceEvidence.conditional["provider-auth-selection"], ), discoveryPolicyIdentity, nativeCatalogSourceIdentity: keyedGatherIdentity( "catalog-native-v1", sourceEvidence.conditional["native-catalog-selection"], ), sourceEvidenceIdentity, processLocalEvidenceIdentity, }); } function detachedClone(value: T): T { if (Array.isArray(value)) return value.map(item => detachedClone(item)) as T; if (value && typeof value === "object") { const clone: Record = {}; for (const key of Object.keys(value)) { clone[key] = detachedClone((value as Record)[key]); } return clone as T; } return value; } function recursivelyFreeze(value: T): T { if (!value || typeof value !== "object" || Object.isFrozen(value)) return value; for (const nested of Object.values(value as Record)) recursivelyFreeze(nested); return Object.freeze(value); } function detachedFrozen(value: T): T { return recursivelyFreeze(detachedClone(value)); } function capturedField( value: T | undefined, key: K, ): CatalogDiscoveryPolicyField { if (!value || !Object.hasOwn(value, key)) return Object.freeze({ state: "absent" }); return detachedFrozen({ state: "present" as const, value: value[key] }); } function captureTrustedOpenAiApiPolicy( name: string, registryTransportMatch: boolean, ): CatalogTrustedOpenAiApiPolicySnapshot { if (name !== OPENAI_API_PROVIDER_ID) return Object.freeze({ state: "unused" }); if (!registryTransportMatch) return Object.freeze({ state: "transport-mismatch" }); const entry = getProviderRegistryEntry(name); if (!entry?.models) return Object.freeze({ state: "registry-models-absent" }); return detachedFrozen({ state: "captured" as const, models: entry.models, ...(entry.modelContextWindows ? { modelContextWindows: entry.modelContextWindows } : {}), ...(entry.modelMaxInputTokens ? { modelMaxInputTokens: entry.modelMaxInputTokens } : {}), ...(entry.modelMaxOutputTokens ? { modelMaxOutputTokens: entry.modelMaxOutputTokens } : {}), ...(entry.virtualModels ? { virtualModels: entry.virtualModels } : {}), ...(entry.modelInputModalities ? { modelInputModalities: entry.modelInputModalities } : {}), ...(entry.modelReasoningEfforts ? { modelReasoningEfforts: entry.modelReasoningEfforts } : {}), }); } function captureModelsRequest( name: string, provider: OcxProviderConfig, observedAuth: ModelsAuthResolution | undefined, ): CapturedModelsRequest { const observed = observedAuth ? { oauthApiBaseUrl: observedAuth.oauthApiBaseUrl } : undefined; const withoutCredential = buildModelsRequest(provider, undefined, name, observed); const withCredential = buildModelsRequest(provider, REQUEST_CREDENTIAL_SENTINEL, name, observed); const method = withoutCredential.method ?? "GET"; if (withoutCredential.url !== withCredential.url || method !== (withCredential.method ?? "GET")) { throw new TypeError(`Provider model discovery URL for ${name} depends on credential bytes.`); } return detachedFrozen({ method, url: withoutCredential.url, headersWithoutCredential: withoutCredential.headers, headersWithCredential: withCredential.headers, }); } function captureProviderGather( name: string, configured: OcxProviderConfig, authResolver: ModelsAuthResolver, retainConfiguredModelIds?: ReadonlySet, config?: Pick, ): CapturedProviderGather { const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured)); enrichProviderFromRegistry(name, enriched); const registryTransportMatch = providerMatchesRegistryTransport(name, enriched); const provider = recursivelyFreeze(enriched); const fastPolicyAuthority = captureFastPolicyAuthority( name, provider, registryTransportMatch, configured, ); const metadataModelIdCaseFold = shouldCaseFoldMetadataModelId(name); const observedAuth = authResolver.kind === "observed" && provider.authMode !== "forward" && provider.liveModels !== false ? authResolver.resolve(name, provider) : undefined; const request = captureModelsRequest(name, provider, observedAuth); const resolved = resolveProviderModelDiscovery(name, provider); const discovery = detachedFrozen({ ...(resolved.spec ? { spec: resolved.spec } : {}), maxResponseBytes: resolved.maxResponseBytes, maxModels: resolved.maxModels, }); const trustedOpenAiApi = captureTrustedOpenAiApiPolicy(name, registryTransportMatch); const policy = detachedFrozen({ provider: name, registryTransportMatch, location: { spec: discovery.spec ? "present" as const : "absent" as const, url: capturedField(discovery.spec, "url"), path: capturedField(discovery.spec, "path"), query: capturedField(discovery.spec, "query"), }, finalMethod: request.method, finalUrl: request.url, filter: capturedField(discovery.spec, "filter"), maxResponseBytes: discovery.maxResponseBytes, maxModels: discovery.maxModels, trustedOpenAiApi, }); const effectiveAlias = effectiveProviderAliasDecision(name, configured, config); return Object.freeze({ name, provider, discovery, policy, request, fastPolicyAuthority, metadataModelIdCaseFold, effectiveAlias, ...(observedAuth ? { observedAuth: Object.freeze({ ...observedAuth }) } : {}), ...(retainConfiguredModelIds && retainConfiguredModelIds.size > 0 ? { retainConfiguredModelIds } : {}), }); } /** Model ids each provider must retain for combo catalog derivation (OCX-111). */ export function configuredComboTargetModelsByProvider( config: Pick, ): Map> { const byProvider = new Map>(); for (const id of listComboIds(config)) { const combo = getCombo(config, id); if (!combo) continue; for (const target of combo.targets) { let models = byProvider.get(target.provider); if (!models) { models = new Set(); byProvider.set(target.provider, models); } models.add(target.model); } } return byProvider; } function captureGatherFlight( config: OcxConfig, createAuthResolver: ModelsAuthResolverFactory, ): GatherFlightCapture { const providerAuthOutcomes: CatalogGatherProviderAuthOutcome[] = []; const authResolver = createAuthResolver(providerAuthOutcomes); const comboTargetsByProvider = configuredComboTargetModelsByProvider(config); const providers = Object.entries(config.providers) .filter(([, provider]) => provider.disabled !== true) .map(([name, provider]) => captureProviderGather( name, provider, authResolver, comboTargetsByProvider.get(name), config, )); const discoveryPolicySnapshots = Object.freeze(providers.map(provider => provider.policy)); return Object.freeze({ discoveryPolicyIdentity: keyedGatherIdentity("catalog-discovery-policy-v1", discoveryPolicySnapshots), // Credentials are hashed under the same unexported per-process key, never // stored or compared in the clear: this value can reach a map key and must // not disclose a token. The final headers are included because a static // header can carry authority just as an `apiKey` can. authIdentity: keyedGatherIdentity("catalog-gather-auth-v1", providers.map(provider => ({ name: provider.name, authMode: provider.provider.authMode ?? null, liveModels: provider.provider.liveModels ?? null, credential: provider.provider.apiKey ?? null, observedAuth: provider.observedAuth ?? null, headers: provider.request.headersWithCredential, url: provider.request.url, }))), // Every enriched provider row the flight will gather from, in admission order. // Anything that can change a catalog row lives in here by construction. providerGraphIdentity: keyedGatherIdentity("catalog-gather-provider-graph-v1", providers.map(provider => ({ name: provider.name, // `fetch` is a caller-owned transport executor, not admitted state: the // outbound transport honors it so a caller can supply its own HTTP path. // It is the one member of a provider row that is legitimately a function, // so it is dropped here rather than allowed to break every encode. provider: omitProviderTransportExecutor(provider.provider), fastPolicyAuthority: provider.fastPolicyAuthority, // Combo retention is capture-time state, not a provider-row field. Two // gathers that share providers but differ in combo targets must not join. retainConfiguredModelIds: [...(provider.retainConfiguredModelIds ?? [])].sort(), }))), discoveryPolicySnapshots, providers: Object.freeze(providers), authResolver, providerAuthOutcomes: Object.freeze([...providerAuthOutcomes]), openAiApiPolicy: providers.find(provider => provider.name === OPENAI_API_PROVIDER_ID)?.policy.trustedOpenAiApi ?? Object.freeze({ state: "unused" as const }), }); } /** * Drop the caller-owned transport executor before hashing a provider row. * * Fails closed on anything ELSE that cannot be encoded: the point of hashing the * whole row is that no field escapes the comparison, so a second function member * must surface as an encode error rather than being quietly skipped here. */ function omitProviderTransportExecutor(provider: OcxProviderConfig): Record { const entries = Object.entries(provider).filter(([key]) => key !== "fetch"); return Object.fromEntries(entries); } function materializeCapturedHeaders( request: CapturedModelsRequest, apiKey: string | undefined, ): Record { const source = apiKey ? request.headersWithCredential : request.headersWithoutCredential; return Object.fromEntries(Object.entries(source).map(([name, value]) => [ name, apiKey ? value.split(REQUEST_CREDENTIAL_SENTINEL).join(apiKey) : value, ])); } function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Record { return { n: name, // Preserve the persisted tri-state. Registry enrichment may turn an omitted value into // `false` while an explicit `true` stays live, so those callers must not share a flight. live: prov.liveModels ?? null, base: prov.baseUrl ?? "", adapter: prov.adapter ?? "", models: [...(prov.models ?? [])].sort(), retain: [...(prov.retainModels ?? [])].sort(), selected: [...(prov.selectedModels ?? [])].sort(), displayNames: prov.modelDisplayNames ?? null, defaultModel: prov.defaultModel ?? null, 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, defRe: prov.modelDefaultReasoningEfforts ?? null, rsSum: prov.modelSupportsReasoningSummaries ?? null, verbosity: prov.modelSupportsVerbosity ?? null, rsDel: prov.modelReasoningSummaryDelivery ?? null, serviceTier: prov.modelSupportsServiceTier ?? null, noVis: [...(prov.noVisionModels ?? [])].sort(), ptc: prov.parallelToolCalls ?? null, gMode: prov.googleMode ?? null, }; } function gatherFlightKey(config: OcxConfig): string { const providers = Object.entries(config.providers) .filter(([, prov]) => prov.disabled !== true) .map(([name, prov]) => providerCatalogFingerprint(name, prov)) .sort((a, b) => String(a.n).localeCompare(String(b.n))); const assembly = stableJson({ providers, disabledModels: [...(config.disabledModels ?? [])].sort(), combos: config.combos ?? {}, customModels: (config.customModels ?? []).map((cm) => ({ p: cm.provider, m: cm.modelId, d: cm.displayName ?? null, cw: cm.contextWindow ?? null, im: cm.inputModalities ?? null, })), caps: config.providerContextCaps ?? null, }); const digest = createHash("sha256").update(assembly).digest("hex").slice(0, 16); return `${digest}#${config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS}`; } /** Drop in-flight gather so tests / full cache clears do not reuse a stale promise. */ export function clearGatherRoutedModelsInflight(): void { gatherInflight.clear(); } const NUMERIC_MODEL_ID_SEGMENT = /^\d+$/; /** * Resolve an unknown Claude point release or date pin from the nearest configured * family row. Only numeric tail segments are removed so unrelated model families * cannot inherit one another's limits. */ function anthropicFamilyContextWindow( record: Record | undefined, id: string, ): number | undefined { if (!record || !id.toLowerCase().startsWith("claude-")) return undefined; let candidate = id; while (true) { const cut = candidate.lastIndexOf("-"); if (cut <= 0 || !NUMERIC_MODEL_ID_SEGMENT.test(candidate.slice(cut + 1))) return undefined; candidate = candidate.slice(0, cut); const value = modelRecordValue(record, candidate); if (typeof value === "number" && value > 0) return value; } } /** * Resolve the configured context window in exact-model, Anthropic numeric-family, * then provider-wide order. Return undefined when the selected value is not positive. */ export function configuredContextWindow(prov: OcxProviderConfig, id: string): number | undefined { const configured = modelRecordValue(prov.modelContextWindows, id) ?? (prov.adapter === "anthropic" ? anthropicFamilyContextWindow(prov.modelContextWindows, id) : undefined) ?? prov.contextWindow; return typeof configured === "number" && configured > 0 ? configured : undefined; } export function configuredInputModalities(prov: OcxProviderConfig, id: string): string[] | undefined { const modalities = modelRecordValue(prov.modelInputModalities, id); return Array.isArray(modalities) && modalities.length > 0 ? [...modalities] : undefined; } /** Exact display-only override for one provider-native model id. */ export function configuredModelDisplayName( prov: OcxProviderConfig, id: string, ): string | undefined { if (!prov.modelDisplayNames || !Object.hasOwn(prov.modelDisplayNames, id)) return undefined; const value = prov.modelDisplayNames[id]; return typeof value === "string" && value.trim() ? value.trim() : undefined; } export function configuredMaxInputTokens(prov: OcxProviderConfig, id: string): number | undefined { const configured = modelRecordValue(prov.modelMaxInputTokens, id); 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, ): number | undefined { if (!prov) return undefined; const configured = modelRecordValue(prov.modelAutoCompactTokenLimits, id); return typeof configured === "number" && Number.isSafeInteger(configured) && configured > 0 ? configured : undefined; } function configuredReasoningSummarySupport(prov: OcxProviderConfig | undefined, id: string): boolean | undefined { if (!prov) return undefined; const explicit = modelRecordValue(prov.modelSupportsReasoningSummaries, id); if (explicit !== undefined) return explicit; return modelRecordValue(prov.modelReasoningSummaryDelivery, id) !== undefined ? true : undefined; } function configuredVerbositySupport(name: string, prov: OcxProviderConfig | undefined, id: string): boolean | undefined { const explicit = prov ? modelRecordValue(prov.modelSupportsVerbosity, id) : undefined; if (explicit !== undefined) return explicit; if (!prov) return undefined; void name; // Provider-wide fallback for ids the per-model map does not enumerate — a live-discovered // model would otherwise re-advertise a control the upstream accepts and ignores. // // Read from the PROVIDER CONFIG, never from PROVIDER_REGISTRY. A gather flight captures its // registry authority up front and forbids any later registry read, so consulting the registry // here made a custom-destination flight fall back to "configured" instead of serving its own // discovery result (tests/codex-integration/codex-gather-authority.test.ts). `applyVerbosityDefaults` in // providers/derive.ts materializes the registry default into the config at seed/enrich time. return prov.supportsVerbosity; } export function applyProviderConfigHints( name: string, prov: OcxProviderConfig, model: CatalogModel, providerCap?: number, metadataModelIdCaseFold?: boolean, effectiveAlias?: string | null, ): CatalogModel { const displayName = configuredModelDisplayName(prov, model.id); // The alias decision is resolved once at flight admission (captureProviderGather) and threaded // through as `effectiveAlias`. Re-deriving it here would read PROVIDER_REGISTRY after admission, // which is exactly the authority leak tests/codex-integration/codex-gather-authority.test.ts // forbids: a flight must not consult the live registry once its transport has been captured. // When no decision was threaded in, carry whatever the row already resolved to instead. const providerAlias = typeof effectiveAlias === "string" || effectiveAlias === null ? effectiveAlias : model.providerAlias; 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 // planning aligned. The catalog must still advertise image input — the Codex app // gates attachments client-side on input_modalities, and a text-only entry would block images // before the sidecar ever runs ("This model does not support image inputs"). Discovery-derived // text-only rows stay untouched: the runtime predicate only reads these two config sources, so // it would not convert those. const sidecarCovered = isModelVisionSidecarConsumer(prov, model.id); if (sidecarCovered) { const base = inputModalities ?? model.inputModalities ?? ["text"]; inputModalities = base.includes("image") ? [...base] : [...base, "image"]; } const reasoningEfforts = configuredReasoningEfforts(prov, model.id); const defaultReasoningEffort = modelRecordValue(prov.modelDefaultReasoningEfforts, model.id) ?? model.defaultReasoningEffort; const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id); const supportsVerbosity = configuredVerbositySupport(name, prov, model.id); const fastPolicy = fastPolicyForModel(prov, model.id, name); const supportsServiceTier = serviceTierSupportFromPolicy(fastPolicy); const { supportsServiceTier: _staleServiceTier, fastTierDescription: _staleFastTierDescription, providerAlias: _staleProviderAlias, ...modelWithoutServiceTier } = model; // 已发现窗口只允许被配置值压低;缺窗口时,已开的 Context cap 就是实际窗口。 const discoveredWindow = typeof model.contextWindow === "number" && model.contextWindow > 0 ? model.contextWindow : undefined; const hintedWindow = discoveredWindow !== undefined ? (configuredCap !== undefined ? Math.min(discoveredWindow, configuredCap) : discoveredWindow) : (configuredCap ?? (providerCap !== undefined ? resolveUnknownRoutedContextWindow(providerCap) : undefined)); const hinted = { ...modelWithoutServiceTier, ...(displayName !== undefined ? { displayName } : {}), ...(providerAlias !== undefined ? { providerAlias } : {}), ...(hintedWindow !== undefined ? { contextWindow: hintedWindow } : {}), ...(inputModalities ? { inputModalities } : {}), ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}), ...(configuredMaxInput !== undefined ? { maxInputTokens: typeof model.maxInputTokens === "number" && model.maxInputTokens > 0 ? Math.min(model.maxInputTokens, configuredMaxInput) : configuredMaxInput, } : {}), ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}), ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}), ...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}), ...(typeof supportsVerbosity === "boolean" ? { supportsVerbosity } : {}), ...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}), ...(supportsServiceTier === true && fastPolicy.fastTierDescription !== undefined ? { fastTierDescription: fastPolicy.fastTierDescription } : {}), // Default-on for openai-chat providers (explicit false opts out); other adapters // advertise only on explicit opt-in. ...(prov.parallelToolCalls === true || (prov.adapter === "openai-chat" && prov.parallelToolCalls !== false) ? { parallelToolCalls: true } : {}), ...(prov.codexToolMode !== undefined ? { codexToolMode: prov.codexToolMode } : {}), }; const capped = applyProviderContextCap(hinted.contextWindow, providerCap); const withCap = providerCap !== undefined ? capped !== hinted.contextWindow ? { ...hinted, contextWindow: capped, contextCap: providerCap, contextCapped: true } : { ...hinted, contextCap: providerCap, contextCapped: false } : hinted; const contextWindow = typeof withCap.contextWindow === "number" && withCap.contextWindow > 0 ? withCap.contextWindow : undefined; const boundedMaxInput = typeof withCap.maxInputTokens === "number" && withCap.maxInputTokens > 0 ? (contextWindow !== undefined ? Math.min(withCap.maxInputTokens, contextWindow) : withCap.maxInputTokens) : undefined; const withHardBounds = boundedMaxInput !== undefined && boundedMaxInput !== withCap.maxInputTokens ? { ...withCap, maxInputTokens: boundedMaxInput } : withCap; const softCandidates = [model.autoCompactTokenLimit, configuredAutoCompact] .filter((value): value is number => typeof value === "number" && value > 0); if (contextWindow === undefined || softCandidates.length === 0) return withHardBounds; return { ...withHardBounds, autoCompactTokenLimit: clampAutoCompactTokenLimit( contextWindow, boundedMaxInput, Math.min(...softCandidates), ), }; } export function catalogHintsFromProviderConfig( name: string, prov: OcxProviderConfig, id: string, contextCap?: number, metadataModelIdCaseFold?: boolean, effectiveAlias?: string | null, ): Partial { const hinted = applyProviderConfigHints(name, prov, { id, provider: name }, contextCap, metadataModelIdCaseFold, effectiveAlias); const { provider: _provider, id: _id, ...hints } = hinted; return hints; } export function applyConfigHintsToCachedModels( name: string, prov: OcxProviderConfig, models: CatalogModel[], contextCap?: number, metadataModelIdCaseFold?: boolean, effectiveAlias?: string | null, ): CatalogModel[] { return models.map(model => applyProviderConfigHints(name, prov, model, contextCap, metadataModelIdCaseFold, effectiveAlias)); } /** * Last-resort context window for combo member synthesis when discovery, * provider config, and an enabled Context cap all omit one. Matches the * catalog entry default in `normalizeRoutedCatalogEntry` so incomplete live * rows still catalog. An enabled Context cap is the operator-facing window, * not a clamp on this placeholder. */ const COMBO_MEMBER_CONTEXT_FALLBACK = 128_000; 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[]; } /** * Ladder advertised for a combo member whose vendor metadata says it reasons but * carries no explicit ladder (Claude, Grok). Codex needs a non-empty ladder to show * the effort control; the routed adapters clamp to the real upstream top rung. */ const ROUTED_COMBO_MEMBER_REASONING_EFFORTS: readonly string[] = ["low", "medium", "high", "xhigh", "max"]; /** * Vendor-table lookup tolerant of point releases and date pins. Configured combo * targets often name a variant the table does not carry (`claude-fable-5-1`, * `claude-opus-4-5-20251101`); the base family row still describes its modality * and reasoning capability, so fall back to it before giving up. */ function comboMemberVendorMetadata(provider: string, modelId: string): ModelMetadata | undefined { const exact = getModelMetadataCaseInsensitive(provider, modelId); if (exact) return exact; let candidate = modelId.replace(/\[[^\]]*\]$/, ""); while (true) { const trimmed = candidate.replace(/-\d+$/, ""); if (trimmed === candidate || !trimmed.includes("-")) return undefined; const hit = getModelMetadataCaseInsensitive(provider, trimmed); if (hit) return hit; candidate = trimmed; } } /** * Combo members are usually thin discovery rows (id + context window). Without a * capability source the combo intersection collapses to text-only / no effort ladder, * and the Codex app then refuses image attachments and hides the effort picker for * every Claude combo. The generated vendor table knows both, so use it as the * last-resort fallback when the caller supplied none. * * `ModelMetadata.maxTokens` is the OUTPUT ceiling, so it fills `maxOutputTokens`. * Mapping it onto `maxInputTokens` would be read by the combo intersection * (`aggregation.ts` `Math.min` over member input ceilings) as a 128k input limit and * shrink a 1M Claude combo window to 128k, taking autoCompactTokenLimit down with it. */ function vendorMetadataComboFallback(target: { provider: string; model: string }): ComboCatalogMemberFallback | undefined { const metadataProvider = resolveMetadataProvider(target.provider); const metadata = metadataProvider ? comboMemberVendorMetadata(metadataProvider, target.model) : undefined; if (!metadata) return undefined; return { ...(typeof metadata.contextWindow === "number" && metadata.contextWindow > 0 ? { contextWindow: metadata.contextWindow } : {}), ...(typeof metadata.maxTokens === "number" && metadata.maxTokens > 0 ? { maxOutputTokens: metadata.maxTokens } : {}), ...(Array.isArray(metadata.input) && metadata.input.length > 0 ? { inputModalities: [...metadata.input] } : {}), ...(metadata.reasoning === true ? { reasoningEfforts: [...ROUTED_COMBO_MEMBER_REASONING_EFFORTS] } : {}), }; } /** * Resolve a combo target to a catalog member for derivation. * Prefer discovery metadata; when the target is missing from the gather map or * lacks a positive contextWindow, synthesize from the (registry-enriched) * provider config so combos remain catalogued when targets are configured but * discovery metadata is incomplete. Disabled providers stay unresolved. * When hints still omit contextWindow, prefer known maxInputTokens, else the * enabled Context cap, else COMBO_MEMBER_CONTEXT_FALLBACK so a live row * without ctx does not drop the whole combo from the public catalog. */ export function resolveComboCatalogMember( target: { provider: string; model: string }, memberByKey: ReadonlyMap, providers: ReadonlyMap, contextCap?: number, callerFallback?: ComboCatalogMemberFallback, metadataModelIdCaseFold?: boolean, ): CatalogModel | undefined { const existing = memberByKey.get(targetKey(target)); const prov = providers.get(target.provider); const fallback = callerFallback ?? vendorMetadataComboFallback(target); // Disabled providers never contribute members — even a complete discovery row // is unusable for catalog derivation while the provider is off. if (prov?.disabled === true) return undefined; const withFallbackMetadata = (member: CatalogModel): CatalogModel => { const contextWindow = typeof member.contextWindow === "number" && member.contextWindow > 0 ? member.contextWindow : 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; const softCandidates = [member.autoCompactTokenLimit, fallback?.autoCompactTokenLimit] .filter((value): value is number => typeof value === "number" && value > 0); const autoCompactTokenLimit = contextWindow !== undefined && softCandidates.length > 0 ? clampAutoCompactTokenLimit(contextWindow, effectiveMaxInput, Math.min(...softCandidates)) : member.autoCompactTokenLimit; const adjustAutoCompact = autoCompactTokenLimit !== member.autoCompactTokenLimit; const addModalities = (!Array.isArray(member.inputModalities) || member.inputModalities.length === 0) && fallback?.inputModalities !== undefined; const addReasoning = member.reasoningEfforts === undefined && fallback?.reasoningEfforts !== undefined; 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!] } : {}), }; }; // Complete live/configured rows still honour providerContextCaps so a high // discovery window cannot outrun an operator-configured cap. Native-alias // fallback metadata may fill only capability gaps; it never raises an explicit // discovered/configured context window. if ( existing && typeof existing.contextWindow === "number" && existing.contextWindow > 0 ) { const capped = applyProviderContextCap(existing.contextWindow, contextCap); if (capped === undefined || capped === existing.contextWindow) { return withFallbackMetadata(existing); } const maxInput = typeof existing.maxInputTokens === "number" && existing.maxInputTokens > 0 ? Math.min(existing.maxInputTokens, capped) : Math.min(fallback?.maxInputTokens ?? capped, capped); return withFallbackMetadata({ ...existing, contextWindow: capped, maxInputTokens: maxInput, contextCap, contextCapped: true as const, }); } const base: CatalogModel = existing ?? { id: target.model, provider: target.provider, }; const hinted = prov ? applyProviderConfigHints(target.provider, prov, base, contextCap, metadataModelIdCaseFold) : base; const hintedContext = typeof hinted.contextWindow === "number" && hinted.contextWindow > 0 ? hinted.contextWindow : undefined; const knownMaxInput = typeof hinted.maxInputTokens === "number" && hinted.maxInputTokens > 0 ? hinted.maxInputTokens : (typeof base.maxInputTokens === "number" && base.maxInputTokens > 0 ? base.maxInputTokens : undefined); // Kept OUT of knownMaxInput on purpose: that value doubles as a context-window fallback // below, and a native alias whose input ceiling (922k) is lower than its window (1.05M) // would otherwise shrink the advertised window to the input limit. const fallbackMaxInput = existing || prov ? fallback?.maxInputTokens : undefined; // Real discovery/config values win. A native alias is the next fallback tier. // The generic 128k/text synthesis from #1305 remains the final fallback. const fallbackContext = existing || prov ? fallback?.contextWindow : undefined; const uncappedContext = hintedContext ?? knownMaxInput ?? fallbackContext ?? (existing || prov ? resolveUnknownRoutedContextWindow(contextCap) : undefined); if (uncappedContext === undefined) return undefined; // 真发现值才压低。resolveUnknownRoutedContextWindow 已经把 cap 当成窗口填进去了,不能再 min 一次。 const usedDiscoveredWindow = hintedContext !== undefined || knownMaxInput !== undefined || fallbackContext !== undefined; const cappedContext = usedDiscoveredWindow ? applyProviderContextCap(uncappedContext, contextCap) : uncappedContext; const contextWindow = cappedContext ?? uncappedContext; const fallbackCapped = usedDiscoveredWindow && contextCap !== undefined && cappedContext !== undefined && cappedContext !== uncappedContext; const inputModalities = hinted.inputModalities ?? base.inputModalities ?? (fallback?.inputModalities ? [...fallback.inputModalities] : undefined) ?? ["text"]; const reasoningEfforts = hinted.reasoningEfforts ?? (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; const maxInputTokens = effectiveMaxInput !== undefined ? Math.min(effectiveMaxInput, contextWindow) : contextWindow; const softCandidates = [ hinted.autoCompactTokenLimit, base.autoCompactTokenLimit, fallback?.autoCompactTokenLimit, configuredAutoCompactTokenLimit(prov, target.model), ].filter((value): value is number => typeof value === "number" && value > 0); // A generic 128k synthesis is a catalog compatibility fallback, not evidence // that a configured soft policy has an authoritative window to clamp against. const hasAuthoritativeAutoCompactBasis = hintedContext !== undefined || fallbackContext !== undefined || contextCap !== undefined; const autoCompactTokenLimit = hasAuthoritativeAutoCompactBasis && softCandidates.length > 0 ? clampAutoCompactTokenLimit(contextWindow, maxInputTokens, Math.min(...softCandidates)) : undefined; return { ...hinted, inputModalities, ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}), contextWindow, maxInputTokens, ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}), ...(autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}), ...(fallbackCapped ? { contextCap, contextCapped: true as const } : {}), }; } const DATED_VARIANT_YYYYMMDD = /^(\d{4})(\d{2})(\d{2})$/; const DATED_VARIANT_YYMMDD = /^(2\d)(\d{2})(\d{2})$/; const DATED_VARIANT_MMDD_OR_YYMM = /^(\d{2})(\d{2})$/; /** Whether a Gregorian year contains February 29th. */ function isLeapYear(year: number): boolean { return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); } /** * Whether a month/day pair exists in the given year. Without a year, February 29th is * accepted because it occurs in at least one calendar year. */ function isValidCalendarDate(year: number | undefined, month: number, day: number): boolean { if (year !== undefined && (year < 1 || year > 9999)) return false; if (month < 1 || month > 12 || day < 1) return false; const daysInMonth = [ 31, year === undefined || isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, ]; return day <= daysInMonth[month - 1]!; } /** * Release-date suffixes providers actually publish: `YYYYMMDD` (`-20251001`), `YYMMDD` * (`-260806`), `MMDD` (`-0813`) and `YYMM` (`-2512`). A `\d{8}`-only rule matched none of * the dated ids on a real multi-provider install, so DeepSeek, Kimi, Mistral, Qwen and * Solar aliases all fell through to `droppedConfiguredIds` (#3024). * * Calendar validation rejects impossible month-end and leap-day values as well as ordinary * numeric suffixes such as `-2048`, `-4096` and `-8192`. `-1024` is the one irreducible * collision — it is a valid `MMDD` (October 24th) — so it reads as dated. That is a known, * accepted cost; the test table pins it so it cannot become a surprise later. * * Hyphenated ISO suffixes (`-2024-08-06`, `-05-06`) are deliberately out of scope: a * hyphenated suffix is ambiguous against ordinary name segments and needs its own call. */ function isDatedVariantSuffix(suffix: string): boolean { const yyyyMmDd = DATED_VARIANT_YYYYMMDD.exec(suffix); if (yyyyMmDd) { return isValidCalendarDate( Number(yyyyMmDd[1]), Number(yyyyMmDd[2]), Number(yyyyMmDd[3]), ); } const yyMmDd = DATED_VARIANT_YYMMDD.exec(suffix); if (yyMmDd) { return isValidCalendarDate( 2000 + Number(yyMmDd[1]), Number(yyMmDd[2]), Number(yyMmDd[3]), ); } const mmDdOrYyMm = DATED_VARIANT_MMDD_OR_YYMM.exec(suffix); if (!mmDdOrYyMm) return false; const first = Number(mmDdOrYyMm[1]); const second = Number(mmDdOrYyMm[2]); return isValidCalendarDate(undefined, first, second) || (first >= 20 && first <= 29 && second >= 1 && second <= 12); } /** Whether `liveId` is a supported dated release of the configured base id. */ export function isDatedVariantId(liveId: string, configuredId: string): boolean { if (!liveId.startsWith(`${configuredId}-`)) return false; return isDatedVariantSuffix(liveId.slice(configuredId.length + 1)); } export const lastDropWarnSignature = new Map(); let lastWarningReconciledGeneration = 0; export function reconcileProviderFetchWarnings(generation: number): number { if (generation <= lastWarningReconciledGeneration) return 0; const removed = lastDropWarnSignature.size; lastDropWarnSignature.clear(); lastWarningReconciledGeneration = generation; return removed; } export const QUIET_AUTHORITATIVE_CATALOG_PROVIDERS = new Set(["kimi", "xai"]); export const CALLABLE_CONFIGURED_COMPATIBILITY_MODELS: Readonly>> = { kimi: new Set([ "k3[1m]", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6", "kimi-k2.5", ]), xai: new Set([ "grok-4.3", "grok-4.20-multi-agent-0309", "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast", ]), }; export function warnDroppedConfiguredIdsOnce(name: string, droppedConfiguredIds: string[]): void { const signature = [...droppedConfiguredIds].sort().join(","); if (lastDropWarnSignature.get(name) === signature) return; lastDropWarnSignature.set(name, signature); console.warn( `[opencodex] Provider model discovery for "${name}" omitted configured model ids; dropping them from the authoritative live catalog: ${droppedConfiguredIds.join(", ")}.`, ); } /** * Z.AI and Neuralwatt advertise GLM reasoning as a bare boolean, which would otherwise * collapse to the four-tier default ladder that omits `max`. These two helpers name the * ladder each GLM generation actually honours on the wire. */ /** GLM-5.2 and its 1M alias: the full five-tier ladder including `max`. */ export function isGlm52ModelId(id: string): boolean { const normalized = id.trim().toLowerCase(); return normalized === "glm-5.2" || normalized === "glm-5.2[1m]"; } /** * GLM-5.3 and its 1M alias. 260814: docs.z.ai/devpack/latest-model folds every incoming * effort into three effective tiers (low/minimal/light -> low, medium/high -> high, * xhigh/max/ultra -> max), so a boolean capability must not be expanded to five rows. */ export function isGlm53ModelId(id: string): boolean { const normalized = id.trim().toLowerCase(); return normalized === "glm-5.3" || normalized === "glm-5.3[1m]"; } function plainRecord(value: unknown): Record | undefined { return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record : undefined; } const MODEL_DISCOVERY_METADATA_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/; function positiveSafeInteger(...values: unknown[]): number | undefined { return values.find(value => typeof value === "number" && Number.isSafeInteger(value) && value > 0) as number | undefined; } 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); return normalized || undefined; } function normalizedStringList(value: unknown, maxItems = 32, maxLength = 64): string[] | undefined { if (!Array.isArray(value)) return undefined; const out: string[] = []; const maxInspectedItems = Math.max(maxItems * 8, maxItems); for (let i = 0; i < value.length && i < maxInspectedItems; i += 1) { const raw = value[i]; if (typeof raw !== "string") continue; const normalized = normalizedMetadataString(raw, maxLength); if (normalized && !out.includes(normalized)) out.push(normalized); if (out.length >= maxItems) break; } return out.length > 0 ? out : undefined; } function modelCapabilities(item: ProviderModelsApiItem): string[] | undefined { const metadata = plainRecord(item.metadata); const metadataCapabilities = metadata?.capabilities; const capabilityRecord = plainRecord(metadataCapabilities) ?? plainRecord(item.capabilities) ?? plainRecord(item.features); const out = new Set(); for (const list of [item.capabilities, item.features, item.supported_features, metadataCapabilities]) { for (const capability of normalizedStringList(list) ?? []) out.add(capability); } const capabilityFields = capabilityRecord ?? {}; let inspectedCapabilityFields = 0; for (const key in capabilityFields) { if (!Object.hasOwn(capabilityFields, key)) continue; inspectedCapabilityFields += 1; if (inspectedCapabilityFields > 256 || out.size >= 32) break; if (capabilityFields[key] === true) { const normalized = normalizedMetadataString(key, 64); if (normalized) out.add(normalized); } } for (const field of ["supports_tools", "supports_tool_calling", "supports_function_calling"] as const) { if (item[field] === true) out.add("tools"); } for (const field of ["supports_reasoning", "reasoning"] as const) { if (item[field] === true) out.add("reasoning"); } return out.size > 0 ? [...out].filter(Boolean).slice(0, 32) : undefined; } function modelInputModalities( item: ProviderModelsApiItem, capabilities: readonly string[] | undefined, ): string[] | undefined { const metadata = plainRecord(item.metadata); const capabilityRecord = plainRecord(metadata?.capabilities) ?? plainRecord(item.capabilities) ?? plainRecord(item.features); const explicit = normalizedStringList( item.input_modalities ?? item.modalities ?? metadata?.input_modalities ?? capabilityRecord?.input_modalities ?? plainRecord(item.architecture)?.input_modalities, 8, 24, )?.filter(value => ( // Codex parses `input_modalities` as a closed enum of text | image | audio. A provider that // advertises anything else (zenmux reports "video") must not reach the catalog: Codex rejects // the whole file, so plugins, apps and MCP servers all stop loading over one model's metadata. value === "text" || value === "image" || value === "audio" )); if (explicit && explicit.length > 0) return explicit; const architecture = plainRecord(item.architecture); const architectureModality = typeof architecture?.modality === "string" ? normalizedMetadataString(architecture.modality, 64) : undefined; if (architectureModality?.includes("->")) { const [rawInput = ""] = architectureModality.split("->"); const inferred = rawInput .split("+") .filter(value => value === "text" || value === "image" || value === "audio"); if (inferred.length > 0) return [...new Set(inferred)]; } // GitHub Copilot nests vision support one level down as `capabilities.supports.vision`, so the // flat read alone finds nothing and every Copilot model falls through to `["text"]` — Codex then // refuses image attachments on models that accept them (#2941). Precedence is by specificity: // a flat boolean is authoritative when present, the nested boolean is consulted only otherwise, // and a non-boolean at either level decides NOTHING so the signals below still apply. Two things // this ordering deliberately avoids: a deny-wins rule across both levels would flip a provider // reporting flat `true` with nested `false` from image-capable to text-only, changing behaviour // that predates Copilot support; and a truthy test would let the string `"no"` advertise image // input. The payload also carries a SECOND `vision` key under `limits` holding an image count, // which is why this reads one exact path instead of searching `capabilities` for a vision-ish key. const nestedSupports = plainRecord(capabilityRecord?.supports); const explicitVisionSupport = typeof capabilityRecord?.vision === "boolean" ? capabilityRecord.vision : typeof nestedSupports?.vision === "boolean" ? nestedSupports.vision : undefined; if (explicitVisionSupport === false) return ["text"]; if (explicitVisionSupport === true || capabilities?.some(value => ( value === "vision" || value === "image-input" || value === "image_input" // llama.cpp and Ollama-compatible servers report vision as "multimodal" — // it is the only image signal those servers emit (#1797). Mapped to the // closed `text|image` enum rather than passed through: an out-of-enum // modality makes Codex reject the entire catalog file. || value === "multimodal" ))) { return ["text", "image"]; } return undefined; } export function catalogHintsFromModelsApiItem(providerName: string, item: ProviderModelsApiItem): Partial { const metadata = plainRecord(item.metadata); const capabilityRecord = plainRecord(metadata?.capabilities) ?? plainRecord(item.capabilities); const limits = plainRecord(metadata?.limits); const capabilityLimits = plainRecord(plainRecord(item.capabilities)?.limits); const contextWindow = positiveSafeInteger( limits?.max_context_length, // GitHub Copilot reports the live context window here instead of in the metadata or // top-level fields used by other OpenAI-compatible catalogs (#3156). Keep the existing // metadata field authoritative when both are present: adding this provider-specific // fallback must not change previously recognized providers. capabilityLimits?.max_context_window_tokens, metadata?.context_length, item.context_length, item.context_size, item.max_model_len, item.max_context_length, item.context_window, item.max_context_window, item.max_context_size, item.n_ctx, plainRecord(item.top_provider)?.max_context_length, plainRecord(metadata?.top_provider)?.max_context_length, // llama.cpp reports the served context under `meta`: `n_ctx` is what the // server was actually started with, `n_ctx_train` the model's trained // maximum. Prefer the served value — routing must not promise a window the // running server will refuse. Both come LAST so no provider already // supplying a recognized field changes behavior (#1797). plainRecord(item.meta)?.n_ctx, item.default_context_size, 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 // before client exporters (including omp) can advertise it. const reasoningParameters = plainRecord(item.reasoning_parameters) ?? plainRecord(metadata?.reasoning_parameters) ?? plainRecord(capabilityRecord?.reasoning_parameters); const rawReasoningEfforts = capabilityRecord?.reasoning_effort ?? item.reasoning_efforts ?? reasoningParameters?.efforts; const listedReasoningEfforts = normalizedStringList(rawReasoningEfforts, 8, 24); const reasoningEfforts = listedReasoningEfforts ? sanitizeCodexReasoningEfforts(listedReasoningEfforts) : typeof rawReasoningEfforts === "boolean" ? (rawReasoningEfforts ? ((providerName === "neuralwatt" || providerName === "zai") && isGlm53ModelId(item.id) ? ["low", "high", "max"] : (providerName === "neuralwatt" || providerName === "zai") && isGlm52ModelId(item.id) ? ["low", "medium", "high", "xhigh", "max"] : ["low", "medium", "high", "xhigh"]) : []) : undefined; const capabilities = modelCapabilities(item); const inputModalities = modelInputModalities(item, capabilities); return { ...(contextWindow && contextWindow > 0 ? { contextWindow } : {}), ...(maxInputTokens && maxInputTokens > 0 ? { maxInputTokens } : {}), ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}), ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}), ...(inputModalities ? { inputModalities } : {}), ...(capabilities ? { capabilities } : {}), }; } function boundedOwnedBy(value: unknown): string | undefined { if (typeof value !== "string" || value.length === 0 || value.length > 256) return undefined; if (MODEL_DISCOVERY_METADATA_CONTROL_CHARS.test(value)) return undefined; return value; } const refreshingModelsAuthResolver: ModelsAuthResolver = { kind: "refreshing" }; function observedModelsAuthResolver( authStoreBuffer: Uint8Array | null, outcomes: CatalogGatherProviderAuthOutcome[], ): ModelsAuthResolver { return { kind: "observed", resolve(name, provider) { if (provider.authMode === "forward") return { apiKey: undefined, observed: true }; if (provider.authMode !== "oauth") { return { apiKey: resolveProviderApiKey(provider.apiKey), observed: true }; } const observation = observeActiveOAuthAccessToken(name, authStoreBuffer); outcomes.push({ provider: name, state: observation.kind }); if (observation.kind !== "available") return { apiKey: undefined, observed: true }; return { apiKey: observation.snapshot.accessToken, observed: true, ...(observation.snapshot.apiBaseUrl ? { oauthApiBaseUrl: observation.snapshot.apiBaseUrl } : {}), ...(observation.snapshot.projectId ? { oauthProjectId: observation.snapshot.projectId } : {}), }; }, }; } async function fetchProviderModelsWithAuth( captured: CapturedProviderGather, ttlMs: number, contextCap: number | undefined, resolveAuth: ModelsAuthResolver, ): Promise { const { name, provider: prov, discovery, request, metadataModelIdCaseFold } = captured; const observed = ( models: CatalogModel[], state: CatalogGatherProviderModelOutcome["state"], ): ProviderModelsResult => ({ models, outcome: { provider: name, state } }); // Capture before any credential refresh or outbound await. OAuth account changes clear this // generation, so a request started with the former account cannot later publish its result. const cacheGeneration = captureModelCacheGeneration(name); const isCurrentCacheGeneration = () => isModelCacheGenerationCurrent(name, cacheGeneration); if (prov.authMode === "forward") return observed([], "authoritative"); // ChatGPT backend has no /models const seedVertexDefault = prov.adapter === "google" && prov.googleMode === "vertex" && (prov.models?.length ?? 0) === 0 && Boolean(prov.defaultModel); const seedStaticDefault = prov.liveModels === false && (prov.models?.length ?? 0) === 0 && Boolean(prov.defaultModel); // Ordered dedupe union: implicit default seed, then `models`, then `retainModels`. `configured` is the // single seed for the static path, the degraded fallback, drop diagnostics, and provider hints, // so a retain-only id must enter here or it never exists to be retained (#1690). const configuredIds = [...new Set([ ...((seedVertexDefault || seedStaticDefault) && prov.defaultModel ? [prov.defaultModel] : []), ...(prov.models ?? []), ...(prov.retainModels ?? []), ])]; const configured: CatalogModel[] = configuredIds.map(id => ({ id, provider: name, ...catalogHintsFromProviderConfig(name, prov, id, contextCap, metadataModelIdCaseFold, captured.effectiveAlias), })); const withConfiguredRetention = ( models: CatalogModel[], options?: { retainComboTargets?: boolean; warnDrops?: boolean }, ): CatalogModel[] => { const { models: merged, droppedConfiguredIds } = mergeConfiguredModelsIntoLiveCatalog({ name, provider: prov, models, configured, retainConfiguredModelIds: captured.retainConfiguredModelIds, contextCap, seedVertexDefault, retainComboTargets: options?.retainComboTargets, metadataModelIdCaseFold, }); if ( options?.warnDrops === true && droppedConfiguredIds.length > 0 && name !== OPENAI_API_PROVIDER_ID && !QUIET_AUTHORITATIVE_CATALOG_PROVIDERS.has(name) ) { warnDroppedConfiguredIdsOnce(name, droppedConfiguredIds); } return merged; }; // Static catalogs never need an OAuth refresh or an upstream model request. Clear any // discovery failure left by an older live configuration even when the account is logged out. if (prov.liveModels === false) { clearProviderDiscoveryStatus(name); return observed(configured, "authoritative"); } const auth: ModelsAuthResolution = captured.observedAuth ?? (resolveAuth.kind === "refreshing" ? prov.authMode === "oauth" && effectiveGoogleMode(name, prov) === "cloud-code-assist" ? await getValidAccessTokenSnapshot(name) .then(snapshot => ({ apiKey: snapshot.accessToken, observed: false, ...(snapshot.projectId ? { oauthProjectId: snapshot.projectId } : {}), })) .catch(() => ({ apiKey: undefined, observed: false })) : { apiKey: await resolveModelsAuthToken(name, prov), observed: false } : resolveAuth.resolve(name, prov)); const apiKey = auth.apiKey; // A configured default is a real callable selector and must remain discoverable when a // compatible provider's live /models request fails (issue #308). Static providers already seed // their default selector above when no explicit model list exists. const failedDiscoveryConfigured = configured.length > 0 || !prov.defaultModel || prov.adapter !== "anthropic" ? configured : [{ id: prov.defaultModel, provider: name, ...catalogHintsFromProviderConfig(name, prov, prov.defaultModel, contextCap, metadataModelIdCaseFold, captured.effectiveAlias), }]; const vertexDefaultSeed = seedVertexDefault ? configured[0] : undefined; const withVertexDefaultSeed = (models: CatalogModel[]): CatalogModel[] => ( vertexDefaultSeed && !models.some(model => model.id === vertexDefaultSeed.id) ? [...models, vertexDefaultSeed] : models ); if (prov.adapter === "cursor") { if (!apiKey) return observed(configured, "degraded"); // Cursor uses a bespoke GetUsableModels RPC (not /models), returning the full effort-suffixed // variants this PLAN can use. Keep the base-model UX (the request builder appends the effort // suffix) but filter the static seed to the bases the account actually has — so models not on the // plan (e.g. claude-fable-5) drop out instead of failing ERROR_BAD_MODEL_NAME. Fall back to the seed. const cachedCursor = getFreshCached(name, ttlMs); if (cachedCursor) { return observed( withConfiguredRetention(applyConfigHintsToCachedModels(name, prov, cachedCursor, undefined, metadataModelIdCaseFold, captured.effectiveAlias)), "authoritative", ); } if (isModelsFetchCoolingDown(name)) { const cooling = getStaleCached(name); return observed( withConfiguredRetention( cooling ? applyConfigHintsToCachedModels(name, prov, cooling, undefined, metadataModelIdCaseFold, captured.effectiveAlias) : configured, ), "degraded", ); } const cursorFetch = (prov as OcxProviderConfig & { fetch?: typeof globalThis.fetch }).fetch; const liveResult = await fetchCursorUsableModels({ apiKey, baseUrl: prov.baseUrl, upstreamHttpVersion: prov.upstreamHttpVersion, ...(cursorFetch ? { fetch: cursorFetch } : {}), }); if (liveResult.ok) { const available = filterCursorConfiguredModelsByLiveDiscovery(configured, liveResult.models); const result = available.length > 0 ? available : configured; // Cache the discovery-filtered roster without combo retention so a later // gather can re-apply the current capture's retain set on read. const forCache = withConfiguredRetention(result, { retainComboTargets: false }); if (!setCached(name, forCache, Date.now(), cacheGeneration)) { return observed(withConfiguredRetention(configured), "degraded"); } // Publish roster-derived state only for a discovery the cache accepted: a stale // in-flight capture (generation revoked by a credential/config change) must not // overwrite the spelling or Max-Mode evidence of the newer one. recordLiveCursorClaudeModels(liveResult.models); // Live Max-Mode evidence feeds the umbrella resolver's ultra gate // (devlog 260828_cursor_umbrella_catalog; union with static evidence). recordLiveCursorMaxModeModels(liveResult.maxModeModels ?? []); markProviderDiscoveryOk(name, liveResult.models.length); return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative"); } if (isCurrentCacheGeneration()) { markModelsFetchFailure(name); markProviderDiscoveryFailed(name, { reason: "provider" }); console.warn( `[opencodex] Cursor model discovery for "${name}" failed [${liveResult.error}]${liveResult.detail ? `: ${liveResult.detail}` : ""}; using stale/static catalog degradation.`, ); } const staleCursor = getStaleCached(name); return observed( withConfiguredRetention( staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor, undefined, metadataModelIdCaseFold, captured.effectiveAlias) : configured, ), "degraded", ); } if (prov.authMode === "oauth" && !apiKey) { // No usable token (logged out, or account marked needsReauth). Still surface the // configured static catalog so the GUI Models tab / rail counts are not empty — // matching Cursor's !apiKey → configured degradation and fetch-failure fallback. return observed(configured, "degraded"); } const cloudCodeAssist = effectiveGoogleMode(name, prov) === "cloud-code-assist"; const project = prov.project ?? auth.oauthProjectId; if (cloudCodeAssist && !project) return observed(configured, "degraded"); const fresh = getFreshCached(name, ttlMs); if (fresh) { return observed( withConfiguredRetention( withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, fresh, contextCap, metadataModelIdCaseFold, captured.effectiveAlias)), ), "authoritative", ); // dedups Codex's frequent /v1/models polling within the TTL } if (isModelsFetchCoolingDown(name)) { // A recently-failed provider (unreachable API, missing proxy, bad key) must not re-pay the // fetch timeout on every catalog poll — the dashboard polls this path per page load. const stale = getStaleCached(name); return observed( withConfiguredRetention( stale ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap, metadataModelIdCaseFold, captured.effectiveAlias)) : failedDiscoveryConfigured, ), "degraded", ); } const url = request.url; let headers = materializeCapturedHeaders(request, apiKey); // One Ollama authority contract: for canonical ollama-cloud/ollama-native rows, discovery // (/v1/models), enrichment (/api/show) and inference (/api/chat) must all materialize the // SAME effective credential/header authority. buildModelsRequest's generic tail writes the // generated Bearer AFTER configured headers, but the native inference adapter applies // provider.headers LAST (configured wins, case-insensitive collapse). Reapply the configured // provider headers here so the whole Ollama request family shares that one authority. if (ollamaShowEnrichable(name, prov)) { headers = applyConfiguredHeadersLast(headers, prov.headers); } const urlClass = new URL(url).hostname.endsWith("aiplatform.googleapis.com") ? "vertex-aiplatform" : "provider-models"; const failedDiscoveryFallback = ( failure: ProviderModelDiscoveryFailure, ): { models: CatalogModel[]; fallback: "stale" | "configured"; shouldLog: boolean } => { if (!isCurrentCacheGeneration()) { return { models: withConfiguredRetention(failedDiscoveryConfigured), fallback: "configured", shouldLog: false, }; } // Decide logging BEFORE recording the new status, so we can compare against the prior one and // suppress an identical repeated failure (#395 log flood). The failure stays observable via the // discovery-status API regardless. const shouldLog = shouldLogDiscoveryFailure(name, failure); markModelsFetchFailure(name); markProviderDiscoveryFailed(name, failure); const stale = getStaleCached(name); return { models: withConfiguredRetention( stale ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap, metadataModelIdCaseFold, captured.effectiveAlias)) : failedDiscoveryConfigured, ), fallback: stale ? "stale" : "configured", shouldLog, }; }; try { // Canonical-URL TUN transparency for Clash/Surge/Mihomo fake-IP DNS: // `isRegistryModelDiscoveryUrl` proves the FINAL request URL is the // registry's own fixed discovery URL, so a purely-benchmark DNS answer may // be pin-connected through the intercepting TUN without proxy env. The // proof is on the URL — not the provider name — because an OAuth/forward // name matches any baseUrl by design. Retargeted or renamed custom rows // fetch a different URL and keep the rejection. const outboundDependencies = { isCanonicalUrl: isRegistryModelDiscoveryUrl }; const res = request.method === "POST" ? await providerOutboundPost(name, prov, url, { headers, body: JSON.stringify({ project }), signal: AbortSignal.timeout(8000), }, outboundDependencies) : await providerOutboundGet(name, prov, url, { headers, signal: AbortSignal.timeout(8000), }, outboundDependencies); const redirectError = await providerRedirectError(res, url); if (redirectError) { const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "http", httpStatus: res.status }); if (shouldLog) { console.warn( `[opencodex] Provider model discovery for "${name}" ${redirectError} [urlClass=${urlClass}, fallback=${fallback}].`, ); } return observed(models, "degraded"); } if (!res.ok) { const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "http", httpStatus: res.status }); if (shouldLog) { console.warn( `[opencodex] Provider model discovery for "${name}" failed with HTTP ${res.status} [urlClass=${urlClass}, fallback=${fallback}].`, ); } return observed(models, "degraded"); } const contentType = ( res.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() || "missing" ).slice(0, 80); const bounded = await readBoundedDiscoveryJson(res, discovery.maxResponseBytes); if (!bounded.ok) { const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "invalid_response" }); const diagnostic = bounded.reason === "response_too_large" ? `exceeded the ${discovery.maxResponseBytes}-byte response limit` : contentType === "application/json" || contentType.endsWith("+json") ? "returned invalid JSON in a 2xx response" : "returned a non-JSON 2xx response"; if (shouldLog) { console.warn( `[opencodex] Provider model discovery for "${name}" ${diagnostic} [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`, ); } return observed(models, "degraded"); } const antigravity = cloudCodeAssist ? parseAntigravityAvailableModels(bounded.value, discovery.maxModels) : undefined; if (cloudCodeAssist && !antigravity) { const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "invalid_response" }); if (shouldLog) { console.warn( `[opencodex] Provider model discovery for "${name}" returned malformed CCA model data [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`, ); } return observed(models, "degraded"); } if (antigravity) { const live = antigravity.map(model => applyProviderConfigHints(name, prov, { id: model.id, provider: name, // CCA only exposes a numeric thinking budget. Until the adapter owns an exact Codex // effort-to-wire mapping for a newly discovered model, do not advertise a false ladder. reasoningEfforts: [], ...(model.contextWindow ? { contextWindow: model.contextWindow } : {}), ...(model.inputModalities ? { inputModalities: model.inputModalities } : {}), }, contextCap, metadataModelIdCaseFold, captured.effectiveAlias)); const forCache = withConfiguredRetention(live, { retainComboTargets: false }); if (!setCached(name, forCache, Date.now(), cacheGeneration)) { return observed(withConfiguredRetention(configured), "degraded"); } registerAntigravityDiscoveredWireModels(prov.baseUrl, antigravity, { provider: name, cacheGeneration, }); markProviderDiscoveryOk(name, live.length); return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative"); } const extracted = extractProviderModelItems(bounded.value, discovery); if (!extracted.ok) { const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "invalid_response" }); const diagnostic: Record = { response_too_large: "returned an oversized 2xx response", invalid_json: "returned invalid JSON in a 2xx response", invalid_shape: "returned malformed 2xx data", too_many_models: `exceeded the ${discovery.maxModels}-row model limit`, }; if (shouldLog) { console.warn( `[opencodex] Provider model discovery for "${name}" ${diagnostic[extracted.reason]} [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`, ); } return observed(models, "degraded"); } const items = extracted.items; // Ollama Cloud enrichment: /v1/models carries no per-model context or capability metadata, // so a newly announced id would otherwise publish generic defaults. /api/show fills that // per model, fail-soft, bounded, and cached with this gather's result. Explicit configured // metadata keeps its normal precedence (applyProviderConfigHints applies the discovered // window only where exact config is absent, and the provider context cap still caps it). const showEnrichment = ollamaShowEnrichable(name, prov) ? await fetchOllamaShowEnrichment({ headers, discoveryUrl: request.url, modelIds: items.map(m => m.id), provider: prov, }).catch(() => undefined) : undefined; const live = items.map(m => { const ownedBy = boundedOwnedBy(m.owned_by); // Precedence: the authoritative /v1/models row wins; /api/show fills only metadata the // models-API row does not carry. applyProviderConfigHints then applies explicit // configured metadata over both, and the provider context cap still caps the result. const modelsApiHints = catalogHintsFromModelsApiItem(name, m); const show = showEnrichment?.metadata.get(m.id); const discoveredHints = { ...modelsApiHints, ...(modelsApiHints.contextWindow === undefined && show?.contextWindow !== undefined ? { contextWindow: show.contextWindow } : {}), ...(modelsApiHints.inputModalities === undefined && show?.nativeVision === true ? { inputModalities: ["text", "image"] as string[] } : {}), }; return applyProviderConfigHints(name, prov, { id: m.id, provider: name, ...(ownedBy ? { owned_by: ownedBy } : {}), ...discoveredHints, }, contextCap, metadataModelIdCaseFold, captured.effectiveAlias); }) .filter(m => shouldExposeProviderModel(name, m.id)); // Capture the count BEFORE the alias/configured augmentation below pushes extra rows into // `live`; otherwise configured entries would be reported as discovered ones. const liveModelCount = live.length; // Dated-release aliases + configured retention (compat allow-list, combo targets, // Vertex default). Cache without combo retention so a later gather re-applies the // current capture's retain set on read (warm-cache OCX-111 / #1308). const forCache = withConfiguredRetention(live, { retainComboTargets: false }); const returned = withConfiguredRetention(forCache, { warnDrops: true }); const droppedConfiguredIds = configured .map(model => model.id) .filter(id => !returned.some(model => model.id === id)); if (returned.length === 0 && name !== OPENAI_API_PROVIDER_ID) { console.warn( `[opencodex] Provider model discovery for "${name}" returned an authoritative empty catalog; ${droppedConfiguredIds.length > 0 ? `dropping configured model ids: ${droppedConfiguredIds.join(", ")}` : "no models will be exposed"}.`, ); } if (!setCached(name, forCache, Date.now(), cacheGeneration)) { return observed(withConfiguredRetention(configured), "degraded"); } markProviderDiscoveryOk(name, liveModelCount); return observed(returned, "authoritative"); } catch (error) { if (error instanceof ProviderOutboundPolicyError) { const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "blocked" }); if (shouldLog) { console.warn( `[opencodex] Provider model discovery for "${name}" was blocked by destination policy: ${error.message} [urlClass=${urlClass}, fallback=${fallback}].`, ); } return observed(models, "degraded"); } const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "network" }); if (shouldLog) { console.warn( `[opencodex] Provider model discovery for "${name}" threw ${error instanceof Error ? error.name : "unknown"} [urlClass=${urlClass}, fallback=${fallback}].`, ); } return observed(models, "degraded"); } } export async function fetchProviderModels( name: string, prov: OcxProviderConfig, ttlMs: number, contextCap?: number, ): Promise { const captured = captureProviderGather(name, prov, refreshingModelsAuthResolver); return (await fetchProviderModelsWithAuth( captured, ttlMs, contextCap, refreshingModelsAuthResolver, )).models; } export function shouldExposeProviderModel(providerName: string, modelId: string): boolean { if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free"); // xAI /models advertises both the dated deployment and this floating alias. // Keep only grok-4.20-multi-agent-0309; the alias is the same server-side id. if (providerName === "xai" && modelId === "grok-4.20-multi-agent-beta-latest") return false; return true; } export function shouldRetainConfiguredProviderModel( providerName: string, modelId: string, prov?: OcxProviderConfig, ): boolean { if (CALLABLE_CONFIGURED_COMPATIBILITY_MODELS[providerName]?.has(modelId)) return true; if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free"); if (modelInList(prov?.retainModels, modelId)) return true; return false; } /** * Fold dated-release aliases and retain configured rows that must survive an * authoritative live roster (compatibility allow-list, combo targets, Vertex * default). Used on every discovery return — live, fresh cache, stale, and * failure fallback — so a warm cache captured before a combo existed still * surfaces the configured target (OCX-111 / #1308). * * Cache writes should pass `retainComboTargets: false` so combo retention is * re-applied on read against the current capture, not frozen into the TTL entry. */ export function mergeConfiguredModelsIntoLiveCatalog(opts: { name: string; provider: OcxProviderConfig; models: readonly CatalogModel[]; configured: readonly CatalogModel[]; retainConfiguredModelIds?: ReadonlySet; contextCap?: number; seedVertexDefault?: boolean; retainComboTargets?: boolean; metadataModelIdCaseFold?: boolean; }): { models: CatalogModel[]; droppedConfiguredIds: string[] } { const { name, provider: prov, configured, retainConfiguredModelIds, contextCap, seedVertexDefault, retainComboTargets = true, metadataModelIdCaseFold, } = opts; const out = [...opts.models]; const present = new Set(out.map(model => model.id)); const droppedConfiguredIds: string[] = []; for (const candidate of configured) { 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, metadataModelIdCaseFold)); present.add(candidate.id); continue; } if ( seedVertexDefault === true || shouldRetainConfiguredProviderModel(name, candidate.id, prov) || (retainComboTargets && retainConfiguredModelIds?.has(candidate.id) === true) ) { out.push(candidate); present.add(candidate.id); continue; } droppedConfiguredIds.push(candidate.id); } return { models: out, droppedConfiguredIds }; } export function filterCatalogVisibleModels( models: CatalogModel[], config: Pick, ): CatalogModel[] { const disabled = new Set(config.disabledModels ?? []); const allowByProvider = new Map>(); for (const [name, prov] of Object.entries(config.providers)) { const sel = prov.selectedModels; // Keyed the way `sync.ts` keys the same list, so a slash-bearing native id and // the encoded slug the Codex picker displays are one entry rather than two. A // bare `Set(sel)` matched only the native form, so an allowlist written from the // displayed slug — which `ocx models remove` also accepts — hid every model it // was meant to keep. // // The key is deliberately lossy: `p/a/b` and `p/a-b` collapse to one entry, so a // provider publishing both spellings has them selected together. That is a real // limitation, pinned by the tests below and tracked as a follow-up; it is NOT // fixed here. Resolving selections against the current roster instead was tried // and rejected — the roster is an incomplete dictionary (live discovery can omit // a published id), so it produces the same over-grant while additionally // disagreeing with the `slugEquivalenceKey` contract `sync.ts` uses at merge time. // Two catalog stages with different equivalence relations is the exact bug class // this change exists to remove. if (Array.isArray(sel) && sel.length > 0) { allowByProvider.set(name, new Set(sel.map(model => slugEquivalenceKey(routedSlug(name, model))))); } } return models.filter(m => { if (initialModelSelectionPending(config.providers[m.provider])) return false; const nativeAlias = m.provider === COMBO_NAMESPACE && m.nativeAlias === true; // disabledModels may be stored raw (canonical) or encoded (legacy UI writes). for (const stored of disabled) { // Combo management stores the public alias, while canonical `combo/` references // remain valid for backward compatibility through slugEquals below. if (m.alias !== undefined && stored === catalogModelSlug(m) && !nativeAlias) return false; if (slugEquals(stored, m.provider, m.id)) return false; } const allow = allowByProvider.get(m.provider); return !allow || allow.has(slugEquivalenceKey(routedSlug(m.provider, m.id))); }); } export async function gatherRoutedModels( config: OcxConfig, options?: GatherRoutedModelsOptions, ): Promise { return gatherRoutedModelsWithAuth( config, `refreshing:${gatherFlightKey(config)}`, () => refreshingModelsAuthResolver, options, ); } /** * Catalog-gather model discovery using only auth-store bytes already captured by the * filesystem-evidence owner. This entry point never reaches the refreshing resolver. */ export async function gatherRoutedModelsForCatalogGather( config: OcxConfig, evidence: CatalogGatherProviderAuthEvidence, options?: GatherRoutedModelsOptions, ): Promise { const authStoreBuffer = evidence.authStoreBuffer === null ? null : Uint8Array.from(evidence.authStoreBuffer); const authIdentity = authStoreBuffer === null ? "absent" : keyedGatherBytesIdentity("catalog-observed-auth-v1", authStoreBuffer); return gatherRoutedModelsWithAuth( config, `observed:${authIdentity}:${gatherFlightKey(config)}`, outcomes => observedModelsAuthResolver(authStoreBuffer, outcomes), options, ); } async function gatherRoutedModelsWithAuth( config: OcxConfig, key: string, createAuthResolver: ModelsAuthResolverFactory, options?: GatherRoutedModelsOptions, ): Promise { const capture = captureGatherFlight(config, createAuthResolver); const bucket = gatherInflight.get(key) ?? []; let entry = bucket.find(candidate => ( candidate.discoveryPolicyIdentity === capture.discoveryPolicyIdentity && candidate.authIdentity === capture.authIdentity && candidate.providerGraphIdentity === capture.providerGraphIdentity )); if (!entry) { const lease = gatherGate.tryAcquire(); if (!lease) throw new CatalogGatherBusyError(); // Claim the slot synchronously before any await so same-key callers join this flight. // Distinct authorities retain separate entries even when their legacy bucket matches. let ownedEntry!: GatherInflightEntry; const flight = gatherRoutedModelsUncached(config, capture).finally(() => { const current = gatherInflight.get(key); const index = current?.indexOf(ownedEntry) ?? -1; if (current && index >= 0) current.splice(index, 1); if (current?.length === 0) gatherInflight.delete(key); lease.release(); }); ownedEntry = Object.freeze({ discoveryPolicyIdentity: capture.discoveryPolicyIdentity, authIdentity: capture.authIdentity, providerGraphIdentity: capture.providerGraphIdentity, promise: flight, }); bucket.push(ownedEntry); gatherInflight.set(key, bucket); entry = ownedEntry; } const { models, comboOmissions, providerAuthOutcomes, providerModelOutcomes, discoveryPolicySnapshots, } = await entry.promise; if (options?.comboOmissions) { options.comboOmissions.length = 0; options.comboOmissions.push(...comboOmissions); } if (options?.providerAuthOutcomes) { options.providerAuthOutcomes.length = 0; options.providerAuthOutcomes.push(...providerAuthOutcomes); } if (options?.providerModelOutcomes) { options.providerModelOutcomes.length = 0; options.providerModelOutcomes.push(...providerModelOutcomes); } if (options?.discoveryPolicySnapshots) { options.discoveryPolicySnapshots.length = 0; options.discoveryPolicySnapshots.push(...discoveryPolicySnapshots); } return models; } /** Bound a proven Codex-forward custom row without changing its stored configuration. */ function boundCustomNativeReasoning( model: CatalogModel, allowed: readonly string[], nativeDefault: string | undefined, ): CatalogModel { if (allowed.length === 0 || model.reasoningEfforts === undefined) return model; const bounded = { ...model }; if (model.reasoningEfforts.length === 0) { bounded.reasoningEfforts = []; delete bounded.defaultReasoningEffort; return bounded; } const declared = new Set(model.reasoningEfforts); const surviving = [...new Set(allowed)].filter(effort => declared.has(effort)); const fallback = nativeDefault && allowed.includes(nativeDefault) ? nativeDefault : allowed[0]!; // A nonempty but incompatible declaration is not an explicit no-reasoning setting. bounded.reasoningEfforts = surviving.length > 0 ? surviving : [fallback]; bounded.defaultReasoningEffort = model.defaultReasoningEffort && bounded.reasoningEfforts.includes(model.defaultReasoningEffort) ? model.defaultReasoningEffort : bounded.reasoningEfforts.includes(fallback) ? fallback : bounded.reasoningEfforts[0]!; return bounded; } async function gatherRoutedModelsUncached( config: OcxConfig, capture: GatherFlightCapture, ): Promise { // Flight-local list: joiners copy from the resolved promise, not a process-global last write. const localOmissions: ComboCatalogOmission[] = []; const localProviderAuthOutcomes = capture.providerAuthOutcomes; const resolveAuth = capture.authResolver; const ttlMs = config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS; // Persisted provider entries can predate newer registry fields (noVisionModels, // modelInputModalities, ...). The ROUTER merges registry seeds at request time // (routedProviderConfig), so the proxy behaves correctly — the catalog listing must see the // same merged view or its advertisements drift from actual proxy behavior (e.g. a // vision-sidecar model advertised text-only, blocking image attachments app-side). // Enrich a CLONE: hydrated defaults must never leak into the persisted config. const activeProviders = capture.providers; const providerResults = await Promise.all( activeProviders.map(provider => fetchProviderModelsWithAuth( provider, ttlMs, providerContextCap(config, provider.name), resolveAuth, )), ); const lists = providerResults.map(result => result.models); const apiAugmented = augmentRoutedModelsWithCapturedOpenAiApiRows( lists.flat(), config, capture.openAiApiPolicy, ); const apiProvider = activeProviders.find(provider => provider.name === OPENAI_API_PROVIDER_ID); // Trusted reconstruction replaces whole rows, including the earlier Fast hints. // Restore only that capability from the same captured authority used by discovery. if (apiProvider) { for (const model of apiAugmented) { if (model.provider !== OPENAI_API_PROVIDER_ID) continue; const policy = fastPolicyForModel(apiProvider.provider, model.id, apiProvider.name); const supported = serviceTierSupportFromPolicy(policy); if (supported !== undefined) model.supportsServiceTier = supported; if (supported === true && policy.fastTierDescription !== undefined) model.fastTierDescription = policy.fastTierDescription; } } 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). .filter(shouldExposeRoutedModel); const memberByKey = new Map(all.map(model => [`${model.provider}/${model.id}`, model])); // [Decision Log] // - 목적과 의도: 콤보 타겟에 native OpenAI(Codex login) 모델이 포함될 때 카탈로그에서 // 누락되는 버그(issue #268)를 수정. "openai" provider는 forward-auth(Codex login // passthrough)이므로 fetchProviderModels가 항상 []를 반환하고, native slugs는 // 별도 정적 경로(nativeOpenAiSlugs)로만 노출됨. 따라서 memberByKey에 // openai/ 키가 존재하지 않아 콤보가 조용히 drop됨. // - 기존 구현 및 제약 조건: memberByKey는 routed provider /models fetch 결과로만 구성. // - 검토한 주요 대안: (A) native slugs를 all 배열에 직접 push — /v1/models와 온디스크 // 카탈로그에서 native 모델이 중복 노출되는 부작용 발생. (B) memberByKey에만 synthetic // CatalogModel을 주입 — 콤보 멤버 해석에만 사용하고 all에는 추가하지 않으므로 기존 // 노출 경로에 영향 없음. // - 선택한 방식: (B) — synthetic entries를 memberByKey에만 주입. // - 다른 대안 대신 이 방식을 선택한 이유: 기존 native 모델 노출 경로(/v1/models, 온디스크 // 카탈로그 sync, management API)를 전혀 변경하지 않고 콤보 resolution만 수선하기 때문. // - 장점, 단점 및 영향: 장점 — 최소 수정, 기존 경로 무변경. 단점 — synthetic entries의 // capability 데이터가 static/upstream snapshot 기반이므로, 사용자가 커스텀 config // 힌트(modelContextWindows 등)로 native 모델의 context window를 오버라이드한 경우 // 반영되지 않음. 하지만 nativeOpenAiContextWindow가 이미 config 오버라이드를 // 우선시하므로 실제 충돌 가능성은 낮음. if (!hasComboTargets(config)) { // Skip the native slug injection entirely when no combos are configured — avoids // calling nativeOpenAiSlugs() (which reads the live Codex catalog from disk) for // configs that will never need it. } else { const disabled = disabledNativeSlugs(config); const openaiContextCap = nativeContextLimits(config); const requiredNativeComboTargets = new Set(listComboIds(config).flatMap(id => { const combo = getCombo(config, id); return combo?.targets.flatMap(target => ( target.provider === "openai" ? [target.model] : [] )) ?? []; })); for (const slug of nativeOpenAiSlugs()) { // 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, openaiContextCap); if (contextWindow === undefined) continue; const synthetic: CatalogModel = { provider: "openai", id: slug, owned_by: "openai", contextWindow, // Input limit, not the total window. These coincide for native GPT-5.6 today (the // advertised 922,000 window is already capped at its measured ceiling), but the two // 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), ...(nativeParallelToolCalls(slug) ? { parallelToolCalls: true } : {}), }; const key = `openai/${slug}`; // Only inject when not already present from a routed provider (an API-key // "openai" provider could shadow the native one). if (!memberByKey.has(key)) memberByKey.set(key, synthetic); } } // Enriched (registry-hydrated) provider clones — shared by combo member synthesis and // custom-model vision-sidecar inheritance so both see the same merged registry view. const enrichedByName = new Map(activeProviders.map(provider => [provider.name, provider.provider])); for (const id of listComboIds(config)) { const combo = getCombo(config, id); if (!combo) continue; const comboNativeLimits = nativeContextLimits(config); const nativeContextWindow = combo.nativeAlias && combo.alias ? nativeOpenAiContextWindow(combo.alias, comboNativeLimits) : undefined; const nativeAliasMaxInput = combo.nativeAlias && combo.alias ? (combo.alias.startsWith("gpt-5.6-") || combo.alias.includes("daybreak") ? NATIVE_GPT56_MAX_INPUT_TOKENS : nativeOpenAiMaxInputTokens(combo.alias, comboNativeLimits) ?? nativeOpenAiContextWindow(combo.alias, comboNativeLimits)) : undefined; const nativeAliasAutoCompact = combo.nativeAlias && combo.alias ? nativeOpenAiAutoCompactTokenLimit(combo.alias, comboNativeLimits) : undefined; const nativeAliasFallback = combo.nativeAlias && combo.alias && nativeContextWindow !== undefined ? { 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), } : undefined; const members = combo.targets .map(target => resolveComboCatalogMember( target, memberByKey, enrichedByName, providerContextCap(config, target.provider), nativeAliasFallback, metadataModelIdCaseFoldByProvider.get(target.provider), )) .filter((member): member is CatalogModel => member !== undefined); const derived = deriveComboCatalogModel(id, combo, members); if (derived) { const nativeDefault = combo.nativeAlias && combo.alias ? nativeDefaultReasoningEffort(combo.alias) : undefined; if (combo.defaultEffort === null && nativeDefault && derived.reasoningEfforts?.includes(nativeDefault)) { derived.defaultReasoningEffort = nativeDefault; } all.push(derived); } else warnUncataloguedComboOnce(id, combo, members, localOmissions); } replaceLastComboCatalogOmissions(localOmissions); all.sort((a, b) => (a.provider === b.provider ? a.id.localeCompare(b.id) : a.provider.localeCompare(b.provider))); // Provider-derived rows keyed by their Codex-facing slug: a custom override replaces the row // with the same slug below, so that row's provider capability metadata is the inheritance source. const replacedByRoutedSlug = new Map(all.map(model => [routedSlug(model.provider, model.id), model])); const customModels = (config.customModels ?? []).map(cm => { const rawProvider = config.providers[cm.provider]; const effectiveProvider = enrichedByName.get(cm.provider) ?? rawProvider; // Registry routing backfills an omitted authMode on the built-in OpenAI provider to // forward. Keep the catalog projection on the same contract while still failing closed // for every explicit non-forward mode and every non-canonical endpoint. const providerForCanonicalCheck = rawProvider ? withCanonicalOpenAiForwardAuthDefault(cm.provider, rawProvider) : undefined; const codexForwardNativeCapabilityAlias = cm.provider === OPENAI_CODEX_PROVIDER_ID && providerForCanonicalCheck !== undefined && isCanonicalOpenAiForwardProvider(providerForCanonicalCheck) && hasNativeOpenAiCapabilityMetadata(cm.modelId); const customNativeLimits = { ...nativeContextLimits(config), ...(typeof cm.contextWindow === "number" && cm.contextWindow > 0 ? { modelWindows: { ...(nativeContextLimits(config).modelWindows ?? {}), [cm.modelId]: cm.contextWindow } } : {}), }; const nativeAliasContextWindow = codexForwardNativeCapabilityAlias ? nativeOpenAiContextWindow(cm.modelId, customNativeLimits) : undefined; const customContextWindow = cm.contextWindow ? nativeAliasContextWindow !== undefined ? nativeAliasContextWindow : cm.contextWindow : nativeAliasContextWindow; const nativeAliasMaxInputTokens = codexForwardNativeCapabilityAlias ? nativeOpenAiMaxInputTokens(cm.modelId, customNativeLimits) : undefined; const nativeAliasMaxOutputTokens = codexForwardNativeCapabilityAlias ? nativeOpenAiMaxOutputTokens(cm.modelId) : undefined; const configuredMaxInput = rawProvider ? configuredMaxInputTokens(rawProvider, cm.modelId) : undefined; const hardMaxCandidates = [nativeAliasMaxInputTokens, configuredMaxInput] .filter((value): value is number => typeof value === "number" && value > 0); const customMaxInputTokens = hardMaxCandidates.length > 0 ? Math.min( ...hardMaxCandidates, ...(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) : customContextWindow !== undefined && configuredAutoCompact !== undefined ? clampAutoCompactTokenLimit(customContextWindow, customMaxInputTokens, configuredAutoCompact) : undefined; const nativeAliasDefaultEffort = codexForwardNativeCapabilityAlias ? nativeDefaultReasoningEffort(cm.modelId) : undefined; const supportsReasoningSummaries = configuredReasoningSummarySupport(rawProvider, cm.modelId); const fastPolicy = effectiveProvider ? fastPolicyForModel(effectiveProvider, cm.modelId, cm.provider) : undefined; const supportsServiceTier = fastPolicy ? serviceTierSupportFromPolicy(fastPolicy) : undefined; const base: CatalogModel = { id: cm.modelId, provider: cm.provider, catalogKind: CODEX_CUSTOM_MODEL_CATALOG_KIND, // Display-only label: never feeds routing (customModels are keyed by routedSlug below). ...(cm.displayName ? { displayName: cm.displayName } : codexForwardNativeCapabilityAlias ? { displayName: nativeOpenAiCapabilityDisplayName(cm.modelId) ?? cm.modelId } : {}), ...(customContextWindow !== undefined ? { contextWindow: customContextWindow } : {}), ...(customMaxInputTokens !== undefined ? { maxInputTokens: customMaxInputTokens } : {}), ...(customMaxOutputTokens !== undefined ? { maxOutputTokens: customMaxOutputTokens } : {}), ...(customAutoCompactTokenLimit !== undefined ? { autoCompactTokenLimit: customAutoCompactTokenLimit } : {}), ...(cm.inputModalities ? { inputModalities: cm.inputModalities } : codexForwardNativeCapabilityAlias ? { inputModalities: nativeInputModalities(cm.modelId) } : {}), ...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}), // Native-alias defaults apply only where the custom row declares nothing: the explicit // spreads below must win (later in object order), so a stored `[]` stays empty and a // declared ladder is narrowed to proven native capabilities after the merge below. ...(codexForwardNativeCapabilityAlias ? { codexForwardNativeCapabilityAlias: true, parallelToolCalls: nativeParallelToolCalls(cm.modelId), ...(Array.isArray(cm.reasoningEfforts) ? {} : { reasoningEfforts: nativeReasoningEfforts(cm.modelId), ...(nativeAliasDefaultEffort ? { defaultReasoningEffort: nativeAliasDefaultEffort } : {}), }), } : {}), // Explicit custom-row ladder wins over the inherited provider row below: the merge only // gap-fills, so a stored `[]` (explicit "no reasoning") or a declared ladder is kept // instead of being replaced by that row's metadata. Only proven native aliases are // bounded against their own capability source after the merge. ...(Array.isArray(cm.reasoningEfforts) ? { reasoningEfforts: [...cm.reasoningEfforts] } : {}), ...(cm.defaultReasoningEffort ? { defaultReasoningEffort: cm.defaultReasoningEffort } : {}), ...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}), ...(supportsServiceTier === true && fastPolicy?.fastTierDescription !== undefined ? { fastTierDescription: fastPolicy.fastTierDescription } : {}), ...(cm.codexToolMode !== undefined ? { codexToolMode: cm.codexToolMode } : effectiveProvider?.codexToolMode !== undefined ? { codexToolMode: effectiveProvider.codexToolMode } : {}), }; // #962: the dedupe below drops the provider-derived row this custom row replaces. Inherit that // row's provider capability metadata (reasoning ladder, default effort, parallel tool calls, // context, ...) so the generated catalog keeps advertising what the router actually provides. // Explicit custom fields win by construction; this only fills gaps. Without it a // noReasoningModels model loses its empty ladder and the catalog synthesizes the generic one, // which Codex then rejects for spawn_agent with effort "none". const replaced = replacedByRoutedSlug.get(routedSlug(cm.provider, cm.modelId)); // The final ladder is what the catalog will advertise; the inherited default only rides // along when it is actually a member — otherwise a provider default like "xhigh" would // re-apply onto a narrower custom ladder and override the fallback in applyReasoningLevels. const effectiveLadder = base.reasoningEfforts ?? replaced?.reasoningEfforts; const mergedMaxInputCandidates = [base.maxInputTokens, replaced?.maxInputTokens] .filter((value): value is number => typeof value === "number" && value > 0); 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 } : {}), ...(base.inputModalities === undefined && replaced.inputModalities !== undefined ? { inputModalities: replaced.inputModalities } : {}), ...(base.reasoningEfforts === undefined && replaced.reasoningEfforts !== undefined ? { reasoningEfforts: replaced.reasoningEfforts } : {}), ...(base.defaultReasoningEffort === undefined && replaced.defaultReasoningEffort !== undefined && Array.isArray(effectiveLadder) && effectiveLadder.includes(replaced.defaultReasoningEffort) ? { defaultReasoningEffort: replaced.defaultReasoningEffort } : {}), ...(base.parallelToolCalls === undefined && replaced.parallelToolCalls !== undefined ? { parallelToolCalls: replaced.parallelToolCalls } : {}), ...(base.supportsVerbosity === undefined && replaced.supportsVerbosity !== undefined ? { supportsVerbosity: replaced.supportsVerbosity } : {}), ...(base.supportsReasoningSummaries === undefined && replaced.supportsReasoningSummaries !== undefined ? { supportsReasoningSummaries: replaced.supportsReasoningSummaries } : {}), ...(base.codexToolMode === undefined && replaced.codexToolMode !== undefined ? { codexToolMode: replaced.codexToolMode } : {}), ...(base.capabilities === undefined && replaced.capabilities !== undefined ? { capabilities: replaced.capabilities } : {}), } : base; const reasoningBounded = codexForwardNativeCapabilityAlias ? boundCustomNativeReasoning(merged, nativeReasoningEfforts(cm.modelId), nativeAliasDefaultEffort) : merged; // Vision-sidecar coverage only: when the enriched provider's shared predicate matches // noVisionModels or text-without-image modelInputModalities, advertise image input so the // Codex app lets images reach the sidecar (#349/#344). Deliberately NOT the full // applyProviderConfigHints pass — custom rows are a // user override, so their explicit contextWindow / inputModalities / reasoning fields must be // preserved verbatim (the hint pass would cap context and overwrite modalities from registry). const mergedContext = typeof reasoningBounded.contextWindow === "number" && reasoningBounded.contextWindow > 0 ? reasoningBounded.contextWindow : undefined; const boundedMergedMaxInput = typeof reasoningBounded.maxInputTokens === "number" && reasoningBounded.maxInputTokens > 0 ? (mergedContext !== undefined ? Math.min(reasoningBounded.maxInputTokens, mergedContext) : reasoningBounded.maxInputTokens) : undefined; const mergedWithHardBounds = boundedMergedMaxInput !== undefined && boundedMergedMaxInput !== reasoningBounded.maxInputTokens ? { ...reasoningBounded, maxInputTokens: boundedMergedMaxInput } : reasoningBounded; const mergedSoftCandidates = [mergedWithHardBounds.autoCompactTokenLimit, configuredAutoCompact] .filter((value): value is number => typeof value === "number" && value > 0); const mergedWithAutoCompact: CatalogModel = mergedContext !== undefined && mergedSoftCandidates.length > 0 ? { ...mergedWithHardBounds, autoCompactTokenLimit: clampAutoCompactTokenLimit( mergedContext, boundedMergedMaxInput, Math.min(...mergedSoftCandidates), ), } : mergedWithHardBounds; const enrichedProvider = enrichedByName.get(cm.provider) ?? rawProvider; // Reuse the request-time consumer predicate so custom rows cannot drift from catalog hints. if (enrichedProvider && isModelVisionSidecarConsumer(enrichedProvider, mergedWithAutoCompact.id)) { const current = mergedWithAutoCompact.inputModalities ?? ["text"]; if (!current.includes("image")) { return { ...mergedWithAutoCompact, inputModalities: [...current, "image"] }; } } return mergedWithAutoCompact; }); // Custom rows override discovered rows that encode to the same Codex-facing slug. const customKeys = new Set(customModels.map(c => routedSlug(c.provider, c.id))); const deduped = all.filter(m => !customKeys.has(routedSlug(m.provider, m.id))); const models = [...deduped, ...customModels]; // ponytail: catalog-scale scan; index ids by provider if catalog growth makes this measurable. const aliasDisplayNames = new Map(activeProviders.flatMap(({ name, provider }) => { const providerModels = models.filter(model => model.provider === name); const aliases = [...effectiveModelAliases(config, provider, providerModels.map(model => model.id))]; return aliases.flatMap(([id, { alias }]) => { const exact = providerModels.filter(model => model.id === id); const matches = exact.length > 0 ? exact : providerModels.filter(model => model.id.toLowerCase() === id.toLowerCase()); return matches.length === 1 ? [[`${name}/${matches[0]!.id}`, `${provider.alias || name}/${alias}`] as const] : []; }); })); const providerModelOutcomes = providerResults.map(result => ( result.outcome.provider === OPENAI_API_PROVIDER_ID && capture.openAiApiPolicy.state === "captured" && capture.openAiApiPolicy.models !== undefined ? { provider: result.outcome.provider, state: "authoritative" as const } : result.outcome )); return { models: models.map(model => { const displayName = aliasDisplayNames.get(`${model.provider}/${model.id}`); return displayName && !model.displayName ? { ...model, displayName } : model; }), comboOmissions: localOmissions, providerAuthOutcomes: localProviderAuthOutcomes, providerModelOutcomes, discoveryPolicySnapshots: capture.discoveryPolicySnapshots, }; } export function augmentRoutedModelsWithRegistryOpenAiApiRows( models: CatalogModel[], config: OcxConfig, ): CatalogModel[] { const configured = config.providers[OPENAI_API_PROVIDER_ID]; if (!configured || configured.disabled === true || !providerMatchesRegistryTransport(OPENAI_API_PROVIDER_ID, configured)) return models; return augmentRoutedModelsWithCapturedOpenAiApiRows( models, config, captureTrustedOpenAiApiPolicy(OPENAI_API_PROVIDER_ID, true), ); } function augmentRoutedModelsWithCapturedOpenAiApiRows( models: CatalogModel[], config: OcxConfig, policy: CatalogTrustedOpenAiApiPolicySnapshot, ): CatalogModel[] { if (policy.state !== "captured" || !policy.models) return models; const configured = config.providers[OPENAI_API_PROVIDER_ID]; if (!configured || configured.disabled === true) return models; const existingById = new Map( models.filter(model => model.provider === OPENAI_API_PROVIDER_ID).map(model => [model.id, model]), ); const trustedRows = policy.models.map((id): CatalogModel => { const officialContext = policy.modelContextWindows?.[id]; const officialMaxInput = policy.modelMaxInputTokens?.[id]; const userContext = configured.modelContextWindows?.[id] ?? configured.contextWindow; const userMaxInput = configured.modelMaxInputTokens?.[id]; const providerCap = providerContextCap(config, OPENAI_API_PROVIDER_ID); const contextWindow = typeof officialContext === "number" ? Math.min(officialContext, userContext ?? officialContext, providerCap ?? officialContext) : undefined; const maxInputTokens = typeof officialMaxInput === "number" ? Math.min( officialMaxInput, userMaxInput ?? officialMaxInput, contextWindow ?? officialMaxInput, ) : undefined; const configuredAutoCompact = configuredAutoCompactTokenLimit(configured, id); const autoCompactTokenLimit = contextWindow !== undefined && configuredAutoCompact !== undefined ? clampAutoCompactTokenLimit(contextWindow, maxInputTokens, configuredAutoCompact) : undefined; const maxOutputTokens = routedMaxOutputTokens( OPENAI_API_PROVIDER_ID, configured, policy.modelMaxOutputTokens?.[id] !== undefined ? { provider: OPENAI_API_PROVIDER_ID, id, maxOutputTokens: policy.modelMaxOutputTokens[id] } : 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]!] } : {}), }; }); for (const trusted of trustedRows) { const live = existingById.get(trusted.id); if (!live) continue; const liveSignature = normalizedOpenAiApiSignature(live); const trustedSignature = normalizedOpenAiApiSignature(trusted); if (liveSignature === trustedSignature) continue; const warningKey = `${trusted.provider}/${trusted.id}\n${liveSignature}\n${trustedSignature}`; if (openAiApiCollisionWarnings.has(warningKey)) continue; openAiApiCollisionWarnings.add(warningKey); console.warn(`[opencodex] replacing conflicting live OpenAI API metadata for ${trusted.provider}/${trusted.id} with trusted registry metadata`); } return [ ...models.filter(model => model.provider !== OPENAI_API_PROVIDER_ID), ...trustedRows, ]; } export function augmentRoutedModelsWithMetadata( models: CatalogModel[], providerNames: string[], providers?: Record, caps?: Pick, metadataModelIdCaseFoldByProvider?: ReadonlyMap, ): CatalogModel[] { const out = [...models]; const seen = new Set(out.map(m => `${m.provider}/${m.id}`)); for (const provider of providerNames) { if (!JAWCODE_CATALOG_AUGMENT_PROVIDERS.has(provider)) continue; if (providers?.[provider]?.liveModels === false) continue; const jawcodeProvider = resolveMetadataProvider(provider); if (!jawcodeProvider) continue; for (const meta of listModelMetadata(jawcodeProvider)) { const key = `${provider}/${meta.id}`; if (seen.has(key)) continue; seen.add(key); const contextCap = caps ? providerContextCap(caps, provider) : undefined; const model: CatalogModel = { provider, 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, metadataModelIdCaseFoldByProvider?.get(provider), ) : {}), }); } } return out; }