@@ -22,11 +22,89 @@ use super::anthropic::AnthropicAdapter;
2222use super :: ollama:: OllamaAdapter ;
2323use super :: openai:: OpenAIAdapter ;
2424
25+ /// Builds the alias → (provider_name, ModelConfig) map respecting the configured
26+ /// `LLMSelectionStrategy`.
27+ ///
28+ /// When multiple enabled providers define the same alias, the strategy decides which
29+ /// entry wins rather than the last-write-wins behaviour of a plain sequential insert:
30+ ///
31+ /// | Strategy | Wins when … |
32+ /// |--------------------|---------------------------------------------------------------------|
33+ /// | `PreferLocal` | New provider `is_local()` and existing entry is not local |
34+ /// | `PreferCloud` | New provider is not local and existing entry is local |
35+ /// | `CostOptimized` | New model's `cost_per_1k_tokens` < existing model's cost |
36+ /// | `LatencyOptimized` | Same as `PreferLocal` — local inference always has lower latency |
37+ ///
38+ /// Providers that have been disabled via `enabled: false` are skipped entirely.
39+ fn build_alias_map (
40+ provider_configs : & [ LLMProviderConfig ] ,
41+ strategy : & LLMSelectionStrategy ,
42+ ) -> HashMap < String , ( String , ModelConfig ) > {
43+ // Intermediate map also tracks is_local for the current winner so we can
44+ // apply the strategy without re-looking up the provider config later.
45+ // (alias -> (provider_name, is_local, model_config))
46+ let mut working: HashMap < String , ( String , bool , ModelConfig ) > = HashMap :: new ( ) ;
47+
48+ for provider_config in provider_configs {
49+ if !provider_config. enabled {
50+ continue ;
51+ }
52+
53+ let new_is_local = provider_config. is_local ( ) ;
54+
55+ for model_config in & provider_config. models {
56+ let alias = & model_config. alias ;
57+
58+ let should_insert = match working. get ( alias) {
59+ None => true ,
60+ Some ( ( _, existing_is_local, existing_model) ) => match strategy {
61+ LLMSelectionStrategy :: PreferLocal | LLMSelectionStrategy :: LatencyOptimized => {
62+ // Overwrite only if the new provider is local and the existing is not.
63+ new_is_local && !existing_is_local
64+ }
65+ LLMSelectionStrategy :: PreferCloud => {
66+ // Overwrite only if the new provider is cloud and the existing is local.
67+ !new_is_local && * existing_is_local
68+ }
69+ LLMSelectionStrategy :: CostOptimized => {
70+ model_config. cost_per_1k_tokens < existing_model. cost_per_1k_tokens
71+ }
72+ } ,
73+ } ;
74+
75+ if should_insert {
76+ info ! (
77+ "Mapping alias '{}' -> {} ({}) [strategy={:?}]" ,
78+ alias, model_config. model, provider_config. name, strategy
79+ ) ;
80+ working. insert (
81+ alias. clone ( ) ,
82+ (
83+ provider_config. name . clone ( ) ,
84+ new_is_local,
85+ model_config. clone ( ) ,
86+ ) ,
87+ ) ;
88+ } else {
89+ info ! (
90+ "Alias '{}' already mapped to a preferred provider, skipping {} ({}) [strategy={:?}]" ,
91+ alias, model_config. model, provider_config. name, strategy
92+ ) ;
93+ }
94+ }
95+ }
96+
97+ // Strip the is_local bookkeeping field — callers only need (provider_name, ModelConfig).
98+ working
99+ . into_iter ( )
100+ . map ( |( alias, ( provider_name, _, model_config) ) | ( alias, ( provider_name, model_config) ) )
101+ . collect ( )
102+ }
103+
25104/// Registry for managing LLM providers and resolving model aliases
26105pub struct ProviderRegistry {
27106 providers : HashMap < String , Arc < dyn LLMProvider > > ,
28107 alias_map : HashMap < String , ( String , ModelConfig ) > , // alias -> (provider_name, model_config)
29- _selection_strategy : LLMSelectionStrategy ,
30108 fallback_provider : Option < String > ,
31109 max_retries : u32 ,
32110 retry_delay_ms : u64 ,
@@ -36,7 +114,6 @@ impl ProviderRegistry {
36114 /// Create provider registry from node configuration
37115 pub fn from_config ( config : & NodeConfigManifest ) -> anyhow:: Result < Self > {
38116 let mut providers = HashMap :: new ( ) ;
39- let mut alias_map = HashMap :: new ( ) ;
40117
41118 info ! ( "Initializing LLM provider registry" ) ;
42119
@@ -52,18 +129,6 @@ impl ProviderRegistry {
52129 match Self :: create_provider ( provider_config) {
53130 Ok ( provider) => {
54131 providers. insert ( provider_config. name . clone ( ) , provider) ;
55-
56- // Build alias mapping
57- for model_config in & provider_config. models {
58- info ! (
59- "Mapping alias '{}' -> {} ({})" ,
60- model_config. alias, model_config. model, provider_config. name
61- ) ;
62- alias_map. insert (
63- model_config. alias . clone ( ) ,
64- ( provider_config. name . clone ( ) , model_config. clone ( ) ) ,
65- ) ;
66- }
67132 }
68133 Err ( e) => {
69134 warn ! (
@@ -79,10 +144,14 @@ impl ProviderRegistry {
79144 warn ! ( "No LLM providers configured - semantic validation will not be available" ) ;
80145 }
81146
147+ let alias_map = build_alias_map (
148+ & config. spec . llm_providers ,
149+ & config. spec . llm_selection . strategy ,
150+ ) ;
151+
82152 Ok ( Self {
83153 providers,
84154 alias_map,
85- _selection_strategy : config. spec . llm_selection . strategy . clone ( ) ,
86155 fallback_provider : config. spec . llm_selection . fallback_provider . clone ( ) ,
87156 max_retries : config. spec . llm_selection . max_retries ,
88157 retry_delay_ms : config. spec . llm_selection . retry_delay_ms ,
0 commit comments