@@ -788,6 +788,15 @@ struct OpenAiCompatDef {
788788 /// this to `false` so the static catalog is used without a noisy warning.
789789 /// Users can still override via `fetch_models = true` in config.
790790 supports_model_discovery : bool ,
791+ /// When `false`, a dummy API key (the provider name) is used if none is
792+ /// configured. Intended for local servers that don't authenticate.
793+ requires_api_key : bool ,
794+ /// Local-only providers (Ollama, LM Studio) are skipped unless the user
795+ /// has an explicit `[providers.<name>]` entry, a `_BASE_URL` env var, or
796+ /// configured models. This avoids probing localhost when nothing is running.
797+ /// Also ensures model discovery is always attempted (never short-circuited
798+ /// by the empty-catalog heuristic).
799+ local_only : bool ,
791800}
792801
793802const OPENAI_COMPAT_PROVIDERS : & [ OpenAiCompatDef ] = & [
@@ -798,6 +807,8 @@ const OPENAI_COMPAT_PROVIDERS: &[OpenAiCompatDef] = &[
798807 default_base_url : "https://api.mistral.ai/v1" ,
799808 models : MISTRAL_MODELS ,
800809 supports_model_discovery : true ,
810+ requires_api_key : true ,
811+ local_only : false ,
801812 } ,
802813 OpenAiCompatDef {
803814 config_name : "openrouter" ,
@@ -806,6 +817,8 @@ const OPENAI_COMPAT_PROVIDERS: &[OpenAiCompatDef] = &[
806817 default_base_url : "https://openrouter.ai/api/v1" ,
807818 models : & [ ] ,
808819 supports_model_discovery : true ,
820+ requires_api_key : true ,
821+ local_only : false ,
809822 } ,
810823 OpenAiCompatDef {
811824 config_name : "cerebras" ,
@@ -814,6 +827,8 @@ const OPENAI_COMPAT_PROVIDERS: &[OpenAiCompatDef] = &[
814827 default_base_url : "https://api.cerebras.ai/v1" ,
815828 models : CEREBRAS_MODELS ,
816829 supports_model_discovery : true ,
830+ requires_api_key : true ,
831+ local_only : false ,
817832 } ,
818833 OpenAiCompatDef {
819834 config_name : "minimax" ,
@@ -823,6 +838,8 @@ const OPENAI_COMPAT_PROVIDERS: &[OpenAiCompatDef] = &[
823838 models : MINIMAX_MODELS ,
824839 // MiniMax API does not expose a /models endpoint (returns 404).
825840 supports_model_discovery : false ,
841+ requires_api_key : true ,
842+ local_only : false ,
826843 } ,
827844 OpenAiCompatDef {
828845 config_name : "moonshot" ,
@@ -831,6 +848,8 @@ const OPENAI_COMPAT_PROVIDERS: &[OpenAiCompatDef] = &[
831848 default_base_url : "https://api.moonshot.ai/v1" ,
832849 models : MOONSHOT_MODELS ,
833850 supports_model_discovery : true ,
851+ requires_api_key : true ,
852+ local_only : false ,
834853 } ,
835854 OpenAiCompatDef {
836855 config_name : "zai" ,
@@ -839,6 +858,8 @@ const OPENAI_COMPAT_PROVIDERS: &[OpenAiCompatDef] = &[
839858 default_base_url : "https://api.z.ai/api/paas/v4" ,
840859 models : ZAI_MODELS ,
841860 supports_model_discovery : true ,
861+ requires_api_key : true ,
862+ local_only : false ,
842863 } ,
843864 OpenAiCompatDef {
844865 config_name : "venice" ,
@@ -847,6 +868,8 @@ const OPENAI_COMPAT_PROVIDERS: &[OpenAiCompatDef] = &[
847868 default_base_url : "https://api.venice.ai/api/v1" ,
848869 models : & [ ] ,
849870 supports_model_discovery : true ,
871+ requires_api_key : true ,
872+ local_only : false ,
850873 } ,
851874 OpenAiCompatDef {
852875 config_name : "deepseek" ,
@@ -855,6 +878,8 @@ const OPENAI_COMPAT_PROVIDERS: &[OpenAiCompatDef] = &[
855878 default_base_url : "https://api.deepseek.com" ,
856879 models : DEEPSEEK_MODELS ,
857880 supports_model_discovery : true ,
881+ requires_api_key : true ,
882+ local_only : false ,
858883 } ,
859884 OpenAiCompatDef {
860885 config_name : "ollama" ,
@@ -863,6 +888,18 @@ const OPENAI_COMPAT_PROVIDERS: &[OpenAiCompatDef] = &[
863888 default_base_url : "http://127.0.0.1:11434/v1" ,
864889 models : & [ ] ,
865890 supports_model_discovery : true ,
891+ requires_api_key : false ,
892+ local_only : true ,
893+ } ,
894+ OpenAiCompatDef {
895+ config_name : "lmstudio" ,
896+ env_key : "LMSTUDIO_API_KEY" ,
897+ env_base_url_key : "LMSTUDIO_BASE_URL" ,
898+ default_base_url : "http://127.0.0.1:1234/v1" ,
899+ models : & [ ] ,
900+ supports_model_discovery : true ,
901+ requires_api_key : false ,
902+ local_only : true ,
866903 } ,
867904 OpenAiCompatDef {
868905 config_name : "gemini" ,
@@ -871,6 +908,8 @@ const OPENAI_COMPAT_PROVIDERS: &[OpenAiCompatDef] = &[
871908 default_base_url : "https://generativelanguage.googleapis.com/v1beta/openai" ,
872909 models : GEMINI_MODELS ,
873910 supports_model_discovery : true ,
911+ requires_api_key : true ,
912+ local_only : false ,
874913 } ,
875914] ;
876915
@@ -1675,10 +1714,10 @@ impl ProviderRegistry {
16751714
16761715 let key = resolve_api_key ( config, def. config_name , def. env_key , env_overrides) ;
16771716
1678- // Ollama doesn 't require an API key — use a dummy value.
1717+ // Local providers don 't require an API key — use a dummy value.
16791718 // Gemini accepts both GEMINI_API_KEY and GOOGLE_API_KEY.
1680- let key = if def. config_name == "ollama" {
1681- key. or_else ( || Some ( secrecy:: Secret :: new ( "ollama" . into ( ) ) ) )
1719+ let key = if ! def. requires_api_key {
1720+ key. or_else ( || Some ( secrecy:: Secret :: new ( def . config_name . into ( ) ) ) )
16821721 } else if def. config_name == "gemini" {
16831722 key. or_else ( || env_value ( env_overrides, "GOOGLE_API_KEY" ) . map ( secrecy:: Secret :: new) )
16841723 } else {
@@ -1703,8 +1742,8 @@ impl ProviderRegistry {
17031742 . map ( |entry| entry. stream_transport )
17041743 . unwrap_or ( ProviderStreamTransport :: Sse ) ;
17051744 let preferred = configured_models_for_provider ( config, def. config_name ) ;
1706- if def. config_name == "ollama" {
1707- let has_explicit_entry = config. get ( "ollama" ) . is_some ( ) ;
1745+ if def. local_only {
1746+ let has_explicit_entry = config. get ( def . config_name ) . is_some ( ) ;
17081747 let has_env_base_url = env_value ( env_overrides, def. env_base_url_key ) . is_some ( ) ;
17091748 if !has_explicit_entry && !has_env_base_url && preferred. is_empty ( ) {
17101749 continue ;
@@ -1715,7 +1754,7 @@ impl ProviderRegistry {
17151754 // OpenRouter supports `/models`, so we discover dynamically.
17161755 let skip_discovery = def. models . is_empty ( )
17171756 && preferred. is_empty ( )
1718- && def. config_name != "ollama"
1757+ && ! def. local_only
17191758 && ( def. config_name == "venice" || cfg ! ( test) ) ;
17201759 // Respect `supports_model_discovery`: providers whose API lacks a
17211760 // /models endpoint (e.g. MiniMax) skip live fetch unless the user
0 commit comments