@@ -329,9 +329,11 @@ export function mergeCatalogEntriesForSync(
329329 multiAgentMode : MultiAgentMode = "default" ,
330330 exactComboSlugs : ReadonlySet < string > = new Set ( ) ,
331331 hasPhysicalComboProvider = false ,
332+ includeNativeOpenAi = true ,
332333) : RawEntry [ ] {
333334 const rank = new Map ( featured . map ( ( slug , i ) => [ slug , i ] as const ) ) ;
334- const native = catalogModels
335+ const native = includeNativeOpenAi
336+ ? catalogModels
335337 . filter ( m => typeof m . slug === "string"
336338 && ! ( m . slug as string ) . includes ( "/" )
337339 && m . owned_by !== COMBO_NAMESPACE
@@ -367,11 +369,14 @@ export function mergeCatalogEntriesForSync(
367369 // for subagent max spawns; wire-clamped to the model's real top rung).
368370 if ( ! isGpt56NativeSlug ( slug ) ) ensureUltraReasoningLevel ( preserved ) ;
369371 return preserved ;
370- } ) ;
372+ } )
373+ : [ ] ;
371374
372375 // Backfill any native OpenAI slug that the on-disk catalog is missing (e.g. gpt-5.5), so a
373376 // routed provider exposing the same id can never delete the native OpenAI/Codex base row.
377+ // Skip when no enabled canonical openai provider exists (#636) — bare gpt-* would 404.
374378 const nativeSlugs = new Set ( native . flatMap ( m => typeof m . slug === "string" ? [ m . slug ] : [ ] ) ) ;
379+ if ( includeNativeOpenAi ) {
375380 for ( const slug of nativeOpenAiSlugs ( ) ) {
376381 if ( nativeSlugs . has ( slug ) ) continue ;
377382 nativeSlugs . add ( slug ) ;
@@ -382,6 +387,7 @@ export function mergeCatalogEntriesForSync(
382387 : 9 ;
383388 native . push ( deriveEntry ( template ? JSON . parse ( JSON . stringify ( template ) ) : null , slug , "OpenAI native model (Codex OAuth passthrough)." , priority ) ) ;
384389 }
390+ }
385391
386392 const freshSlugs = new Set (
387393 routedEntries . flatMap ( entry => typeof entry . slug === "string" ? [ entry . slug ] : [ ] ) ,
@@ -497,7 +503,16 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{
497503 // native AND routed so the advertised flag matches the implemented endpoint (phase 120.4) and a
498504 // native template can never leak supports_websockets while the flag is off.
499505 const wsEnabled = websocketsEnabled ( config ) ;
500- catalog . models = mergeCatalogEntriesForSync ( catalog . models ?? [ ] , goEntries , baseline , featured , wsEnabled , goIds , template , disabledNativeSlugs ( config ) , gatheredProviderNames , multiAgentMode , exactComboSlugs , hasPhysicalComboProvider ) ;
506+ const enabledProviders = Object . entries ( config . providers ?? { } )
507+ . filter ( ( [ , prov ] ) => prov . disabled !== true ) ;
508+ const hasCanonicalOpenai = enabledProviders . some ( ( [ name , prov ] ) =>
509+ name === "openai" && isCanonicalOpenAiForwardProvider ( prov ) ,
510+ ) ;
511+ // #636: when the user only configured non-OpenAI providers (e.g. kimi), do not advertise
512+ // bare gpt-* rows that hard-404 via NoEnabledOpenAiProviderError. Keep natives when no
513+ // providers are configured yet (fresh install / catalog bootstrap tests).
514+ const includeNativeOpenAi = enabledProviders . length === 0 || hasCanonicalOpenai ;
515+ catalog . models = mergeCatalogEntriesForSync ( catalog . models ?? [ ] , goEntries , baseline , featured , wsEnabled , goIds , template , disabledNativeSlugs ( config ) , gatheredProviderNames , multiAgentMode , exactComboSlugs , hasPhysicalComboProvider , includeNativeOpenAi ) ;
501516 clampCatalogModelsToCodexSupport ( catalog . models ) ;
502517
503518 atomicWriteFile ( catalogPath , JSON . stringify ( catalog , null , 2 ) + "\n" ) ;
0 commit comments