@@ -36,9 +36,18 @@ const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 })
3636// Zod schema for validating ModelRecord structure from disk cache
3737const modelRecordSchema = z . record ( z . string ( ) , modelInfoSchema )
3838
39+ export interface ModelFetchResult {
40+ models : ModelRecord
41+ /**
42+ * True only when `models` came directly from a successful provider request.
43+ * Memory/disk cache hits and graceful-degradation fallbacks are never authoritative.
44+ */
45+ authoritative : boolean
46+ }
47+
3948// Track in-flight refresh requests to prevent concurrent API calls for the same provider
4049// This prevents race conditions where multiple calls might overwrite each other's results
41- const inFlightRefresh = new Map < RouterName , Promise < ModelRecord > > ( )
50+ const inFlightRefresh = new Map < RouterName , Promise < ModelFetchResult > > ( )
4251
4352async function writeModels ( router : RouterName , data : ModelRecord ) {
4453 const filename = `${ router } _models.json`
@@ -137,7 +146,7 @@ async function fetchModelsFromProvider(options: GetModelsOptions): Promise<Model
137146 * @param baseUrl - Optional base URL for the provider (currently used only for LiteLLM).
138147 * @returns The models from the cache or the fetched models.
139148 */
140- export const getModels = async ( options : GetModelsOptions ) : Promise < ModelRecord > => {
149+ export const getModelsWithMetadata = async ( options : GetModelsOptions ) : Promise < ModelFetchResult > => {
141150 const { provider } = options
142151 const refreshOnDiskCacheHit = "refreshOnDiskCacheHit" in options && options . refreshOnDiskCacheHit
143152 const hadMemoryModels = memoryCache . get < ModelRecord > ( provider ) != null
@@ -156,7 +165,7 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
156165 console . error ( `[getModels] Background refresh failed for ${ provider } :` , error )
157166 } )
158167 }
159- return models
168+ return { models, authoritative : false }
160169 }
161170 }
162171 models = await fetchModelsFromProvider ( options )
@@ -178,7 +187,7 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
178187 } )
179188 }
180189
181- return models
190+ return { models, authoritative : true }
182191 } catch ( error ) {
183192 // Log the error and re-throw it so the caller can handle it (e.g., show a UI message).
184193 console . error ( `[getModels] Failed to fetch models in modelCache for ${ provider } :` , error )
@@ -187,6 +196,9 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
187196 }
188197}
189198
199+ export const getModels = async ( options : GetModelsOptions ) : Promise < ModelRecord > =>
200+ ( await getModelsWithMetadata ( options ) ) . models
201+
190202/**
191203 * Force-refresh models from API, bypassing cache.
192204 * Uses atomic writes so cache remains available during refresh.
@@ -196,7 +208,7 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
196208 * @param options - Provider options for fetching models
197209 * @returns Fresh models from API, or existing cache if refresh yields worse data
198210 */
199- export const refreshModels = async ( options : GetModelsOptions ) : Promise < ModelRecord > => {
211+ export const refreshModelsWithMetadata = async ( options : GetModelsOptions ) : Promise < ModelFetchResult > => {
200212 const { provider } = options
201213
202214 // Check if there's already an in-flight refresh for this provider
@@ -208,7 +220,7 @@ export const refreshModels = async (options: GetModelsOptions): Promise<ModelRec
208220 }
209221
210222 // Create the refresh promise and track it
211- const refreshPromise = ( async ( ) : Promise < ModelRecord > => {
223+ const refreshPromise = ( async ( ) : Promise < ModelFetchResult > => {
212224 try {
213225 // Force fresh API fetch - skip getModelsFromCache() check
214226 const models = await fetchModelsFromProvider ( options )
@@ -226,9 +238,9 @@ export const refreshModels = async (options: GetModelsOptions): Promise<ModelRec
226238 existingCacheSize : existingCount ,
227239 } )
228240 if ( existingCount > 0 ) {
229- return existingCache !
241+ return { models : existingCache ! , authoritative : false }
230242 } else {
231- return { }
243+ return { models : { } , authoritative : false }
232244 }
233245 }
234246
@@ -240,11 +252,11 @@ export const refreshModels = async (options: GetModelsOptions): Promise<ModelRec
240252 console . error ( `[refreshModels] Error writing ${ provider } models to disk:` , err ) ,
241253 )
242254
243- return models
255+ return { models, authoritative : true }
244256 } catch ( error ) {
245257 // Log the error for debugging, then return existing cache if available (graceful degradation)
246258 console . error ( `[refreshModels] Failed to refresh ${ provider } models:` , error )
247- return getModelsFromCache ( provider ) || { }
259+ return { models : getModelsFromCache ( provider ) || { } , authoritative : false }
248260 } finally {
249261 // Always clean up the in-flight tracking
250262 inFlightRefresh . delete ( provider )
@@ -257,6 +269,9 @@ export const refreshModels = async (options: GetModelsOptions): Promise<ModelRec
257269 return refreshPromise
258270}
259271
272+ export const refreshModels = async ( options : GetModelsOptions ) : Promise < ModelRecord > =>
273+ ( await refreshModelsWithMetadata ( options ) ) . models
274+
260275/**
261276 * Initialize background model cache refresh.
262277 * Refreshes public provider caches without blocking or requiring auth.
@@ -292,16 +307,16 @@ export async function initializeModelCacheRefresh(): Promise<void> {
292307export const flushModels = async (
293308 options : GetModelsOptions ,
294309 refresh : boolean = false ,
295- cb ?: ( v : any ) => void ,
310+ cb ?: ( models : ModelRecord , metadata : ModelFetchResult ) => void ,
296311) : Promise < void > => {
297312 const { provider } = options
298313 if ( refresh ) {
299314 // Don't delete memory cache - let refreshModels atomically replace it
300315 // This prevents a race condition where getModels() might be called
301316 // before refresh completes, avoiding a gap in cache availability
302317 // Await the refresh to ensure the cache is updated before returning
303- await refreshModels ( options )
304- . then ( cb )
318+ await refreshModelsWithMetadata ( options )
319+ . then ( ( result ) => cb ?. ( result . models , result ) )
305320 . catch ( ( error ) => {
306321 console . log ( `[flushModels] Refresh failed for ${ provider } :` , error . message )
307322 } )
0 commit comments