Skip to main content

mars_agents/models/
harness_model.rs

1use crate::routing::probe_match::select_probe_slug;
2use crate::routing::slug;
3
4use super::availability::{ResolvedRunnablePath, RunnableConfidence, RunnablePathSource};
5use super::probes::{OpenCodeProbeResult, PiProbeResult};
6
7pub struct HarnessModelInput<'a> {
8    pub harness: &'a str,
9    pub model_id: &'a str,
10    pub provider_constraint: Option<&'a str>,
11    pub provider_for_order: Option<&'a str>,
12    pub settings_provider_order: Option<&'a [String]>,
13    pub opencode_probe: Option<&'a OpenCodeProbeResult>,
14    pub pi_probe: Option<&'a PiProbeResult>,
15}
16
17pub fn resolve_harness_model(input: HarnessModelInput<'_>) -> ResolvedRunnablePath {
18    let model_id = input.model_id.trim();
19    if model_id.is_empty() {
20        return ResolvedRunnablePath {
21            harness_model_id: String::new(),
22            source: RunnablePathSource::Passthrough,
23            confidence: RunnableConfidence::Unknown,
24        };
25    }
26
27    let harness = input.harness;
28    if harness.eq_ignore_ascii_case("pi") {
29        return resolve_pi_harness_model(input);
30    }
31    if harness.eq_ignore_ascii_case("opencode") {
32        return resolve_opencode_harness_model(input);
33    }
34
35    if native_provider_matches_harness(input, harness) {
36        return ResolvedRunnablePath {
37            harness_model_id: model_id.to_string(),
38            source: RunnablePathSource::ProviderMatch,
39            confidence: RunnableConfidence::Likely,
40        };
41    }
42
43    ResolvedRunnablePath {
44        harness_model_id: model_id.to_string(),
45        source: RunnablePathSource::Passthrough,
46        confidence: RunnableConfidence::Unknown,
47    }
48}
49
50fn native_provider_matches_harness(input: HarnessModelInput<'_>, harness: &str) -> bool {
51    let provider_matches = |provider: &str| {
52        !provider.trim().is_empty() && slug::provider_matches_native_harness(provider, harness)
53    };
54    input.provider_constraint.is_some_and(provider_matches)
55        || input.provider_for_order.is_some_and(provider_matches)
56}
57
58fn resolve_pi_harness_model(input: HarnessModelInput<'_>) -> ResolvedRunnablePath {
59    let model_id = input.model_id.trim();
60    let Some(pi_probe) = input.pi_probe else {
61        return constraint_qualified_passthrough(model_id, input.provider_constraint);
62    };
63    if !pi_probe.compatible {
64        return constraint_qualified_passthrough(model_id, input.provider_constraint);
65    }
66
67    probe_slug_or_passthrough(
68        model_id,
69        input.provider_constraint,
70        input.provider_for_order,
71        input.settings_provider_order,
72        pi_probe.model_slugs.iter().map(String::as_str),
73    )
74}
75
76fn resolve_opencode_harness_model(input: HarnessModelInput<'_>) -> ResolvedRunnablePath {
77    let model_id = input.model_id.trim();
78    let Some(opencode_probe) = input.opencode_probe else {
79        return constraint_qualified_passthrough(model_id, input.provider_constraint);
80    };
81    if !opencode_probe.model_probe_success {
82        return constraint_qualified_passthrough(model_id, input.provider_constraint);
83    }
84
85    probe_slug_or_passthrough(
86        model_id,
87        input.provider_constraint,
88        input.provider_for_order,
89        input.settings_provider_order,
90        opencode_probe.model_slugs.iter().map(String::as_str),
91    )
92}
93
94fn probe_slug_or_passthrough<'a>(
95    model_id: &str,
96    provider_constraint: Option<&str>,
97    provider_for_order: Option<&str>,
98    settings_provider_order: Option<&[String]>,
99    slugs: impl IntoIterator<Item = &'a str>,
100) -> ResolvedRunnablePath {
101    let selection = select_probe_slug(
102        model_id,
103        probe_constraint_for_selection(provider_constraint, provider_for_order),
104        provider_for_order,
105        settings_provider_order,
106        slugs,
107    );
108    if let Some(slug) = selection.chosen_slug {
109        return ResolvedRunnablePath {
110            harness_model_id: slug,
111            source: RunnablePathSource::CachedProbe,
112            confidence: RunnableConfidence::Confirmed,
113        };
114    }
115
116    constraint_qualified_passthrough(model_id, provider_constraint)
117}
118
119/// Broad alias constraints (e.g. `openai`) must not force an exact-tier probe pick
120/// before variant preference; keep the constraint for qualified passthrough fallback.
121fn probe_constraint_for_selection<'a>(
122    provider_constraint: Option<&'a str>,
123    provider_for_order: Option<&'a str>,
124) -> Option<&'a str> {
125    let constraint = provider_constraint.filter(|provider| !provider.trim().is_empty())?;
126    let order = provider_for_order.filter(|provider| !provider.trim().is_empty());
127    if order.is_some_and(|order| slug::providers_exact_match(constraint, order)) {
128        return None;
129    }
130    Some(constraint)
131}
132
133fn constraint_qualified_passthrough(
134    model_id: &str,
135    provider_constraint: Option<&str>,
136) -> ResolvedRunnablePath {
137    if model_id.contains('/') {
138        return passthrough_bare(model_id);
139    }
140    if let Some(constraint) = provider_constraint.filter(|provider| !provider.trim().is_empty()) {
141        return ResolvedRunnablePath {
142            harness_model_id: format!("{}/{}", constraint.trim(), model_id),
143            source: RunnablePathSource::Passthrough,
144            confidence: RunnableConfidence::Confirmed,
145        };
146    }
147    passthrough_bare(model_id)
148}
149
150fn passthrough_bare(model_id: &str) -> ResolvedRunnablePath {
151    ResolvedRunnablePath {
152        harness_model_id: model_id.to_string(),
153        source: RunnablePathSource::Passthrough,
154        confidence: RunnableConfidence::Unknown,
155    }
156}
157#[cfg(test)]
158mod tests {
159    use std::collections::HashSet;
160
161    use super::*;
162    use crate::models::probes::PiProbeResult;
163
164    #[test]
165    fn qualified_provider_constraint_passthrough_without_probe() {
166        let resolved = resolve_harness_model(HarnessModelInput {
167            harness: "pi",
168            model_id: "gpt-5.4-mini",
169            provider_constraint: Some("openai-codex"),
170            provider_for_order: Some("openai-codex"),
171            settings_provider_order: None,
172            opencode_probe: None,
173            pi_probe: None,
174        });
175
176        assert_eq!(resolved.harness_model_id, "openai-codex/gpt-5.4-mini");
177        assert_eq!(resolved.source, RunnablePathSource::Passthrough);
178        assert_eq!(resolved.confidence, RunnableConfidence::Confirmed);
179    }
180
181    #[test]
182    fn pi_bare_model_uses_probe_slug() {
183        let mut model_slugs = HashSet::new();
184        model_slugs.insert("openai-codex/gpt-5.4-mini".to_string());
185        model_slugs.insert("openai/gpt-5.4-mini".to_string());
186        let pi_probe = PiProbeResult {
187            compatible: true,
188            model_slugs,
189            ..PiProbeResult::default()
190        };
191
192        let resolved = resolve_harness_model(HarnessModelInput {
193            harness: "pi",
194            model_id: "gpt-5.4-mini",
195            provider_constraint: None,
196            provider_for_order: Some("openai"),
197            settings_provider_order: None,
198            opencode_probe: None,
199            pi_probe: Some(&pi_probe),
200        });
201
202        assert_eq!(resolved.harness_model_id, "openai-codex/gpt-5.4-mini");
203        assert_eq!(resolved.source, RunnablePathSource::CachedProbe);
204        assert_eq!(resolved.confidence, RunnableConfidence::Confirmed);
205    }
206
207    #[test]
208    fn pi_constraint_prefers_matching_provider_slug() {
209        let mut model_slugs = HashSet::new();
210        model_slugs.insert("openai-codex/gpt-5.4-mini".to_string());
211        model_slugs.insert("openai/gpt-5.4-mini".to_string());
212        let pi_probe = PiProbeResult {
213            compatible: true,
214            model_slugs,
215            ..PiProbeResult::default()
216        };
217
218        let resolved = resolve_harness_model(HarnessModelInput {
219            harness: "pi",
220            model_id: "gpt-5.4-mini",
221            provider_constraint: Some("openai-codex"),
222            provider_for_order: Some("openai-codex"),
223            settings_provider_order: None,
224            opencode_probe: None,
225            pi_probe: Some(&pi_probe),
226        });
227
228        assert_eq!(resolved.harness_model_id, "openai-codex/gpt-5.4-mini");
229    }
230
231    #[test]
232    fn opencode_uses_probe_slug_without_provider_constraint() {
233        let opencode_probe = OpenCodeProbeResult {
234            model_slugs: vec![
235                "openai/gpt-5.4-mini".to_string(),
236                "openai/gpt-5.5".to_string(),
237            ],
238            model_probe_success: true,
239            error: None,
240        };
241
242        let resolved = resolve_harness_model(HarnessModelInput {
243            harness: "opencode",
244            model_id: "gpt-5.4-mini",
245            provider_constraint: None,
246            provider_for_order: Some("openai"),
247            settings_provider_order: None,
248            opencode_probe: Some(&opencode_probe),
249            pi_probe: None,
250        });
251
252        assert_eq!(resolved.harness_model_id, "openai/gpt-5.4-mini");
253        assert_eq!(resolved.source, RunnablePathSource::CachedProbe);
254    }
255
256    #[test]
257    fn codex_native_provider_match_returns_bare_model() {
258        let resolved = resolve_harness_model(HarnessModelInput {
259            harness: "codex",
260            model_id: "gpt-5.4-mini",
261            provider_constraint: None,
262            provider_for_order: Some("openai"),
263            settings_provider_order: None,
264            opencode_probe: None,
265            pi_probe: None,
266        });
267
268        assert_eq!(resolved.harness_model_id, "gpt-5.4-mini");
269        assert_eq!(resolved.source, RunnablePathSource::ProviderMatch);
270        assert_eq!(resolved.confidence, RunnableConfidence::Likely);
271    }
272
273    #[test]
274    fn codex_provider_constraint_native_match_returns_bare_model() {
275        let resolved = resolve_harness_model(HarnessModelInput {
276            harness: "codex",
277            model_id: "gpt-5.4-mini",
278            provider_constraint: Some("openai"),
279            provider_for_order: Some("openai"),
280            settings_provider_order: None,
281            opencode_probe: None,
282            pi_probe: None,
283        });
284
285        assert_eq!(resolved.harness_model_id, "gpt-5.4-mini");
286        assert_eq!(resolved.source, RunnablePathSource::ProviderMatch);
287        assert_eq!(resolved.confidence, RunnableConfidence::Likely);
288    }
289
290    #[test]
291    fn pi_provider_constraint_uses_probe_slug_not_blind_prefix() {
292        let mut model_slugs = HashSet::new();
293        model_slugs.insert("openai-codex/gpt-5.4-mini".to_string());
294        model_slugs.insert("openai/gpt-5.4-mini".to_string());
295        let pi_probe = PiProbeResult {
296            compatible: true,
297            model_slugs,
298            ..PiProbeResult::default()
299        };
300
301        let resolved = resolve_harness_model(HarnessModelInput {
302            harness: "pi",
303            model_id: "gpt-5.4-mini",
304            provider_constraint: Some("openai"),
305            provider_for_order: Some("openai"),
306            settings_provider_order: None,
307            opencode_probe: None,
308            pi_probe: Some(&pi_probe),
309        });
310
311        assert_eq!(resolved.harness_model_id, "openai-codex/gpt-5.4-mini");
312        assert_ne!(resolved.harness_model_id, "openai/gpt-5.4-mini");
313        assert_eq!(resolved.source, RunnablePathSource::CachedProbe);
314        assert_eq!(resolved.confidence, RunnableConfidence::Confirmed);
315    }
316}