1use std::collections::BTreeMap;
5
6use serde::Serialize;
7
8use super::*;
9
10#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
11pub struct ResolvedModel {
12 pub id: String,
13 pub provider: String,
14 pub alias: Option<String>,
15 pub tool_format: String,
16 pub tier: String,
17 pub family: String,
18 pub lineage: String,
19}
20
21#[derive(Debug, Clone, PartialEq)]
27pub struct ModelExecutionContract {
28 pub selector: String,
29 pub resolved: ResolvedModel,
30 pub wire_model: String,
31 pub generation_defaults: BTreeMap<String, toml::Value>,
32}
33
34pub fn resolve_model(alias: &str) -> (String, Option<String>) {
36 let config = effective_config();
37 if let Some(a) = config.aliases.get(alias) {
38 return (a.id.clone(), Some(a.provider.clone()));
39 }
40 (normalize_model_id_with_config(alias, &config), None)
41}
42
43pub fn normalize_model_id(raw: &str) -> String {
50 normalize_model_id_with_config(raw, &effective_config())
51}
52
53fn normalize_model_id_with_config(raw: &str, config: &ProvidersConfig) -> String {
54 for prefix in PROVIDER_SELECTOR_PREFIXES {
55 if let Some(stripped) = raw.strip_prefix(prefix) {
56 return stripped.to_string();
57 }
58 }
59 if let Some((provider, model)) = raw.split_once(':') {
60 if !model.is_empty() && (provider == "mock" || config.providers.contains_key(provider)) {
61 return model.to_string();
62 }
63 }
64 raw.to_string()
65}
66
67const PROVIDER_SELECTOR_PREFIXES: &[&str] =
68 &["ollama:", "local:", "huggingface:", "hf:", "cerebras/"];
69
70pub fn resolve_model_info(selector: &str) -> ResolvedModel {
73 let config = effective_config();
74 if let Some(alias) = config.aliases.get(selector) {
75 let id = alias.id.clone();
76 let provider = alias.provider.clone();
77 let requested = alias
78 .tool_format
79 .clone()
80 .unwrap_or_else(|| default_tool_format_with_config(&config, &id, &provider));
81 let tool_format = guard_tool_format(&provider, &id, &requested, Some(selector));
82 return ResolvedModel {
83 tier: model_tier_with_config(&config, &id),
84 family: model_family_with_config(&config, &provider, &id),
85 lineage: model_lineage_with_config(&config, &provider, &id),
86 id,
87 provider,
88 alias: Some(selector.to_string()),
89 tool_format,
90 };
91 }
92
93 let id = normalize_model_id_with_config(selector, &config);
94 let inference = infer_provider_with_config(&config, selector);
95 let source = inference.source;
96 let provider = inference.provider;
97 let requested = default_tool_format_with_config(&config, &id, &provider);
98 let tool_format = guard_tool_format(&provider, &id, &requested, None);
99 let tier = model_tier_with_config(&config, &id);
100 let family = model_family_with_inference_source(&config, &provider, &id, source);
101 let lineage = model_lineage_with_inference_source(&config, &provider, &id, source);
102 ResolvedModel {
103 id,
104 provider,
105 alias: None,
106 tool_format,
107 tier,
108 family,
109 lineage,
110 }
111}
112
113pub fn model_execution_contract(selector: &str) -> ModelExecutionContract {
116 let resolved = resolve_model_info(selector);
117 let wire_model = wire_model_id(&resolved.id);
118 let generation_defaults = generation_defaults_for_route(&resolved.provider, &resolved.id);
119 ModelExecutionContract {
120 selector: selector.to_string(),
121 resolved,
122 wire_model,
123 generation_defaults,
124 }
125}
126
127fn guard_tool_format(provider: &str, model: &str, requested: &str, alias: Option<&str>) -> String {
134 let decision = crate::llm::capabilities::validate_tool_format(provider, model, requested);
135 if let Some(reason) = &decision.correction {
136 tracing::warn!(
137 target: "harn::llm::tool_format",
138 alias = alias.unwrap_or(""),
139 "{reason}"
140 );
141 }
142 decision.effective
143}
144
145#[cfg(test)]
146mod tests {
147 use super::{normalize_model_id, resolve_model_info};
148
149 #[test]
150 fn registered_provider_selector_normalizes_to_native_model_id() {
151 let model = resolve_model_info("openai:o3");
152 assert_eq!(model.provider, "openai");
153 assert_eq!(model.id, "o3");
154 assert_eq!(normalize_model_id("mock:o3"), "o3");
155 assert_eq!(
156 normalize_model_id("ollama:qwen3.2:latest"),
157 "qwen3.2:latest",
158 "only the first selector colon is transport syntax"
159 );
160 }
161
162 #[test]
163 fn grok_code_aliases_resolve_through_live_resolver() {
164 for selector in ["grok-code", "grok-code-fast", "grok-code-fast-1"] {
165 let model = resolve_model_info(selector);
166 assert_eq!(
167 (
168 model.id.as_str(),
169 model.provider.as_str(),
170 model.alias.as_deref(),
171 model.tool_format.as_str(),
172 ),
173 ("grok-build-0.1", "xai", Some(selector), "native"),
174 "selector: {selector}",
175 );
176 }
177 }
178
179 #[test]
180 fn huggingface_qwen3_coder_aliases_resolve_through_live_resolver() {
181 for selector in ["huggingface-qwen3-coder", "hf-qwen3-coder"] {
182 let model = resolve_model_info(selector);
183 assert_eq!(
184 (
185 model.id.as_str(),
186 model.provider.as_str(),
187 model.alias.as_deref(),
188 model.tool_format.as_str(),
189 model.tier.as_str(),
190 ),
191 (
192 "Qwen/Qwen3-Coder-480B-A35B-Instruct",
193 "huggingface",
194 Some(selector),
195 "native",
196 "frontier",
197 ),
198 "selector: {selector}",
199 );
200 }
201 }
202}