@@ -58,20 +58,68 @@ import upstreamModelsSnapshot from "../data/upstream-models.json";
5858import { JAWCODE_CATALOG_AUGMENT_PROVIDERS , catalogModelSlug , shouldExposeRoutedModel } from "./parsing" ;
5959import type { CatalogModel } from "./parsing" ;
6060import { disabledNativeSlugs , hasComboTargets , nativeInputModalities , nativeOpenAiContextWindow , nativeOpenAiSlugs , nativeParallelToolCalls , nativeReasoningEfforts } from "./metadata" ;
61- import { deriveComboCatalogModel , getLastComboCatalogOmissions , normalizedOpenAiApiSignature , openAiApiCollisionWarnings , replaceLastComboCatalogOmissions , warnUncataloguedComboOnce } from "./aggregation" ;
61+ import { deriveComboCatalogModel , normalizedOpenAiApiSignature , openAiApiCollisionWarnings , replaceLastComboCatalogOmissions , warnUncataloguedComboOnce } from "./aggregation" ;
6262import type { ComboCatalogOmission } from "./aggregation" ;
6363
64- /** Concurrent gatherRoutedModels callers with the same provider set share one live discovery.
65- * Keyed by gatherFlightKey so a different provider set cannot evict an in-flight gather. */
66- const gatherInflight = new Map < string , Promise < CatalogModel [ ] > > ( ) ;
64+ /** Concurrent gatherRoutedModels callers with the same catalog identity share one live discovery.
65+ * Keyed by gatherFlightKey so a different config cannot join or evict the wrong flight. */
66+ interface GatherFlightResult {
67+ models : CatalogModel [ ] ;
68+ comboOmissions : ComboCatalogOmission [ ] ;
69+ }
70+
71+ const gatherInflight = new Map < string , Promise < GatherFlightResult > > ( ) ;
72+
73+ function stableJson ( value : unknown ) : string {
74+ return JSON . stringify ( value , ( _key , nested ) => {
75+ if ( nested && typeof nested === "object" && ! Array . isArray ( nested ) ) {
76+ return Object . fromEntries ( Object . entries ( nested as Record < string , unknown > ) . sort ( ( [ a ] , [ b ] ) => a . localeCompare ( b ) ) ) ;
77+ }
78+ return nested ;
79+ } ) ;
80+ }
81+
82+ function providerCatalogFingerprint ( name : string , prov : OcxProviderConfig ) : Record < string , unknown > {
83+ return {
84+ n : name ,
85+ live : prov . liveModels !== false ,
86+ base : prov . baseUrl ?? "" ,
87+ adapter : prov . adapter ?? "" ,
88+ models : [ ...( prov . models ?? [ ] ) ] . sort ( ) ,
89+ defaultModel : prov . defaultModel ?? null ,
90+ ctx : prov . contextWindow ?? null ,
91+ ctxW : prov . modelContextWindows ?? null ,
92+ maxIn : prov . modelMaxInputTokens ?? null ,
93+ inMod : prov . modelInputModalities ?? null ,
94+ re : prov . modelReasoningEfforts ?? null ,
95+ defRe : prov . modelDefaultReasoningEfforts ?? null ,
96+ rsSum : prov . modelSupportsReasoningSummaries ?? null ,
97+ rsDel : prov . modelReasoningSummaryDelivery ?? null ,
98+ noVis : [ ...( prov . noVisionModels ?? [ ] ) ] . sort ( ) ,
99+ ptc : prov . parallelToolCalls ?? null ,
100+ gMode : prov . googleMode ?? null ,
101+ } ;
102+ }
67103
68104function gatherFlightKey ( config : OcxConfig ) : string {
69105 const providers = Object . entries ( config . providers )
70106 . filter ( ( [ , prov ] ) => prov . disabled !== true )
71- . map ( ( [ name , prov ] ) => `${ name } \0${ prov . liveModels === false ? "0" : "1" } \0${ prov . baseUrl ?? "" } ` )
72- . sort ( )
73- . join ( "\n" ) ;
74- return `${ providers } \n#${ config . modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS } ` ;
107+ . map ( ( [ name , prov ] ) => providerCatalogFingerprint ( name , prov ) )
108+ . sort ( ( a , b ) => String ( a . n ) . localeCompare ( String ( b . n ) ) ) ;
109+ const assembly = stableJson ( {
110+ providers,
111+ combos : config . combos ?? { } ,
112+ customModels : ( config . customModels ?? [ ] ) . map ( ( cm ) => ( {
113+ p : cm . provider ,
114+ m : cm . modelId ,
115+ d : cm . displayName ?? null ,
116+ cw : cm . contextWindow ?? null ,
117+ im : cm . inputModalities ?? null ,
118+ } ) ) ,
119+ caps : config . providerContextCaps ?? null ,
120+ } ) ;
121+ const digest = createHash ( "sha256" ) . update ( assembly ) . digest ( "hex" ) . slice ( 0 , 16 ) ;
122+ return `${ digest } #${ config . modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS } ` ;
75123}
76124
77125/** Drop in-flight gather so tests / full cache clears do not reuse a stale promise. */
@@ -594,27 +642,25 @@ export async function gatherRoutedModels(
594642 let promise = gatherInflight . get ( key ) ;
595643 if ( ! promise ) {
596644 // Claim the slot synchronously before any await so same-key callers join this flight.
597- // Distinct keys keep their own entries — a second provider set must not evict the first.
598- const flight = gatherRoutedModelsUncached ( config , options ) . finally ( ( ) => {
645+ // Distinct keys keep their own entries — a second config must not evict the first.
646+ const flight = gatherRoutedModelsUncached ( config ) . finally ( ( ) => {
599647 if ( gatherInflight . get ( key ) === flight ) gatherInflight . delete ( key ) ;
600648 } ) ;
601649 gatherInflight . set ( key , flight ) ;
602650 promise = flight ;
603651 }
604- const models = await promise ;
652+ const { models, comboOmissions } = await promise ;
605653 if ( options ?. comboOmissions ) {
606- const last = getLastComboCatalogOmissions ( ) ;
607654 options . comboOmissions . length = 0 ;
608- options . comboOmissions . push ( ...last ) ;
655+ options . comboOmissions . push ( ...comboOmissions ) ;
609656 }
610657 return models ;
611658}
612659
613660async function gatherRoutedModelsUncached (
614661 config : OcxConfig ,
615- options ?: { comboOmissions ?: ComboCatalogOmission [ ] } ,
616- ) : Promise < CatalogModel [ ] > {
617- // Per-invocation list: sync passes `comboOmissions` so overlapping gathers cannot race.
662+ ) : Promise < GatherFlightResult > {
663+ // Flight-local list: joiners copy from the resolved promise, not a process-global last write.
618664 const localOmissions : ComboCatalogOmission [ ] = [ ] ;
619665 const ttlMs = config . modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS ;
620666 // Persisted provider entries can predate newer registry fields (noVisionModels,
@@ -696,10 +742,6 @@ async function gatherRoutedModelsUncached(
696742 else warnUncataloguedComboOnce ( id , combo , members , localOmissions ) ;
697743 }
698744 replaceLastComboCatalogOmissions ( localOmissions ) ;
699- if ( options ?. comboOmissions ) {
700- options . comboOmissions . length = 0 ;
701- options . comboOmissions . push ( ...localOmissions ) ;
702- }
703745 all . sort ( ( a , b ) => ( a . provider === b . provider ? a . id . localeCompare ( b . id ) : a . provider . localeCompare ( b . provider ) ) ) ;
704746 // Enriched (registry-hydrated) provider clones, keyed by name — the same view used above so
705747 // custom rows get the same noVisionModels / inputModalities treatment as discovered rows.
@@ -733,7 +775,7 @@ async function gatherRoutedModelsUncached(
733775 // Custom rows override discovered rows that encode to the same Codex-facing slug.
734776 const customKeys = new Set ( customModels . map ( c => routedSlug ( c . provider , c . id ) ) ) ;
735777 const deduped = all . filter ( m => ! customKeys . has ( routedSlug ( m . provider , m . id ) ) ) ;
736- return [ ...deduped , ...customModels ] ;
778+ return { models : [ ...deduped , ...customModels ] , comboOmissions : localOmissions } ;
737779}
738780
739781export function augmentRoutedModelsWithRegistryOpenAiApiRows (
0 commit comments