@@ -376,6 +376,10 @@ pub struct AppState {
376376 /// Model catalog registry for enriching API responses with model metadata.
377377 /// Loaded from embedded data at startup and optionally synced at runtime.
378378 pub model_catalog : catalog:: ModelCatalogRegistry ,
379+ /// In-memory cache of model lists fetched from static (config-file) providers.
380+ /// Warmed on startup and refreshed periodically to avoid per-request latency.
381+ pub static_models_cache :
382+ Arc < tokio:: sync:: RwLock < std:: collections:: HashMap < String , providers:: ModelsResponse > > > ,
379383}
380384
381385impl AppState {
@@ -1059,7 +1063,7 @@ impl AppState {
10591063 Arc :: new ( services:: ProviderMetricsService :: new ( ) )
10601064 } ;
10611065
1062- Ok ( Self {
1066+ let result = Ok ( Self {
10631067 http_client,
10641068 config : Arc :: new ( config) ,
10651069 db,
@@ -1096,7 +1100,19 @@ impl AppState {
10961100 default_org_id,
10971101 provider_metrics,
10981102 model_catalog,
1099- } )
1103+ static_models_cache : Arc :: new ( tokio:: sync:: RwLock :: new (
1104+ std:: collections:: HashMap :: new ( ) ,
1105+ ) ) ,
1106+ } ) ;
1107+
1108+ // Warm the static models cache so /v1/models is fast from the first request
1109+ if let Ok ( ref state) = result
1110+ && state. config . features . static_models_cache . enabled ( )
1111+ {
1112+ state. warm_static_models_cache ( ) . await ;
1113+ }
1114+
1115+ result
11001116 }
11011117
11021118 /// Ensure a default user exists for anonymous access when auth is disabled.
@@ -1816,6 +1832,49 @@ impl AppState {
18161832 }
18171833 }
18181834 }
1835+
1836+ /// Fetch model lists from all static (config-file) providers in parallel and
1837+ /// store them in `self.static_models_cache`. Failures for individual providers
1838+ /// are logged and skipped so one slow/broken provider cannot block the rest.
1839+ pub async fn warm_static_models_cache ( & self ) {
1840+ use futures:: future:: join_all;
1841+
1842+ let futures: Vec < _ > = self
1843+ . config
1844+ . providers
1845+ . iter ( )
1846+ . map ( |( name, cfg) | {
1847+ let name = name. to_owned ( ) ;
1848+ let http = self . http_client . clone ( ) ;
1849+ let cbs = self . circuit_breakers . clone ( ) ;
1850+ async move {
1851+ let result = providers:: list_models_for_config ( cfg, & name, & http, & cbs) . await ;
1852+ ( name, result)
1853+ }
1854+ } )
1855+ . collect ( ) ;
1856+
1857+ let results = join_all ( futures) . await ;
1858+
1859+ let mut cache = self . static_models_cache . write ( ) . await ;
1860+ cache. retain ( |name, _| self . config . providers . get ( name) . is_some ( ) ) ;
1861+ for ( name, result) in results {
1862+ match result {
1863+ Ok ( response) => {
1864+ cache. insert ( name, response) ;
1865+ }
1866+ Err ( e) => {
1867+ tracing:: warn!( provider = %name, error = %e, "Failed to fetch models for cache warm" ) ;
1868+ }
1869+ }
1870+ }
1871+ let total_models: usize = cache. values ( ) . map ( |r| r. data . len ( ) ) . sum ( ) ;
1872+ tracing:: info!(
1873+ providers = cache. len( ) ,
1874+ models = total_models,
1875+ "Static models cache warmed"
1876+ ) ;
1877+ }
18191878}
18201879
18211880#[ cfg( feature = "server" ) ]
0 commit comments