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
158pub fn pi_harness_model_requires_probe_slug(
159    harness: &str,
160    selection_kind: &str,
161    model_id: &str,
162    resolved: &ResolvedRunnablePath,
163) -> bool {
164    harness.eq_ignore_ascii_case("pi")
165        && selection_kind.eq_ignore_ascii_case("fixed")
166        && !model_id.trim().is_empty()
167        && resolved.source == RunnablePathSource::Passthrough
168        && !resolved.harness_model_id.contains('/')
169}
170
171#[cfg(test)]
172mod tests {
173    use std::collections::HashSet;
174
175    use super::*;
176    use crate::models::probes::PiProbeResult;
177
178    #[test]
179    fn qualified_provider_constraint_passthrough_without_probe() {
180        let resolved = resolve_harness_model(HarnessModelInput {
181            harness: "pi",
182            model_id: "gpt-5.4-mini",
183            provider_constraint: Some("openai-codex"),
184            provider_for_order: Some("openai-codex"),
185            settings_provider_order: None,
186            opencode_probe: None,
187            pi_probe: None,
188        });
189
190        assert_eq!(resolved.harness_model_id, "openai-codex/gpt-5.4-mini");
191        assert_eq!(resolved.source, RunnablePathSource::Passthrough);
192        assert_eq!(resolved.confidence, RunnableConfidence::Confirmed);
193    }
194
195    #[test]
196    fn pi_bare_model_uses_probe_slug() {
197        let mut model_slugs = HashSet::new();
198        model_slugs.insert("openai-codex/gpt-5.4-mini".to_string());
199        model_slugs.insert("openai/gpt-5.4-mini".to_string());
200        let pi_probe = PiProbeResult {
201            compatible: true,
202            model_slugs,
203            ..PiProbeResult::default()
204        };
205
206        let resolved = resolve_harness_model(HarnessModelInput {
207            harness: "pi",
208            model_id: "gpt-5.4-mini",
209            provider_constraint: None,
210            provider_for_order: Some("openai"),
211            settings_provider_order: None,
212            opencode_probe: None,
213            pi_probe: Some(&pi_probe),
214        });
215
216        assert_eq!(resolved.harness_model_id, "openai-codex/gpt-5.4-mini");
217        assert_eq!(resolved.source, RunnablePathSource::CachedProbe);
218        assert_eq!(resolved.confidence, RunnableConfidence::Confirmed);
219    }
220
221    #[test]
222    fn pi_constraint_prefers_matching_provider_slug() {
223        let mut model_slugs = HashSet::new();
224        model_slugs.insert("openai-codex/gpt-5.4-mini".to_string());
225        model_slugs.insert("openai/gpt-5.4-mini".to_string());
226        let pi_probe = PiProbeResult {
227            compatible: true,
228            model_slugs,
229            ..PiProbeResult::default()
230        };
231
232        let resolved = resolve_harness_model(HarnessModelInput {
233            harness: "pi",
234            model_id: "gpt-5.4-mini",
235            provider_constraint: Some("openai-codex"),
236            provider_for_order: Some("openai-codex"),
237            settings_provider_order: None,
238            opencode_probe: None,
239            pi_probe: Some(&pi_probe),
240        });
241
242        assert_eq!(resolved.harness_model_id, "openai-codex/gpt-5.4-mini");
243    }
244
245    #[test]
246    fn opencode_uses_probe_slug_without_provider_constraint() {
247        let opencode_probe = OpenCodeProbeResult {
248            model_slugs: vec![
249                "openai/gpt-5.4-mini".to_string(),
250                "openai/gpt-5.5".to_string(),
251            ],
252            model_probe_success: true,
253            error: None,
254        };
255
256        let resolved = resolve_harness_model(HarnessModelInput {
257            harness: "opencode",
258            model_id: "gpt-5.4-mini",
259            provider_constraint: None,
260            provider_for_order: Some("openai"),
261            settings_provider_order: None,
262            opencode_probe: Some(&opencode_probe),
263            pi_probe: None,
264        });
265
266        assert_eq!(resolved.harness_model_id, "openai/gpt-5.4-mini");
267        assert_eq!(resolved.source, RunnablePathSource::CachedProbe);
268    }
269
270    #[test]
271    fn codex_native_provider_match_returns_bare_model() {
272        let resolved = resolve_harness_model(HarnessModelInput {
273            harness: "codex",
274            model_id: "gpt-5.4-mini",
275            provider_constraint: None,
276            provider_for_order: Some("openai"),
277            settings_provider_order: None,
278            opencode_probe: None,
279            pi_probe: None,
280        });
281
282        assert_eq!(resolved.harness_model_id, "gpt-5.4-mini");
283        assert_eq!(resolved.source, RunnablePathSource::ProviderMatch);
284        assert_eq!(resolved.confidence, RunnableConfidence::Likely);
285    }
286
287    #[test]
288    fn codex_provider_constraint_native_match_returns_bare_model() {
289        let resolved = resolve_harness_model(HarnessModelInput {
290            harness: "codex",
291            model_id: "gpt-5.4-mini",
292            provider_constraint: Some("openai"),
293            provider_for_order: Some("openai"),
294            settings_provider_order: None,
295            opencode_probe: None,
296            pi_probe: None,
297        });
298
299        assert_eq!(resolved.harness_model_id, "gpt-5.4-mini");
300        assert_eq!(resolved.source, RunnablePathSource::ProviderMatch);
301        assert_eq!(resolved.confidence, RunnableConfidence::Likely);
302    }
303
304    #[test]
305    fn pi_provider_constraint_uses_probe_slug_not_blind_prefix() {
306        let mut model_slugs = HashSet::new();
307        model_slugs.insert("openai-codex/gpt-5.4-mini".to_string());
308        model_slugs.insert("openai/gpt-5.4-mini".to_string());
309        let pi_probe = PiProbeResult {
310            compatible: true,
311            model_slugs,
312            ..PiProbeResult::default()
313        };
314
315        let resolved = resolve_harness_model(HarnessModelInput {
316            harness: "pi",
317            model_id: "gpt-5.4-mini",
318            provider_constraint: Some("openai"),
319            provider_for_order: Some("openai"),
320            settings_provider_order: None,
321            opencode_probe: None,
322            pi_probe: Some(&pi_probe),
323        });
324
325        assert_eq!(resolved.harness_model_id, "openai-codex/gpt-5.4-mini");
326        assert_ne!(resolved.harness_model_id, "openai/gpt-5.4-mini");
327        assert_eq!(resolved.source, RunnablePathSource::CachedProbe);
328        assert_eq!(resolved.confidence, RunnableConfidence::Confirmed);
329    }
330
331    #[test]
332    fn pi_fixed_without_probe_slug_is_detected() {
333        let resolved = passthrough_bare("gpt-5.4-mini");
334        assert!(pi_harness_model_requires_probe_slug(
335            "pi",
336            "fixed",
337            "gpt-5.4-mini",
338            &resolved
339        ));
340    }
341}