@@ -389,8 +389,92 @@ fn append_openai_routes(
389389 }
390390}
391391
392- /// Configured OpenAI-compatible profiles (NVIDIA NIM, Groq, ...), excluding
393- /// the active direct profile which contributes through the OpenRouter path.
392+ fn named_provider_profile_routes (
393+ profile_name : & str ,
394+ profile : & crate :: config:: NamedProviderConfig ,
395+ ) -> Vec < ModelRoute > {
396+ let mut models = profile
397+ . models
398+ . iter ( )
399+ . filter ( |model| {
400+ model. input . is_empty ( )
401+ || model
402+ . input
403+ . iter ( )
404+ . any ( |input| input. eq_ignore_ascii_case ( "text" ) )
405+ } )
406+ . map ( |model| model. id . trim ( ) . to_string ( ) )
407+ . filter ( |model| !model. is_empty ( ) )
408+ . collect :: < Vec < _ > > ( ) ;
409+
410+ let default_model = profile
411+ . default_model
412+ . as_deref ( )
413+ . map ( str:: trim)
414+ . filter ( |model| !model. is_empty ( ) ) ;
415+ if let Some ( default_model) = default_model {
416+ if !models. iter ( ) . any ( |model| model == default_model) {
417+ models. push ( default_model. to_string ( ) ) ;
418+ }
419+ }
420+
421+ let available = match profile. auth {
422+ crate :: config:: NamedProviderAuth :: None => true ,
423+ crate :: config:: NamedProviderAuth :: Bearer | crate :: config:: NamedProviderAuth :: Header => {
424+ profile
425+ . requires_api_key
426+ . map ( |requires_key| !requires_key)
427+ . unwrap_or ( false )
428+ || profile
429+ . api_key
430+ . as_deref ( )
431+ . map ( str:: trim)
432+ . is_some_and ( |key| !key. is_empty ( ) )
433+ || profile
434+ . api_key_env
435+ . as_deref ( )
436+ . map ( str:: trim)
437+ . filter ( |key_env| !key_env. is_empty ( ) )
438+ . is_some_and ( |key_env| {
439+ std:: env:: var_os ( key_env) . is_some ( )
440+ || profile
441+ . env_file
442+ . as_deref ( )
443+ . map ( str:: trim)
444+ . filter ( |env_file| !env_file. is_empty ( ) )
445+ . is_some_and ( |env_file| {
446+ crate :: provider_catalog:: load_env_value_from_env_or_config (
447+ key_env, env_file,
448+ )
449+ . is_some ( )
450+ } )
451+ } )
452+ }
453+ } ;
454+ let detail = if available {
455+ profile. base_url . clone ( )
456+ } else {
457+ "no credentials" . to_string ( )
458+ } ;
459+ let api_method = format ! ( "openai-compatible:{profile_name}" ) ;
460+
461+ models
462+ . into_iter ( )
463+ . filter ( |model| is_listable_model_name ( model) )
464+ . map ( |model| ModelRoute {
465+ model,
466+ provider : profile_name. to_string ( ) ,
467+ api_method : api_method. clone ( ) ,
468+ available,
469+ detail : detail. clone ( ) ,
470+ cheapness : None ,
471+ } )
472+ . collect ( )
473+ }
474+
475+ /// Configured OpenAI-compatible profiles (NVIDIA NIM, Groq, ...), plus custom
476+ /// `[providers.<name>]` profiles, excluding the active direct profile which
477+ /// contributes through the OpenRouter path.
394478/// Returns whether any routes were added.
395479fn append_openai_compatible_profile_routes (
396480 provider : & MultiProvider ,
@@ -423,6 +507,17 @@ fn append_openai_compatible_profile_routes(
423507 added_any |= !profile_routes. is_empty ( ) ;
424508 routes. extend ( profile_routes) ;
425509 }
510+
511+ for ( profile_name, profile) in & crate :: config:: config ( ) . providers {
512+ let api_method = format ! ( "openai-compatible:{profile_name}" ) ;
513+ if active_direct_openai_compatible_api_method. as_deref ( ) == Some ( api_method. as_str ( ) ) {
514+ continue ;
515+ }
516+
517+ let profile_routes = named_provider_profile_routes ( profile_name, profile) ;
518+ added_any |= !profile_routes. is_empty ( ) ;
519+ routes. extend ( profile_routes) ;
520+ }
426521 added_any
427522}
428523
@@ -1188,6 +1283,38 @@ mod tests {
11881283 }
11891284 }
11901285
1286+ #[ test]
1287+ fn named_provider_profile_routes_include_configured_text_models ( ) {
1288+ let profile = crate :: config:: NamedProviderConfig {
1289+ base_url : "https://llm.example.test/v1" . to_string ( ) ,
1290+ auth : crate :: config:: NamedProviderAuth :: None ,
1291+ default_model : Some ( "gateway/default-model" . to_string ( ) ) ,
1292+ models : vec ! [
1293+ crate :: config:: NamedProviderModelConfig {
1294+ id: "gateway/text-model" . to_string( ) ,
1295+ input: vec![ "text" . to_string( ) ] ,
1296+ ..Default :: default ( )
1297+ } ,
1298+ crate :: config:: NamedProviderModelConfig {
1299+ id: "gateway/image-only" . to_string( ) ,
1300+ input: vec![ "image" . to_string( ) ] ,
1301+ ..Default :: default ( )
1302+ } ,
1303+ ] ,
1304+ ..Default :: default ( )
1305+ } ;
1306+
1307+ let routes = named_provider_profile_routes ( "my-gateway" , & profile) ;
1308+ let models = routes. iter ( ) . map ( |route| route. model . as_str ( ) ) . collect :: < Vec < _ > > ( ) ;
1309+
1310+ assert_eq ! ( models, vec![ "gateway/text-model" , "gateway/default-model" ] ) ;
1311+ assert ! ( routes. iter( ) . all( |route| route. provider == "my-gateway" ) ) ;
1312+ assert ! ( routes
1313+ . iter( )
1314+ . all( |route| route. api_method == "openai-compatible:my-gateway" ) ) ;
1315+ assert ! ( routes. iter( ) . all( |route| route. available) ) ;
1316+ }
1317+
11911318 #[ test]
11921319 fn remote_compatible_route_uses_live_cache_and_does_not_mark_fallback ( ) {
11931320 let guard = EnvGuard :: new ( ) ;
0 commit comments