@@ -303,6 +303,20 @@ type RawEntry = Record<string, unknown>;
303303type RawCatalog = { models ?: RawEntry [ ] ; [ k : string ] : unknown } ;
304304const JAWCODE_CATALOG_AUGMENT_PROVIDERS = new Set ( [ "opencode-go" ] ) ;
305305
306+ /**
307+ * Exact provider/model pairs whose discovery endpoint advertises them but whose inference backend
308+ * rejects them. Apply this after live/static/metadata sources converge so no source can resurrect
309+ * an uncallable picker row. Remove an entry once authenticated inference proves it usable again.
310+ */
311+ const ROUTED_MODEL_COMPATIBILITY_EXCLUSIONS = new Set ( [
312+ // Issue #82: Zen Go /models advertises HY3, but Console Go rejects it as outside the lite list.
313+ "opencode-go/hy3-preview" ,
314+ ] ) ;
315+
316+ function isRoutedModelCompatibilityExcluded ( slug : string ) : boolean {
317+ return ROUTED_MODEL_COMPATIBILITY_EXCLUSIONS . has ( slug ) ;
318+ }
319+
306320/**
307321 * Image/video GENERATION model families. opencodex routes chat/coding models into Codex; media-
308322 * generation models (Grok image/video, DALL·E, Imagen, Sora, Veo, …) are useless to a coding agent
@@ -328,6 +342,7 @@ export function isMediaGenerationModelId(id: string): boolean {
328342}
329343
330344function shouldExposeRoutedModel ( model : CatalogModel ) : boolean {
345+ if ( isRoutedModelCompatibilityExcluded ( `${ model . provider } /${ model . id } ` ) ) return false ;
331346 if ( model . provider === "cursor" && model . id === "gemini-3-pro-image-preview" ) return true ;
332347 return ! isMediaGenerationModelId ( model . id ) ;
333348}
@@ -1329,9 +1344,10 @@ export function mergeCatalogEntriesForSync(
13291344 }
13301345
13311346 let finalRoutedEntries = routedEntries ;
1332- if ( routedEntries . length === 0 && catalogModels . some ( m => typeof m . slug === "string" && ( m . slug as string ) . includes ( "/" ) ) ) {
1347+ const preservingExistingRouted = routedEntries . length === 0
1348+ && catalogModels . some ( m => typeof m . slug === "string" && ( m . slug as string ) . includes ( "/" ) ) ;
1349+ if ( preservingExistingRouted ) {
13331350 finalRoutedEntries = catalogModels . filter ( m => typeof m . slug === "string" && ( m . slug as string ) . includes ( "/" ) ) ;
1334- console . warn ( `[opencodex] catalog sync: routed model fetch returned empty; preserving ${ finalRoutedEntries . length } existing routed entr${ finalRoutedEntries . length === 1 ? "y" : "ies" } on disk.` ) ;
13351351 } else {
13361352 const freshSlugs = new Set ( routedEntries . flatMap ( entry => typeof entry . slug === "string" ? [ entry . slug ] : [ ] ) ) ;
13371353 const preservedForeignRouted = catalogModels . filter ( m => {
@@ -1341,6 +1357,14 @@ export function mergeCatalogEntriesForSync(
13411357 } ) ;
13421358 finalRoutedEntries = [ ...routedEntries , ...preservedForeignRouted ] ;
13431359 }
1360+ // Reapply final catalog policy to rows preserved from disk. Those rows bypass
1361+ // gatherRoutedModels, so filtering only the freshly gathered list can resurrect an excluded id.
1362+ finalRoutedEntries = finalRoutedEntries . filter ( entry =>
1363+ typeof entry . slug !== "string" || ! isRoutedModelCompatibilityExcluded ( entry . slug )
1364+ ) ;
1365+ if ( preservingExistingRouted ) {
1366+ console . warn ( `[opencodex] catalog sync: routed model fetch returned empty; preserving ${ finalRoutedEntries . length } existing routed entr${ finalRoutedEntries . length === 1 ? "y" : "ies" } on disk.` ) ;
1367+ }
13441368
13451369 const mergedEntries = [ ...native , ...finalRoutedEntries ] . map ( m => {
13461370 const normalized = normalizeServiceTiers ( m ) ;
0 commit comments