Skip to main content

harn_vm/llm_config/
resolution.rs

1//! Selector resolution: turn an alias or provider/model selector into the
2//! complete `ResolvedModel` identity (provider, normalized id, tool format,
3//! tier, family, lineage).
4use serde::Serialize;
5
6use super::*;
7
8#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
9pub struct ResolvedModel {
10    pub id: String,
11    pub provider: String,
12    pub alias: Option<String>,
13    pub tool_format: String,
14    pub tier: String,
15    pub family: String,
16    pub lineage: String,
17}
18
19/// Resolve a model alias to (model_id, provider_name).
20pub fn resolve_model(alias: &str) -> (String, Option<String>) {
21    let config = effective_config();
22    if let Some(a) = config.aliases.get(alias) {
23        return (a.id.clone(), Some(a.provider.clone()));
24    }
25    (normalize_model_id(alias), None)
26}
27
28/// Strip host/provider selector prefixes that identify transport, not the
29/// provider-native model id. This mirrors the host's existing normalization so
30/// `ollama:qwen3:30b` reaches Ollama as `qwen3:30b` instead of an invalid
31/// model named `ollama`. Cerebras follows the same convention but uses a
32/// slash separator (`cerebras/gpt-oss-120b`) because its own /v1/models
33/// endpoint returns bare names that overlap OpenAI's families.
34pub fn normalize_model_id(raw: &str) -> String {
35    for prefix in PROVIDER_SELECTOR_PREFIXES {
36        if let Some(stripped) = raw.strip_prefix(prefix) {
37            return stripped.to_string();
38        }
39    }
40    raw.to_string()
41}
42
43const PROVIDER_SELECTOR_PREFIXES: &[&str] =
44    &["ollama:", "local:", "huggingface:", "hf:", "cerebras/"];
45
46/// Resolve an alias or selector into the complete catalog identity hosts need:
47/// provider inference, prefix-normalized model id, default tool format, and tier.
48pub fn resolve_model_info(selector: &str) -> ResolvedModel {
49    let config = effective_config();
50    if let Some(alias) = config.aliases.get(selector) {
51        let id = alias.id.clone();
52        let provider = alias.provider.clone();
53        let requested = alias
54            .tool_format
55            .clone()
56            .unwrap_or_else(|| default_tool_format_with_config(&config, &id, &provider));
57        let tool_format = guard_tool_format(&provider, &id, &requested, Some(selector));
58        return ResolvedModel {
59            tier: model_tier_with_config(&config, &id),
60            family: model_family_with_config(&config, &provider, &id),
61            lineage: model_lineage_with_config(&config, &provider, &id),
62            id,
63            provider,
64            alias: Some(selector.to_string()),
65            tool_format,
66        };
67    }
68
69    let id = normalize_model_id(selector);
70    let inference = infer_provider_with_config(&config, selector);
71    let source = inference.source;
72    let provider = inference.provider;
73    let requested = default_tool_format_with_config(&config, &id, &provider);
74    let tool_format = guard_tool_format(&provider, &id, &requested, None);
75    let tier = model_tier_with_config(&config, &id);
76    let family = model_family_with_inference_source(&config, &provider, &id, source);
77    let lineage = model_lineage_with_inference_source(&config, &provider, &id, source);
78    ResolvedModel {
79        id,
80        provider,
81        alias: None,
82        tool_format,
83        tier,
84        family,
85        lineage,
86    }
87}
88
89/// Run the requested `tool_format` through the capability registry's
90/// dialect-validity gate, returning the safe format to actually use. When the
91/// registry auto-corrects a known-broken combo (e.g. a `native` pin on a
92/// `native_unreliable` route that silently drops to unparsed DSML text), the
93/// correction is logged once at resolution time so a harness developer sees
94/// *why* their pinned format was not honored — never a silent vanishing.
95fn guard_tool_format(provider: &str, model: &str, requested: &str, alias: Option<&str>) -> String {
96    let decision = crate::llm::capabilities::validate_tool_format(provider, model, requested);
97    if let Some(reason) = &decision.correction {
98        tracing::warn!(
99            target: "harn::llm::tool_format",
100            alias = alias.unwrap_or(""),
101            "{reason}"
102        );
103    }
104    decision.effective
105}