Skip to main content

mars_agents/models/
availability.rs

1use std::collections::HashSet;
2
3use serde::Serialize;
4
5use crate::routing::slug;
6
7use super::probes::{OpenCodeProbeResult, PiProbeResult};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
10#[serde(rename_all = "snake_case")]
11pub enum AvailabilityStatus {
12    Runnable,
13    Unavailable,
14    Unknown,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
18#[serde(rename_all = "snake_case")]
19pub enum AvailabilitySource {
20    HarnessInstalled,
21    UniversalHarness,
22    #[serde(rename = "pi_probe")]
23    PiProbe,
24    #[serde(rename = "pi_probe_negative")]
25    PiProbeNegative,
26    #[serde(rename = "opencode_probe")]
27    OpenCodeProbe,
28    #[serde(rename = "opencode_probe_negative")]
29    OpenCodeProbeNegative,
30    #[serde(rename = "opencode_probe_unknown")]
31    OpenCodeProbeUnknown,
32    NoHarness,
33    Offline,
34}
35
36/// A runnable model path — one specific way to execute a model.
37#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
38pub struct RunnablePath {
39    pub harness: String,
40    pub mars_provider: String,
41    pub harness_model_id: String,
42}
43
44/// Full availability assessment for a resolved model.
45#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
46pub struct ModelAvailability {
47    pub status: AvailabilityStatus,
48    pub source: AvailabilitySource,
49    pub runnable_paths: Vec<RunnablePath>,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum RunnablePathSource {
54    CachedProbe,
55    ProviderMatch,
56    Synthesized,
57    Passthrough,
58}
59
60impl RunnablePathSource {
61    pub fn label(self) -> &'static str {
62        match self {
63            Self::CachedProbe => "cached-probe",
64            Self::ProviderMatch => "provider-match",
65            Self::Synthesized => "synthesized",
66            Self::Passthrough => "passthrough",
67        }
68    }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum RunnableConfidence {
73    Confirmed,
74    Likely,
75    Unknown,
76}
77
78impl RunnableConfidence {
79    pub fn label(self) -> &'static str {
80        match self {
81            Self::Confirmed => "confirmed",
82            Self::Likely => "likely",
83            Self::Unknown => "unknown",
84        }
85    }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct ResolvedRunnablePath {
90    pub harness_model_id: String,
91    pub source: RunnablePathSource,
92    pub confidence: RunnableConfidence,
93}
94
95pub fn resolve_runnable_path(
96    model_id: &str,
97    provider: &str,
98    target_harness: &str,
99    probe_result: Option<&OpenCodeProbeResult>,
100) -> ResolvedRunnablePath {
101    if let Some(cached_path) =
102        resolve_cached_probe_path(model_id, provider, target_harness, probe_result)
103    {
104        return cached_path;
105    }
106
107    if is_provider_native_harness(provider, target_harness) {
108        return ResolvedRunnablePath {
109            harness_model_id: model_id.to_string(),
110            source: RunnablePathSource::ProviderMatch,
111            confidence: RunnableConfidence::Likely,
112        };
113    }
114
115    ResolvedRunnablePath {
116        harness_model_id: model_id.to_string(),
117        source: RunnablePathSource::Passthrough,
118        confidence: RunnableConfidence::Unknown,
119    }
120}
121
122fn resolve_cached_probe_path(
123    model_id: &str,
124    provider: &str,
125    target_harness: &str,
126    probe_result: Option<&OpenCodeProbeResult>,
127) -> Option<ResolvedRunnablePath> {
128    if !target_harness.eq_ignore_ascii_case("opencode") {
129        return None;
130    }
131    if provider.trim().is_empty() {
132        return None;
133    }
134
135    let probe = probe_result?;
136    if !probe.model_probe_success {
137        return None;
138    }
139
140    let matched_slug = slug::find_exact_match(
141        model_id,
142        provider,
143        probe.model_slugs.iter().map(String::as_str),
144    )?
145    .slug;
146    Some(ResolvedRunnablePath {
147        harness_model_id: matched_slug,
148        source: RunnablePathSource::CachedProbe,
149        confidence: RunnableConfidence::Confirmed,
150    })
151}
152
153/// Classify availability for a model through a specific harness.
154pub fn classify_for_harness(
155    harness: &str,
156    provider: &str,
157    model_id: &str,
158    installed: &HashSet<String>,
159    probe_result: Option<&OpenCodeProbeResult>,
160) -> Option<(AvailabilityStatus, AvailabilitySource, Option<RunnablePath>)> {
161    let harness = harness.to_ascii_lowercase();
162    if !installed.contains(&harness) {
163        return Some((
164            AvailabilityStatus::Unavailable,
165            AvailabilitySource::NoHarness,
166            None,
167        ));
168    }
169
170    let direct_match = match harness.as_str() {
171        "claude" => slug::providers_match(provider, "anthropic"),
172        "codex" => slug::providers_match(provider, "openai"),
173        "opencode" => return classify_opencode(provider, model_id, probe_result),
174        "pi" | "cursor" => return classify_universal_harness(),
175        _ => false,
176    };
177
178    if direct_match {
179        Some((
180            AvailabilityStatus::Runnable,
181            AvailabilitySource::HarnessInstalled,
182            Some(RunnablePath {
183                harness,
184                mars_provider: provider.to_string(),
185                harness_model_id: model_id.to_string(),
186            }),
187        ))
188    } else {
189        Some((
190            AvailabilityStatus::Unavailable,
191            AvailabilitySource::NoHarness,
192            None,
193        ))
194    }
195}
196
197fn classify_universal_harness()
198-> Option<(AvailabilityStatus, AvailabilitySource, Option<RunnablePath>)> {
199    Some((
200        AvailabilityStatus::Unknown,
201        AvailabilitySource::UniversalHarness,
202        None,
203    ))
204}
205
206fn classify_opencode(
207    provider: &str,
208    model_id: &str,
209    probe_result: Option<&OpenCodeProbeResult>,
210) -> Option<(AvailabilityStatus, AvailabilitySource, Option<RunnablePath>)> {
211    let Some(probe) = probe_result else {
212        return Some((
213            AvailabilityStatus::Unknown,
214            AvailabilitySource::OpenCodeProbeUnknown,
215            None,
216        ));
217    };
218
219    if !probe.model_probe_success {
220        return Some((
221            AvailabilityStatus::Unknown,
222            AvailabilitySource::OpenCodeProbeUnknown,
223            None,
224        ));
225    }
226
227    if is_unknown_provider(provider) {
228        return Some((
229            AvailabilityStatus::Unknown,
230            AvailabilitySource::OpenCodeProbeUnknown,
231            None,
232        ));
233    }
234
235    let Some(harness_model_id) = slug::find_exact_match(
236        model_id,
237        provider,
238        probe.model_slugs.iter().map(String::as_str),
239    )
240    .map(|matched| matched.slug) else {
241        return Some((
242            AvailabilityStatus::Unavailable,
243            AvailabilitySource::OpenCodeProbeNegative,
244            None,
245        ));
246    };
247
248    Some((
249        AvailabilityStatus::Runnable,
250        AvailabilitySource::OpenCodeProbe,
251        Some(RunnablePath {
252            harness: "opencode".to_string(),
253            mars_provider: provider.to_string(),
254            harness_model_id,
255        }),
256    ))
257}
258
259fn is_unknown_provider(provider: &str) -> bool {
260    let provider = provider.trim();
261    provider.is_empty() || provider.eq_ignore_ascii_case("unknown")
262}
263
264fn is_provider_native_harness(provider: &str, target_harness: &str) -> bool {
265    slug::provider_matches_native_harness(provider, target_harness)
266}
267
268pub fn classify_model(
269    model_id: &str,
270    provider: &str,
271    installed: &HashSet<String>,
272    opencode_probe_result: Option<&OpenCodeProbeResult>,
273    pi_probe_result: Option<&PiProbeResult>,
274    offline: bool,
275) -> ModelAvailability {
276    let mut statuses = Vec::new();
277    let mut runnable_paths = Vec::new();
278
279    for harness in ["claude", "codex", "cursor"] {
280        let Some((status, source, path)) =
281            classify_for_harness(harness, provider, model_id, installed, None)
282        else {
283            continue;
284        };
285        if let Some(path) = path {
286            runnable_paths.push(path);
287        }
288        statuses.push((status, source));
289    }
290
291    if let Some((status, source, path)) =
292        classify_pi_for_model(provider, model_id, installed, pi_probe_result, offline)
293    {
294        if let Some(path) = path {
295            runnable_paths.push(path);
296        }
297        statuses.push((status, source));
298    }
299
300    if installed.contains("opencode") {
301        if offline {
302            statuses.push((AvailabilityStatus::Unknown, AvailabilitySource::Offline));
303        } else if let Some(result) = opencode_probe_result {
304            if let Some((status, source, path)) =
305                classify_for_harness("opencode", provider, model_id, installed, Some(result))
306            {
307                if let Some(path) = path {
308                    runnable_paths.push(path);
309                }
310                statuses.push((status, source));
311            }
312        } else {
313            statuses.push((
314                AvailabilityStatus::Unknown,
315                AvailabilitySource::OpenCodeProbeUnknown,
316            ));
317        }
318    }
319
320    aggregate_statuses(statuses, runnable_paths)
321}
322
323fn classify_pi_for_model(
324    provider: &str,
325    model_id: &str,
326    installed: &HashSet<String>,
327    pi_probe_result: Option<&PiProbeResult>,
328    offline: bool,
329) -> Option<(AvailabilityStatus, AvailabilitySource, Option<RunnablePath>)> {
330    if !installed.contains("pi") {
331        return None;
332    }
333
334    if offline || pi_probe_result.is_none() {
335        return classify_universal_harness();
336    }
337
338    let pi_probe_result = pi_probe_result.expect("checked is_some above");
339    if !pi_probe_result.compatible {
340        return Some((
341            AvailabilityStatus::Unavailable,
342            AvailabilitySource::PiProbeNegative,
343            None,
344        ));
345    }
346
347    let Some(harness_model_id) = slug::find_exact_match(
348        model_id,
349        provider,
350        pi_probe_result.model_slugs.iter().map(String::as_str),
351    )
352    .map(|matched| matched.slug) else {
353        return Some((
354            AvailabilityStatus::Unavailable,
355            AvailabilitySource::PiProbeNegative,
356            None,
357        ));
358    };
359
360    Some((
361        AvailabilityStatus::Runnable,
362        AvailabilitySource::PiProbe,
363        Some(RunnablePath {
364            harness: "pi".to_string(),
365            mars_provider: provider.to_string(),
366            harness_model_id,
367        }),
368    ))
369}
370
371fn aggregate_statuses(
372    statuses: Vec<(AvailabilityStatus, AvailabilitySource)>,
373    runnable_paths: Vec<RunnablePath>,
374) -> ModelAvailability {
375    if statuses.is_empty() {
376        return ModelAvailability {
377            status: AvailabilityStatus::Unavailable,
378            source: AvailabilitySource::NoHarness,
379            runnable_paths: Vec::new(),
380        };
381    }
382
383    if statuses
384        .iter()
385        .any(|(status, _)| *status == AvailabilityStatus::Runnable)
386    {
387        return ModelAvailability {
388            status: AvailabilityStatus::Runnable,
389            source: statuses
390                .iter()
391                .find_map(|(status, source)| {
392                    (*status == AvailabilityStatus::Runnable).then(|| source.clone())
393                })
394                .expect("runnable status exists"),
395            runnable_paths,
396        };
397    }
398
399    if statuses
400        .iter()
401        .any(|(status, _)| *status == AvailabilityStatus::Unknown)
402    {
403        return ModelAvailability {
404            status: AvailabilityStatus::Unknown,
405            source: statuses
406                .iter()
407                .find_map(|(status, source)| {
408                    (*status == AvailabilityStatus::Unknown).then(|| source.clone())
409                })
410                .unwrap_or(AvailabilitySource::OpenCodeProbeUnknown),
411            runnable_paths: Vec::new(),
412        };
413    }
414
415    ModelAvailability {
416        status: AvailabilityStatus::Unavailable,
417        source: statuses
418            .iter()
419            .find_map(|(_, source)| {
420                (*source != AvailabilitySource::NoHarness).then(|| source.clone())
421            })
422            .or_else(|| statuses.first().map(|(_, source)| source.clone()))
423            .unwrap_or(AvailabilitySource::NoHarness),
424        runnable_paths: Vec::new(),
425    }
426}
427
428#[cfg(test)]
429mod tests {
430    use super::*;
431
432    fn installed(names: &[&str]) -> HashSet<String> {
433        names.iter().map(|name| (*name).to_string()).collect()
434    }
435
436    #[test]
437    fn test_classify_claude_anthropic() {
438        let result = classify_for_harness(
439            "claude",
440            "Anthropic",
441            "claude-opus-4-7",
442            &installed(&["claude"]),
443            None,
444        )
445        .unwrap();
446        assert_eq!(result.0, AvailabilityStatus::Runnable);
447        assert_eq!(result.1, AvailabilitySource::HarnessInstalled);
448        assert_eq!(
449            result.2.unwrap().harness_model_id,
450            "claude-opus-4-7".to_string()
451        );
452    }
453
454    #[test]
455    fn test_classify_codex_openai() {
456        let result =
457            classify_for_harness("codex", "OpenAI", "gpt-5.4", &installed(&["codex"]), None)
458                .unwrap();
459        assert_eq!(result.0, AvailabilityStatus::Runnable);
460        assert_eq!(result.1, AvailabilitySource::HarnessInstalled);
461    }
462
463    #[test]
464    fn test_classify_codex_openai_codex_variant_is_runnable() {
465        let result = classify_for_harness(
466            "codex",
467            "openai-codex",
468            "gpt-5.4-mini",
469            &installed(&["codex"]),
470            None,
471        )
472        .unwrap();
473        assert_eq!(result.0, AvailabilityStatus::Runnable);
474        assert_eq!(result.1, AvailabilitySource::HarnessInstalled);
475        assert_eq!(
476            result
477                .2
478                .expect("runnable path should be present")
479                .harness_model_id,
480            "gpt-5.4-mini"
481        );
482    }
483
484    #[test]
485    fn test_classify_pi_is_universal_unknown_when_installed() {
486        let result =
487            classify_for_harness("pi", "OpenAI", "gpt-5.4-mini", &installed(&["pi"]), None)
488                .unwrap();
489        assert_eq!(result.0, AvailabilityStatus::Unknown);
490        assert_eq!(result.1, AvailabilitySource::UniversalHarness);
491        assert!(result.2.is_none());
492    }
493
494    #[test]
495    fn test_classify_cursor_is_universal_unknown_when_installed() {
496        let result = classify_for_harness(
497            "cursor",
498            "Anthropic",
499            "claude-opus-4-7",
500            &installed(&["cursor"]),
501            None,
502        )
503        .unwrap();
504        assert_eq!(result.0, AvailabilityStatus::Unknown);
505        assert_eq!(result.1, AvailabilitySource::UniversalHarness);
506        assert!(result.2.is_none());
507    }
508
509    #[test]
510    fn test_classify_no_harness() {
511        let result = classify_for_harness(
512            "claude",
513            "Anthropic",
514            "claude-opus-4-7",
515            &installed(&[]),
516            None,
517        )
518        .unwrap();
519        assert_eq!(result.0, AvailabilityStatus::Unavailable);
520        assert_eq!(result.1, AvailabilitySource::NoHarness);
521        assert!(result.2.is_none());
522    }
523
524    #[test]
525    fn test_classify_multi_harness_any_runnable() {
526        let result = classify_model(
527            "claude-opus-4-7",
528            "Anthropic",
529            &installed(&["claude", "codex"]),
530            None,
531            None,
532            false,
533        );
534        assert_eq!(result.status, AvailabilityStatus::Runnable);
535        assert_eq!(result.source, AvailabilitySource::HarnessInstalled);
536        assert_eq!(result.runnable_paths.len(), 1);
537        assert_eq!(result.runnable_paths[0].harness, "claude");
538    }
539
540    #[test]
541    fn test_classify_multi_harness_all_unavailable() {
542        let result = classify_model(
543            "custom-model",
544            "Unknown",
545            &installed(&[]),
546            None,
547            None,
548            false,
549        );
550        assert_eq!(result.status, AvailabilityStatus::Unavailable);
551        assert_eq!(result.source, AvailabilitySource::NoHarness);
552        assert!(result.runnable_paths.is_empty());
553    }
554
555    #[test]
556    fn test_classify_google_model_with_only_pi_installed_is_unknown_universal() {
557        let result = classify_model(
558            "gemini-2.5-pro",
559            "Google",
560            &installed(&["pi"]),
561            None,
562            None,
563            false,
564        );
565        assert_eq!(result.status, AvailabilityStatus::Unknown);
566        assert_eq!(result.source, AvailabilitySource::UniversalHarness);
567        assert!(result.runnable_paths.is_empty());
568    }
569
570    #[test]
571    fn test_classify_pi_probe_compatible_is_runnable() {
572        let pi_probe = PiProbeResult {
573            compatible: true,
574            model_slugs: HashSet::from(["openai/gpt-5.4-mini".to_string()]),
575            ..PiProbeResult::default()
576        };
577
578        let result = classify_model(
579            "gpt-5.4-mini",
580            "OpenAI",
581            &installed(&["pi"]),
582            None,
583            Some(&pi_probe),
584            false,
585        );
586
587        assert_eq!(result.status, AvailabilityStatus::Runnable);
588        assert_eq!(result.source, AvailabilitySource::PiProbe);
589        assert_eq!(result.runnable_paths.len(), 1);
590        assert_eq!(result.runnable_paths[0].harness, "pi");
591        assert_eq!(
592            result.runnable_paths[0].harness_model_id,
593            "openai/gpt-5.4-mini"
594        );
595    }
596
597    #[test]
598    fn test_classify_pi_probe_incompatible_is_unavailable_without_other_harnesses() {
599        let pi_probe = PiProbeResult {
600            compatible: false,
601            ..PiProbeResult::default()
602        };
603
604        let result = classify_model(
605            "gpt-5.4-mini",
606            "OpenAI",
607            &installed(&["pi"]),
608            None,
609            Some(&pi_probe),
610            false,
611        );
612
613        assert_eq!(result.status, AvailabilityStatus::Unavailable);
614        assert_eq!(result.source, AvailabilitySource::PiProbeNegative);
615        assert!(result.runnable_paths.is_empty());
616    }
617
618    #[test]
619    fn test_classify_pi_probe_incompatible_yields_to_runnable_harness() {
620        let pi_probe = PiProbeResult {
621            compatible: false,
622            ..PiProbeResult::default()
623        };
624
625        let result = classify_model(
626            "gpt-5.4-mini",
627            "OpenAI",
628            &installed(&["pi", "codex"]),
629            None,
630            Some(&pi_probe),
631            false,
632        );
633
634        assert_eq!(result.status, AvailabilityStatus::Runnable);
635        assert_eq!(result.source, AvailabilitySource::HarnessInstalled);
636        assert_eq!(result.runnable_paths.len(), 1);
637        assert_eq!(result.runnable_paths[0].harness, "codex");
638    }
639
640    #[test]
641    fn test_classify_pi_probe_missing_model_is_unavailable() {
642        let pi_probe = PiProbeResult {
643            compatible: true,
644            model_slugs: HashSet::from(["openai/gpt-5.4".to_string()]),
645            ..PiProbeResult::default()
646        };
647
648        let result = classify_model(
649            "gpt-5.4-mini",
650            "OpenAI",
651            &installed(&["pi"]),
652            None,
653            Some(&pi_probe),
654            false,
655        );
656
657        assert_eq!(result.status, AvailabilityStatus::Unavailable);
658        assert_eq!(result.source, AvailabilitySource::PiProbeNegative);
659        assert!(result.runnable_paths.is_empty());
660    }
661
662    #[test]
663    fn test_classify_offline_mode() {
664        let result = classify_model(
665            "gpt-5.4",
666            "OpenAI",
667            &installed(&["codex"]),
668            None,
669            None,
670            true,
671        );
672        assert_eq!(result.status, AvailabilityStatus::Runnable);
673        assert_eq!(result.source, AvailabilitySource::HarnessInstalled);
674        assert_eq!(result.runnable_paths.len(), 1);
675        assert_eq!(result.runnable_paths[0].harness, "codex");
676
677        let result = classify_model(
678            "gpt-5.4",
679            "OpenAI",
680            &installed(&["opencode"]),
681            None,
682            None,
683            true,
684        );
685        assert_eq!(result.status, AvailabilityStatus::Unknown);
686        assert_eq!(result.source, AvailabilitySource::Offline);
687        assert!(result.runnable_paths.is_empty());
688    }
689
690    #[test]
691    fn test_classify_opencode_direct_slug() {
692        let probe = OpenCodeProbeResult {
693            model_slugs: vec!["openai/gpt-5.4".to_string()],
694            model_probe_success: true,
695            error: None,
696        };
697
698        let result = classify_model(
699            "gpt-5.4",
700            "OpenAI",
701            &installed(&["opencode"]),
702            Some(&probe),
703            None,
704            false,
705        );
706
707        assert_eq!(result.status, AvailabilityStatus::Runnable);
708        assert_eq!(result.source, AvailabilitySource::OpenCodeProbe);
709        assert_eq!(result.runnable_paths.len(), 1);
710        assert_eq!(result.runnable_paths[0].harness, "opencode");
711        assert_eq!(result.runnable_paths[0].harness_model_id, "openai/gpt-5.4");
712    }
713
714    #[test]
715    fn test_classify_opencode_nested_provider_slug_is_not_flattened() {
716        let probe = OpenCodeProbeResult {
717            model_slugs: vec!["openrouter/anthropic/claude-opus-4.7".to_string()],
718            model_probe_success: true,
719            error: None,
720        };
721
722        let result = classify_model(
723            "claude-opus-4-7",
724            "Anthropic",
725            &installed(&["opencode"]),
726            Some(&probe),
727            None,
728            false,
729        );
730
731        assert_eq!(result.status, AvailabilityStatus::Unavailable);
732        assert_eq!(result.source, AvailabilitySource::OpenCodeProbeNegative);
733        assert!(result.runnable_paths.is_empty());
734    }
735
736    #[test]
737    fn test_classify_opencode_provider_negative() {
738        let probe = OpenCodeProbeResult {
739            model_slugs: vec!["google/gemini-2.5-pro".to_string()],
740            model_probe_success: true,
741            ..OpenCodeProbeResult::default()
742        };
743
744        let result = classify_model(
745            "gpt-5.4",
746            "OpenAI",
747            &installed(&["opencode"]),
748            Some(&probe),
749            None,
750            false,
751        );
752
753        assert_eq!(result.status, AvailabilityStatus::Unavailable);
754        assert_eq!(result.source, AvailabilitySource::OpenCodeProbeNegative);
755        assert!(result.runnable_paths.is_empty());
756    }
757
758    #[test]
759    fn test_classify_opencode_empty_slugs() {
760        let probe = OpenCodeProbeResult {
761            model_slugs: Vec::new(),
762            model_probe_success: true,
763            error: None,
764        };
765
766        let result = classify_model(
767            "claude-opus-4-7",
768            "Anthropic",
769            &installed(&["opencode"]),
770            Some(&probe),
771            None,
772            false,
773        );
774
775        assert_eq!(result.status, AvailabilityStatus::Unavailable);
776        assert_eq!(result.source, AvailabilitySource::OpenCodeProbeNegative);
777        assert!(result.runnable_paths.is_empty());
778    }
779
780    #[test]
781    fn test_classify_opencode_no_matching_slug() {
782        let probe = OpenCodeProbeResult {
783            model_slugs: vec!["anthropic/claude-3-5-sonnet".to_string()],
784            model_probe_success: true,
785            error: None,
786        };
787
788        let result = classify_model(
789            "claude-opus-4-7",
790            "Anthropic",
791            &installed(&["opencode"]),
792            Some(&probe),
793            None,
794            false,
795        );
796
797        assert_eq!(result.status, AvailabilityStatus::Unavailable);
798        assert_eq!(result.source, AvailabilitySource::OpenCodeProbeNegative);
799        assert!(result.runnable_paths.is_empty());
800    }
801
802    #[test]
803    fn test_classify_opencode_unknown_when_model_probe_fails() {
804        let probe = OpenCodeProbeResult {
805            model_probe_success: false,
806            error: Some("model probe failed: timeout".to_string()),
807            ..OpenCodeProbeResult::default()
808        };
809
810        let result = classify_model(
811            "claude-opus-4-7",
812            "Anthropic",
813            &installed(&["opencode"]),
814            Some(&probe),
815            None,
816            false,
817        );
818
819        assert_eq!(result.status, AvailabilityStatus::Unknown);
820        assert_eq!(result.source, AvailabilitySource::OpenCodeProbeUnknown);
821        assert!(result.runnable_paths.is_empty());
822    }
823
824    #[test]
825    fn test_resolve_runnable_path_prefers_cached_probe_slug() {
826        let probe = OpenCodeProbeResult {
827            model_slugs: vec!["openai/gpt-5.4".to_string()],
828            model_probe_success: true,
829            error: None,
830        };
831
832        let resolved = resolve_runnable_path("gpt-5.4", "OpenAI", "opencode", Some(&probe));
833        assert_eq!(resolved.harness_model_id, "openai/gpt-5.4");
834        assert_eq!(resolved.source, RunnablePathSource::CachedProbe);
835        assert_eq!(resolved.confidence, RunnableConfidence::Confirmed);
836    }
837
838    #[test]
839    fn test_resolve_runnable_path_falls_back_to_passthrough_without_slug_match() {
840        let probe = OpenCodeProbeResult {
841            model_slugs: vec!["openrouter/anthropic/claude-sonnet-4-7".to_string()],
842            model_probe_success: true,
843            error: None,
844        };
845
846        let resolved =
847            resolve_runnable_path("claude-opus-4-7", "Anthropic", "opencode", Some(&probe));
848        assert_eq!(resolved.harness_model_id, "claude-opus-4-7");
849        assert_eq!(resolved.source, RunnablePathSource::Passthrough);
850        assert_eq!(resolved.confidence, RunnableConfidence::Unknown);
851    }
852
853    #[test]
854    fn test_classify_opencode_unknown_when_probe_fails() {
855        let probe = OpenCodeProbeResult {
856            error: Some("model probe failed: timeout".to_string()),
857            ..OpenCodeProbeResult::default()
858        };
859
860        let result = classify_model(
861            "gpt-5.4",
862            "OpenAI",
863            &installed(&["opencode"]),
864            Some(&probe),
865            None,
866            false,
867        );
868
869        assert_eq!(result.status, AvailabilityStatus::Unknown);
870        assert_eq!(result.source, AvailabilitySource::OpenCodeProbeUnknown);
871        assert!(result.runnable_paths.is_empty());
872    }
873
874    #[test]
875    fn test_classify_opencode_unknown_provider_stays_unknown() {
876        let probe = OpenCodeProbeResult {
877            model_slugs: vec!["openai/gpt-5.4".to_string()],
878            model_probe_success: true,
879            ..OpenCodeProbeResult::default()
880        };
881
882        let result = classify_model(
883            "mystery-model",
884            "unknown",
885            &installed(&["opencode"]),
886            Some(&probe),
887            None,
888            false,
889        );
890
891        assert_eq!(result.status, AvailabilityStatus::Unknown);
892        assert_eq!(result.source, AvailabilitySource::OpenCodeProbeUnknown);
893        assert!(result.runnable_paths.is_empty());
894    }
895}