Skip to main content

mars_agents/routing/
mod.rs

1use std::collections::HashSet;
2
3pub mod acceptance;
4pub mod evidence;
5pub mod probe_match;
6pub mod report;
7pub mod slug;
8
9pub(crate) use probe_match::{SlugSelection, select_probe_slug};
10
11use crate::models;
12use crate::models::harness::HarnessOrderFailure;
13use crate::models::probes::CursorProbeResult;
14use crate::models::probes::OpenCodeProbeResult;
15use crate::models::probes::PiProbeResult;
16
17pub use evidence::{RoutingEvidence, RoutingSettingsEvidence};
18
19/// How the harness was selected — orthogonal to slug evidence.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum SelectionKind {
22    Auto,
23    Fixed,
24    ConfigDefault,
25    LinkedFallback,
26    HardcodedDefault,
27}
28
29impl SelectionKind {
30    pub fn label(self) -> &'static str {
31        match self {
32            Self::Auto => "auto",
33            Self::Fixed => "fixed",
34            Self::ConfigDefault => "config_default",
35            Self::LinkedFallback => "linked_fallback",
36            Self::HardcodedDefault => "hardcoded_default",
37        }
38    }
39}
40
41/// Slug evidence the evaluator found for this harness.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum MatchEvidence {
44    Confirmed,
45    Constrained,
46    Passthrough,
47    None,
48}
49
50impl MatchEvidence {
51    pub fn label(self) -> &'static str {
52        match self {
53            Self::Confirmed => "confirmed",
54            Self::Constrained => "constrained",
55            Self::Passthrough => "passthrough",
56            Self::None => "none",
57        }
58    }
59}
60
61/// How the harness was selected.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum RouteSource {
64    Cli,
65    Profile,
66    Alias,
67    ConfigOrder,
68    ConfigDefault,
69    Provider,
70    HardcodedDefault,
71}
72
73impl RouteSource {
74    pub fn label(self) -> &'static str {
75        match self {
76            Self::Cli => "cli",
77            Self::Profile => "profile",
78            Self::Alias => "alias",
79            Self::ConfigOrder => "config-order",
80            Self::ConfigDefault => "config",
81            Self::Provider => "provider",
82            Self::HardcodedDefault => "default",
83        }
84    }
85}
86
87/// Assessment of one candidate harness.
88#[derive(Debug, Clone)]
89pub struct CandidateAssessment {
90    pub harness: String,
91    pub installed: bool,
92    pub candidate_slugs: Vec<String>,
93    pub filtered_slugs: Vec<String>,
94    pub chosen_slug: Option<String>,
95    pub chosen_model: Option<String>,
96    pub match_evidence: Option<MatchEvidence>,
97    pub skip_reason: Option<&'static str>,
98}
99
100/// Full routing trace for diagnostics/provenance.
101#[derive(Debug, Clone)]
102pub struct RoutingTrace {
103    pub source: RouteSource,
104    pub selection_kind: SelectionKind,
105    pub match_evidence: MatchEvidence,
106    pub harness: String,
107    pub harness_order_position: Option<usize>,
108    pub candidates_tried: Vec<String>,
109    pub assessments: Vec<CandidateAssessment>,
110    pub diagnostics: Vec<String>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct SelectedChosenSlugEvidence {
115    pub slug: String,
116    pub match_evidence: Option<MatchEvidence>,
117}
118
119impl RoutingTrace {
120    pub fn selected_harness(&self) -> &str {
121        &self.harness
122    }
123
124    pub fn selected_selection_kind(&self) -> SelectionKind {
125        self.selection_kind
126    }
127
128    pub fn selected_match_evidence(&self) -> MatchEvidence {
129        self.match_evidence
130    }
131
132    pub fn selected_diagnostics(&self) -> &[String] {
133        &self.diagnostics
134    }
135
136    pub fn selected_harness_order_position(&self) -> Option<usize> {
137        self.harness_order_position
138    }
139
140    pub fn selected_chosen_slug_evidence(&self) -> Option<SelectedChosenSlugEvidence> {
141        self.assessments
142            .iter()
143            .find(|assessment| assessment.harness == self.harness)
144            .and_then(|assessment| {
145                assessment
146                    .chosen_slug
147                    .as_ref()
148                    .map(|slug| SelectedChosenSlugEvidence {
149                        slug: slug.clone(),
150                        match_evidence: assessment.match_evidence,
151                    })
152            })
153    }
154
155    pub fn to_report(&self) -> report::RouteDecisionReport {
156        report::RouteDecisionReport::from_trace(self)
157    }
158}
159
160/// Input to the routing engine.
161pub struct RoutingInput<'a> {
162    pub model_id: &'a str,
163    pub provider_for_order: Option<&'a str>,
164    pub provider_constraint: Option<&'a str>,
165    pub settings_provider_order: Option<&'a [String]>,
166    pub settings_harness_order: Option<&'a [String]>,
167    pub config_default_harness: Option<&'a str>,
168    pub installed_harnesses: &'a HashSet<String>,
169    pub linked_harnesses: Option<&'a [String]>,
170    pub opencode_probe_result: Option<&'a OpenCodeProbeResult>,
171    pub pi_probe_result: Option<&'a PiProbeResult>,
172    pub cursor_probe_result: Option<&'a CursorProbeResult>,
173    /// Cached catalog slugs (`provider/model`) for full model-id matching on native harnesses.
174    pub catalog_model_slugs: Option<&'a [String]>,
175}
176
177pub trait ProbeResolver {
178    fn opencode_probe_result(&mut self) -> Option<OpenCodeProbeResult>;
179    fn pi_probe_result(&mut self) -> Option<PiProbeResult>;
180    fn cursor_probe_result(&mut self) -> Option<CursorProbeResult>;
181}
182
183#[derive(Debug, Default)]
184struct StaticProbeResolver {
185    opencode_probe_result: Option<OpenCodeProbeResult>,
186    pi_probe_result: Option<PiProbeResult>,
187    cursor_probe_result: Option<CursorProbeResult>,
188}
189
190impl StaticProbeResolver {
191    fn from_input(input: &RoutingInput<'_>) -> Self {
192        Self {
193            opencode_probe_result: input.opencode_probe_result.cloned(),
194            pi_probe_result: input.pi_probe_result.cloned(),
195            cursor_probe_result: input.cursor_probe_result.cloned(),
196        }
197    }
198}
199
200impl ProbeResolver for StaticProbeResolver {
201    fn opencode_probe_result(&mut self) -> Option<OpenCodeProbeResult> {
202        self.opencode_probe_result.clone()
203    }
204
205    fn pi_probe_result(&mut self) -> Option<PiProbeResult> {
206        self.pi_probe_result.clone()
207    }
208
209    fn cursor_probe_result(&mut self) -> Option<CursorProbeResult> {
210        self.cursor_probe_result.clone()
211    }
212}
213
214/// Evaluate all candidates and return a routing trace.
215/// This is the ONLY candidate evaluator. Both `mars models` and `mars build` call this.
216pub fn evaluate_candidates(input: &RoutingInput<'_>) -> RoutingTrace {
217    let mut probe_resolver = StaticProbeResolver::from_input(input);
218    evaluate_candidates_with_auth_and_probes(
219        input,
220        &mut probe_resolver,
221        models::harness::native_harness_authenticated,
222    )
223}
224
225/// Evaluate one fixed harness choice without fallback.
226/// Used by fixed-selection precedence paths (CLI/profile/alias).
227pub fn evaluate_fixed_harness(input: &RoutingInput<'_>, harness: &str) -> CandidateAssessment {
228    let mut probe_resolver = StaticProbeResolver::from_input(input);
229    evaluate_fixed_harness_with_auth_and_probes(
230        input,
231        harness,
232        &mut probe_resolver,
233        models::harness::native_harness_authenticated,
234    )
235}
236
237pub fn evaluate_fixed_harness_with_auth<F>(
238    input: &RoutingInput<'_>,
239    harness: &str,
240    auth_check: F,
241) -> CandidateAssessment
242where
243    F: Fn(&str) -> bool,
244{
245    let mut probe_resolver = StaticProbeResolver::from_input(input);
246    evaluate_fixed_harness_with_auth_and_probes(input, harness, &mut probe_resolver, auth_check)
247}
248
249pub fn evaluate_fixed_harness_with_auth_and_probes<F, P>(
250    input: &RoutingInput<'_>,
251    harness: &str,
252    probe_resolver: &mut P,
253    auth_check: F,
254) -> CandidateAssessment
255where
256    F: Fn(&str) -> bool,
257    P: ProbeResolver + ?Sized,
258{
259    candidate_match_evidence_with_auth(
260        input,
261        harness,
262        input.settings_provider_order,
263        probe_resolver,
264        &auth_check,
265    )
266}
267
268/// Build a fixed-selection routing trace from one fixed harness assessment.
269pub fn trace_for_fixed_harness(
270    source: RouteSource,
271    harness: &str,
272    assessment: CandidateAssessment,
273    diagnostics: Vec<String>,
274) -> RoutingTrace {
275    let match_evidence = assessment.match_evidence.unwrap_or(MatchEvidence::None);
276    RoutingTrace {
277        source,
278        selection_kind: SelectionKind::Fixed,
279        match_evidence,
280        harness: harness.to_string(),
281        harness_order_position: None,
282        candidates_tried: vec![harness.to_string()],
283        assessments: vec![assessment],
284        diagnostics,
285    }
286}
287
288pub fn provider_for_order_for_fixed_harness<'a>(
289    provider_for_order: Option<&'a str>,
290    harness: &str,
291) -> Option<&'a str> {
292    let has_explicit_provider = provider_for_order.is_some_and(|provider| {
293        let normalized = provider.trim();
294        !normalized.is_empty() && !normalized.eq_ignore_ascii_case("unknown")
295    });
296    if has_explicit_provider {
297        return provider_for_order;
298    }
299
300    native_provider_for_harness(harness).or(provider_for_order)
301}
302
303pub fn evaluate_candidates_with_auth<F>(input: &RoutingInput<'_>, auth_check: F) -> RoutingTrace
304where
305    F: Fn(&str) -> bool,
306{
307    let mut probe_resolver = StaticProbeResolver::from_input(input);
308    evaluate_candidates_with_auth_and_probes(input, &mut probe_resolver, auth_check)
309}
310
311pub fn evaluate_candidates_with_auth_and_probes<F, P>(
312    input: &RoutingInput<'_>,
313    probe_resolver: &mut P,
314    auth_check: F,
315) -> RoutingTrace
316where
317    F: Fn(&str) -> bool,
318    P: ProbeResolver + ?Sized,
319{
320    let mut diagnostics = Vec::new();
321    let parsed_provider_order =
322        parse_settings_provider_order(input.settings_provider_order, &mut diagnostics);
323    let config_default_harness =
324        normalize_config_default_harness(input.config_default_harness, &mut diagnostics);
325    let linked_harnesses = input
326        .linked_harnesses
327        .filter(|harnesses| !harnesses.is_empty());
328    let linked_harnesses_set = linked_harnesses
329        .map(|harnesses| harnesses.iter().map(String::as_str).collect::<HashSet<_>>());
330    let has_link_constraints = linked_harnesses_set.is_some();
331    let effective_config_default_harness = config_default_harness
332        .as_ref()
333        .filter(|harness| {
334            linked_harnesses_set
335                .as_ref()
336                .is_none_or(|known| known.contains(harness.as_str()))
337        })
338        .cloned();
339    if has_link_constraints
340        && config_default_harness.is_some()
341        && effective_config_default_harness.is_none()
342    {
343        diagnostics.push(
344            "settings.default_harness is excluded by known linked harness constraints; ignoring fallback"
345                .to_string(),
346        );
347    }
348
349    let mut harness_order_failure = None;
350
351    let mut candidate_source = RouteSource::Provider;
352
353    let candidates = if let Some(order) = input.settings_harness_order {
354        let parsed_order = models::harness::parse_settings_harness_order(order);
355        diagnostics.extend(parsed_order.warnings);
356
357        if parsed_order.failure == Some(HarnessOrderFailure::Empty) {
358            diagnostics.push(
359                "settings.harness_order is empty; falling through to provider candidate order"
360                    .to_string(),
361            );
362            let provider_for_order = input.provider_for_order.unwrap_or("unknown");
363            filter_candidates_by_links(
364                models::harness::harness_candidates_for_provider(provider_for_order),
365                linked_harnesses_set.as_ref(),
366            )
367            .into_iter()
368            .map(|harness| (harness, None))
369            .collect::<Vec<_>>()
370        } else {
371            candidate_source = RouteSource::ConfigOrder;
372            let mut candidate_pairs = parsed_order
373                .valid_candidates
374                .into_iter()
375                .enumerate()
376                .map(|(index, harness)| (harness, Some(index)))
377                .collect::<Vec<_>>();
378
379            filter_candidate_pairs_by_links(&mut candidate_pairs, linked_harnesses_set.as_ref());
380
381            let valid_candidates = candidate_pairs
382                .iter()
383                .map(|(harness, _)| harness.clone())
384                .collect::<Vec<_>>();
385
386            if !valid_candidates.is_empty()
387                && valid_candidates
388                    .iter()
389                    .all(|candidate| !input.installed_harnesses.contains(candidate))
390            {
391                harness_order_failure = Some(HarnessOrderFailure::NoneInstalled {
392                    valid_candidates: valid_candidates.clone(),
393                });
394            }
395
396            candidate_pairs
397        }
398    } else if input.model_id.trim().is_empty() {
399        filter_candidates_by_links(
400            models::harness::VALID_HARNESSES
401                .iter()
402                .map(|harness| (*harness).to_string())
403                .collect(),
404            linked_harnesses_set.as_ref(),
405        )
406        .into_iter()
407        .map(|harness| (harness, None))
408        .collect::<Vec<_>>()
409    } else {
410        let provider_for_order = input.provider_for_order.unwrap_or("unknown");
411        filter_candidates_by_links(
412            models::harness::harness_candidates_for_provider(provider_for_order),
413            linked_harnesses_set.as_ref(),
414        )
415        .into_iter()
416        .map(|harness| (harness, None))
417        .collect::<Vec<_>>()
418    };
419
420    let mut candidates_tried = Vec::new();
421    let mut assessments = Vec::new();
422    let mut passthrough_selection: Option<(String, Option<usize>, MatchEvidence)> = None;
423
424    for (harness, harness_order_position) in candidates {
425        let assessment = candidate_match_evidence_with_auth(
426            input,
427            &harness,
428            Some(parsed_provider_order.as_slice()),
429            probe_resolver,
430            &auth_check,
431        );
432
433        candidates_tried.push(harness.clone());
434        let match_evidence = assessment.match_evidence;
435        assessments.push(assessment);
436
437        if let Some(match_evidence) = match_evidence {
438            match match_evidence {
439                MatchEvidence::Confirmed | MatchEvidence::Constrained => {
440                    return RoutingTrace {
441                        source: candidate_source,
442                        selection_kind: SelectionKind::Auto,
443                        match_evidence,
444                        harness,
445                        harness_order_position,
446                        candidates_tried,
447                        assessments,
448                        diagnostics,
449                    };
450                }
451                MatchEvidence::Passthrough => {
452                    if passthrough_selection.is_none() {
453                        passthrough_selection =
454                            Some((harness, harness_order_position, match_evidence));
455                    }
456                }
457                MatchEvidence::None => {}
458            }
459        }
460    }
461
462    if let Some((harness, harness_order_position, match_evidence)) = passthrough_selection {
463        return RoutingTrace {
464            source: candidate_source,
465            selection_kind: SelectionKind::Auto,
466            match_evidence,
467            harness,
468            harness_order_position,
469            candidates_tried,
470            assessments,
471            diagnostics,
472        };
473    }
474
475    if input.settings_harness_order.is_some()
476        && let Some(warning) = format_harness_order_fallback_warning(
477            harness_order_failure.as_ref(),
478            effective_config_default_harness.is_some(),
479            has_link_constraints,
480        )
481    {
482        diagnostics.push(warning);
483    }
484
485    if let Some(harness) = effective_config_default_harness {
486        return RoutingTrace {
487            source: RouteSource::ConfigDefault,
488            selection_kind: SelectionKind::ConfigDefault,
489            match_evidence: MatchEvidence::Passthrough,
490            harness,
491            harness_order_position: None,
492            candidates_tried,
493            assessments,
494            diagnostics,
495        };
496    }
497
498    if let Some(known_links) = linked_harnesses {
499        if let Some(harness) = select_linked_fallback_harness(input, known_links, &assessments) {
500            diagnostics.push(format!(
501                "known linked harness constraints left no eligible auto-routing candidates; selecting linked harness `{harness}` in harness order (skipped incompatible candidates)"
502            ));
503            candidates_tried.push(harness.clone());
504
505            return RoutingTrace {
506                source: candidate_source,
507                selection_kind: SelectionKind::LinkedFallback,
508                match_evidence: MatchEvidence::Passthrough,
509                harness,
510                harness_order_position: None,
511                candidates_tried,
512                assessments,
513                diagnostics,
514            };
515        }
516
517        diagnostics.push(
518            "known linked harness constraints left no linked harness eligible for this model after routing assessments"
519                .to_string(),
520        );
521    }
522
523    diagnostics
524        .push("harness not set by CLI/profile/alias/provider/config; defaulting to `pi`".into());
525
526    RoutingTrace {
527        source: RouteSource::HardcodedDefault,
528        selection_kind: SelectionKind::HardcodedDefault,
529        match_evidence: MatchEvidence::Passthrough,
530        harness: "pi".to_string(),
531        harness_order_position: None,
532        candidates_tried,
533        assessments,
534        diagnostics,
535    }
536}
537
538/// Normalize and validate config default_harness. Returns normalized name or None with warning.
539pub fn normalize_config_default_harness(
540    config_default_harness: Option<&str>,
541    warnings: &mut Vec<String>,
542) -> Option<String> {
543    match config_default_harness {
544        Some(value) => match models::harness::normalize_harness_name(value) {
545            Some(valid) => Some(valid),
546            None => {
547                warnings.push(format!(
548                    "settings.default_harness `{value}` is invalid; expected one of: {}",
549                    models::harness::VALID_HARNESSES.join(", ")
550                ));
551                None
552            }
553        },
554        None => None,
555    }
556}
557
558fn filter_candidate_pairs_by_links(
559    candidates: &mut Vec<(String, Option<usize>)>,
560    linked_harnesses: Option<&HashSet<&str>>,
561) {
562    if let Some(linked_harnesses) = linked_harnesses {
563        candidates.retain(|(harness, _)| linked_harnesses.contains(harness.as_str()));
564    }
565}
566
567fn filter_candidates_by_links(
568    candidates: Vec<String>,
569    linked_harnesses: Option<&HashSet<&str>>,
570) -> Vec<String> {
571    let Some(linked_harnesses) = linked_harnesses else {
572        return candidates;
573    };
574
575    candidates
576        .into_iter()
577        .filter(|harness| linked_harnesses.contains(harness.as_str()))
578        .collect()
579}
580
581fn candidate_match_evidence_with_auth<F, P>(
582    input: &RoutingInput<'_>,
583    harness: &str,
584    provider_order: Option<&[String]>,
585    probe_resolver: &mut P,
586    auth_check: &F,
587) -> CandidateAssessment
588where
589    F: Fn(&str) -> bool,
590    P: ProbeResolver + ?Sized,
591{
592    if !input.installed_harnesses.contains(harness) {
593        return CandidateAssessment {
594            harness: harness.to_string(),
595            installed: false,
596            candidate_slugs: Vec::new(),
597            filtered_slugs: Vec::new(),
598            chosen_slug: None,
599            chosen_model: None,
600            match_evidence: None,
601            skip_reason: Some("not_installed"),
602        };
603    }
604
605    if is_native_harness(harness)
606        && provider_constraint_excludes_native_harness(input.provider_constraint, harness)
607    {
608        return CandidateAssessment {
609            harness: harness.to_string(),
610            installed: true,
611            candidate_slugs: Vec::new(),
612            filtered_slugs: Vec::new(),
613            chosen_slug: None,
614            chosen_model: None,
615            match_evidence: None,
616            skip_reason: Some("provider_constraint_unsatisfied"),
617        };
618    }
619
620    if input.model_id.trim().is_empty() {
621        return CandidateAssessment {
622            harness: harness.to_string(),
623            installed: true,
624            candidate_slugs: Vec::new(),
625            filtered_slugs: Vec::new(),
626            chosen_slug: None,
627            chosen_model: None,
628            match_evidence: Some(MatchEvidence::Passthrough),
629            skip_reason: None,
630        };
631    }
632
633    if is_native_harness(harness) {
634        let native_slugs = catalog_slugs_for_native_harness(harness, input.catalog_model_slugs);
635        if !native_slugs.is_empty() {
636            let selection = select_probe_slug(
637                input.model_id,
638                input.provider_constraint,
639                effective_provider_for_order(input).as_deref(),
640                provider_order,
641                native_slugs,
642            );
643            return assessment_from_slug_selection(
644                harness,
645                selection,
646                input.provider_constraint,
647                true,
648                &auth_check,
649            );
650        }
651
652        if is_native_match(effective_provider_for_order(input).as_deref(), harness) {
653            if auth_check(harness) {
654                return CandidateAssessment {
655                    harness: harness.to_string(),
656                    installed: true,
657                    candidate_slugs: Vec::new(),
658                    filtered_slugs: Vec::new(),
659                    chosen_slug: None,
660                    chosen_model: Some(input.model_id.to_string()),
661                    match_evidence: Some(match_evidence_for_match(input.provider_constraint)),
662                    skip_reason: None,
663                };
664            }
665
666            return CandidateAssessment {
667                harness: harness.to_string(),
668                installed: true,
669                candidate_slugs: Vec::new(),
670                filtered_slugs: Vec::new(),
671                chosen_slug: None,
672                chosen_model: None,
673                match_evidence: None,
674                skip_reason: Some("native_auth_unavailable"),
675            };
676        }
677
678        return CandidateAssessment {
679            harness: harness.to_string(),
680            installed: true,
681            candidate_slugs: Vec::new(),
682            filtered_slugs: Vec::new(),
683            chosen_slug: None,
684            chosen_model: None,
685            match_evidence: None,
686            skip_reason: Some("no_model_match"),
687        };
688    }
689
690    if harness == "opencode" {
691        let Some(opencode_probe) = probe_resolver.opencode_probe_result() else {
692            return CandidateAssessment {
693                harness: harness.to_string(),
694                installed: true,
695                candidate_slugs: Vec::new(),
696                filtered_slugs: Vec::new(),
697                chosen_slug: None,
698                chosen_model: None,
699                match_evidence: Some(MatchEvidence::Passthrough),
700                skip_reason: None,
701            };
702        };
703        if !opencode_probe.model_probe_success {
704            return CandidateAssessment {
705                harness: harness.to_string(),
706                installed: true,
707                candidate_slugs: Vec::new(),
708                filtered_slugs: Vec::new(),
709                chosen_slug: None,
710                chosen_model: None,
711                match_evidence: Some(MatchEvidence::Passthrough),
712                skip_reason: None,
713            };
714        }
715
716        let selection = select_probe_slug(
717            input.model_id,
718            input.provider_constraint,
719            input.provider_for_order,
720            provider_order,
721            opencode_probe.model_slugs.iter().map(String::as_str),
722        );
723
724        if let Some(chosen_slug) = selection.chosen_slug.clone() {
725            return CandidateAssessment {
726                harness: harness.to_string(),
727                installed: true,
728                candidate_slugs: selection.candidate_slugs,
729                filtered_slugs: selection.filtered_slugs,
730                chosen_model: slug::parse(&chosen_slug).map(|parts| parts.model_id.to_string()),
731                chosen_slug: Some(chosen_slug),
732                match_evidence: Some(match_evidence_for_match(input.provider_constraint)),
733                skip_reason: None,
734            };
735        }
736
737        if !selection.candidate_slugs.is_empty() {
738            return CandidateAssessment {
739                harness: harness.to_string(),
740                installed: true,
741                candidate_slugs: selection.candidate_slugs,
742                filtered_slugs: selection.filtered_slugs,
743                chosen_slug: None,
744                chosen_model: None,
745                match_evidence: None,
746                skip_reason: Some("provider_constraint_unsatisfied"),
747            };
748        }
749
750        return CandidateAssessment {
751            harness: harness.to_string(),
752            installed: true,
753            candidate_slugs: selection.candidate_slugs,
754            filtered_slugs: selection.filtered_slugs,
755            chosen_slug: None,
756            chosen_model: None,
757            match_evidence: None,
758            skip_reason: Some("no_model_match"),
759        };
760    }
761
762    if harness == "pi" {
763        if let Some(pi_probe) = probe_resolver.pi_probe_result() {
764            if pi_probe.compatible {
765                let selection = select_probe_slug(
766                    input.model_id,
767                    input.provider_constraint,
768                    input.provider_for_order,
769                    provider_order,
770                    pi_probe.model_slugs.iter().map(String::as_str),
771                );
772
773                if let Some(chosen_slug) = selection.chosen_slug.clone() {
774                    return CandidateAssessment {
775                        harness: harness.to_string(),
776                        installed: true,
777                        candidate_slugs: selection.candidate_slugs,
778                        filtered_slugs: selection.filtered_slugs,
779                        chosen_model: slug::parse(&chosen_slug)
780                            .map(|parts| parts.model_id.to_string()),
781                        chosen_slug: Some(chosen_slug),
782                        match_evidence: Some(match_evidence_for_match(input.provider_constraint)),
783                        skip_reason: None,
784                    };
785                }
786
787                if !selection.candidate_slugs.is_empty() {
788                    return CandidateAssessment {
789                        harness: harness.to_string(),
790                        installed: true,
791                        candidate_slugs: selection.candidate_slugs,
792                        filtered_slugs: selection.filtered_slugs,
793                        chosen_slug: None,
794                        chosen_model: None,
795                        match_evidence: None,
796                        skip_reason: Some("provider_constraint_unsatisfied"),
797                    };
798                }
799
800                return CandidateAssessment {
801                    harness: harness.to_string(),
802                    installed: true,
803                    candidate_slugs: selection.candidate_slugs,
804                    filtered_slugs: selection.filtered_slugs,
805                    chosen_slug: None,
806                    chosen_model: None,
807                    match_evidence: None,
808                    skip_reason: Some("no_model_match"),
809                };
810            }
811            return CandidateAssessment {
812                harness: harness.to_string(),
813                installed: true,
814                candidate_slugs: Vec::new(),
815                filtered_slugs: Vec::new(),
816                chosen_slug: None,
817                chosen_model: None,
818                match_evidence: None,
819                skip_reason: Some("pi_incompatible"),
820            };
821        }
822
823        return CandidateAssessment {
824            harness: harness.to_string(),
825            installed: true,
826            candidate_slugs: Vec::new(),
827            filtered_slugs: Vec::new(),
828            chosen_slug: None,
829            chosen_model: None,
830            match_evidence: Some(MatchEvidence::Passthrough),
831            skip_reason: None,
832        };
833    }
834
835    if harness == "cursor" {
836        let Some(cursor_probe) = probe_resolver.cursor_probe_result() else {
837            return passthrough_assessment(harness);
838        };
839        if !cursor_probe.model_probe_success {
840            return passthrough_assessment(harness);
841        }
842        if cursor_probe.slugs.is_empty() {
843            return passthrough_assessment(harness);
844        }
845
846        let normalized_model = crate::models::probes::cursor::normalize_slug(input.model_id);
847        if cursor_probe
848            .slugs
849            .iter()
850            .any(|slug| crate::models::probes::cursor::normalize_slug(slug) == normalized_model)
851        {
852            return CandidateAssessment {
853                harness: harness.to_string(),
854                installed: true,
855                candidate_slugs: vec![input.model_id.to_string()],
856                filtered_slugs: vec![input.model_id.to_string()],
857                chosen_slug: Some(input.model_id.to_string()),
858                chosen_model: Some(input.model_id.to_string()),
859                match_evidence: Some(MatchEvidence::Confirmed),
860                skip_reason: None,
861            };
862        }
863
864        let matches = crate::models::probes::cursor::find_cursor_prefix_matches(
865            input.model_id,
866            &cursor_probe.slugs,
867        );
868        if !matches.is_empty() {
869            let candidate_slugs: Vec<String> =
870                matches.iter().map(|slug| (*slug).to_string()).collect();
871            return CandidateAssessment {
872                harness: harness.to_string(),
873                installed: true,
874                candidate_slugs: candidate_slugs.clone(),
875                filtered_slugs: candidate_slugs,
876                chosen_slug: Some(input.model_id.to_string()),
877                chosen_model: Some(input.model_id.to_string()),
878                match_evidence: Some(MatchEvidence::Confirmed),
879                skip_reason: None,
880            };
881        }
882
883        // Probe slugs didn't match, but if the alias declares provider=cursor,
884        // trust the constraint over possibly-stale probe cache.
885        if input
886            .provider_constraint
887            .is_some_and(|p| p.eq_ignore_ascii_case("cursor"))
888        {
889            return CandidateAssessment {
890                harness: harness.to_string(),
891                installed: true,
892                candidate_slugs: Vec::new(),
893                filtered_slugs: Vec::new(),
894                chosen_slug: None,
895                chosen_model: None,
896                match_evidence: Some(MatchEvidence::Constrained),
897                skip_reason: None,
898            };
899        }
900
901        return CandidateAssessment {
902            harness: harness.to_string(),
903            installed: true,
904            candidate_slugs: Vec::new(),
905            filtered_slugs: Vec::new(),
906            chosen_slug: None,
907            chosen_model: None,
908            match_evidence: None,
909            skip_reason: Some("no_model_match"),
910        };
911    }
912
913    CandidateAssessment {
914        harness: harness.to_string(),
915        installed: true,
916        candidate_slugs: Vec::new(),
917        filtered_slugs: Vec::new(),
918        chosen_slug: None,
919        chosen_model: None,
920        match_evidence: None,
921        skip_reason: Some("unsupported_candidate"),
922    }
923}
924
925fn passthrough_assessment(harness: &str) -> CandidateAssessment {
926    CandidateAssessment {
927        harness: harness.to_string(),
928        installed: true,
929        candidate_slugs: Vec::new(),
930        filtered_slugs: Vec::new(),
931        chosen_slug: None,
932        chosen_model: None,
933        match_evidence: Some(MatchEvidence::Passthrough),
934        skip_reason: None,
935    }
936}
937
938fn native_provider_for_harness(harness: &str) -> Option<&'static str> {
939    match harness {
940        "claude" => Some("anthropic"),
941        "codex" => Some("openai"),
942        _ => None,
943    }
944}
945
946fn is_native_match(provider: Option<&str>, harness: &str) -> bool {
947    provider
948        .map(|provider| slug::provider_matches_native_harness(provider, harness))
949        .unwrap_or(false)
950}
951
952fn is_native_harness(harness: &str) -> bool {
953    matches!(harness, "claude" | "codex")
954}
955
956fn provider_constraint_excludes_native_harness(
957    provider_constraint: Option<&str>,
958    harness: &str,
959) -> bool {
960    let Some(provider_constraint) = provider_constraint else {
961        return false;
962    };
963
964    !slug::provider_matches_native_harness(provider_constraint, harness)
965}
966
967fn match_evidence_for_match(provider_constraint: Option<&str>) -> MatchEvidence {
968    if provider_constraint.is_some() {
969        MatchEvidence::Constrained
970    } else {
971        MatchEvidence::Confirmed
972    }
973}
974
975fn parse_settings_provider_order(
976    provider_order: Option<&[String]>,
977    diagnostics: &mut Vec<String>,
978) -> Vec<String> {
979    let Some(provider_order) = provider_order else {
980        return Vec::new();
981    };
982
983    provider_order
984        .iter()
985        .filter_map(|provider| {
986            let normalized = provider.trim().to_ascii_lowercase();
987            if normalized.is_empty() {
988                return None;
989            }
990            if !is_known_provider_or_variant(&normalized) {
991                diagnostics.push(format!(
992                    "settings.provider_order contains unknown provider `{provider}`; keeping it for forward-compat routing preferences"
993                ));
994            }
995            Some(normalized)
996        })
997        .collect()
998}
999
1000fn is_known_provider_or_variant(provider: &str) -> bool {
1001    matches!(
1002        provider,
1003        "anthropic"
1004            | "openai"
1005            | "google"
1006            | "meta"
1007            | "mistral"
1008            | "deepseek"
1009            | "cohere"
1010            | "openrouter"
1011            | "openai-codex"
1012            | "anthropic-claude"
1013    )
1014}
1015
1016fn effective_provider_for_order(input: &RoutingInput<'_>) -> Option<String> {
1017    input
1018        .provider_for_order
1019        .map(str::trim)
1020        .filter(|provider| !provider.is_empty() && !provider.eq_ignore_ascii_case("unknown"))
1021        .map(str::to_string)
1022        .or_else(|| models::infer_provider_from_model_id(input.model_id).map(str::to_string))
1023}
1024
1025fn catalog_slugs_for_native_harness<'a>(
1026    harness: &str,
1027    catalog_model_slugs: Option<&'a [String]>,
1028) -> Vec<&'a str> {
1029    let Some(slugs) = catalog_model_slugs else {
1030        return Vec::new();
1031    };
1032    slugs
1033        .iter()
1034        .filter(|slug| {
1035            slug::parse(slug)
1036                .is_some_and(|parts| slug::provider_matches_native_harness(parts.provider, harness))
1037        })
1038        .map(String::as_str)
1039        .collect()
1040}
1041
1042fn assessment_from_slug_selection<F>(
1043    harness: &str,
1044    selection: SlugSelection,
1045    provider_constraint: Option<&str>,
1046    require_auth: bool,
1047    auth_check: &F,
1048) -> CandidateAssessment
1049where
1050    F: Fn(&str) -> bool,
1051{
1052    if let Some(chosen_slug) = selection.chosen_slug.clone() {
1053        if require_auth && !auth_check(harness) {
1054            return CandidateAssessment {
1055                harness: harness.to_string(),
1056                installed: true,
1057                candidate_slugs: selection.candidate_slugs,
1058                filtered_slugs: selection.filtered_slugs,
1059                chosen_slug: None,
1060                chosen_model: None,
1061                match_evidence: None,
1062                skip_reason: Some("native_auth_unavailable"),
1063            };
1064        }
1065        return CandidateAssessment {
1066            harness: harness.to_string(),
1067            installed: true,
1068            candidate_slugs: selection.candidate_slugs,
1069            filtered_slugs: selection.filtered_slugs,
1070            chosen_model: slug::parse(&chosen_slug).map(|parts| parts.model_id.to_string()),
1071            chosen_slug: Some(chosen_slug),
1072            match_evidence: Some(match_evidence_for_match(provider_constraint)),
1073            skip_reason: None,
1074        };
1075    }
1076
1077    if !selection.candidate_slugs.is_empty() {
1078        return CandidateAssessment {
1079            harness: harness.to_string(),
1080            installed: true,
1081            candidate_slugs: selection.candidate_slugs,
1082            filtered_slugs: selection.filtered_slugs,
1083            chosen_slug: None,
1084            chosen_model: None,
1085            match_evidence: None,
1086            skip_reason: Some("provider_constraint_unsatisfied"),
1087        };
1088    }
1089
1090    CandidateAssessment {
1091        harness: harness.to_string(),
1092        installed: true,
1093        candidate_slugs: selection.candidate_slugs,
1094        filtered_slugs: selection.filtered_slugs,
1095        chosen_slug: None,
1096        chosen_model: None,
1097        match_evidence: None,
1098        skip_reason: Some("no_model_match"),
1099    }
1100}
1101
1102fn is_hard_assessment_skip(skip_reason: Option<&str>) -> bool {
1103    matches!(
1104        skip_reason,
1105        Some(
1106            "pi_incompatible"
1107                | "no_model_match"
1108                | "unsupported_candidate"
1109                | "not_installed"
1110                | "provider_constraint_unsatisfied"
1111        )
1112    )
1113}
1114
1115fn select_linked_fallback_harness(
1116    input: &RoutingInput<'_>,
1117    linked_harnesses: &[String],
1118    assessments: &[CandidateAssessment],
1119) -> Option<String> {
1120    let linked_set: HashSet<&str> = linked_harnesses.iter().map(String::as_str).collect();
1121
1122    let walk_order: Vec<String> = input
1123        .settings_harness_order
1124        .map(|order| {
1125            order
1126                .iter()
1127                .filter(|harness| linked_set.contains(harness.as_str()))
1128                .cloned()
1129                .collect()
1130        })
1131        .unwrap_or_else(|| linked_harnesses.to_vec());
1132
1133    for harness in walk_order {
1134        let rejected = assessments
1135            .iter()
1136            .find(|assessment| assessment.harness == harness)
1137            .and_then(|assessment| assessment.skip_reason)
1138            .is_some_and(|reason| is_hard_assessment_skip(Some(reason)));
1139        if !rejected {
1140            return Some(harness);
1141        }
1142    }
1143
1144    None
1145}
1146
1147fn format_harness_order_fallback_warning(
1148    harness_order_failure: Option<&HarnessOrderFailure>,
1149    has_config_default_harness: bool,
1150    has_link_constraints: bool,
1151) -> Option<String> {
1152    let mut warning = match harness_order_failure {
1153        Some(HarnessOrderFailure::Empty) => "settings.harness_order is empty".to_string(),
1154        Some(HarnessOrderFailure::NoneInstalled { valid_candidates }) => format!(
1155            "settings.harness_order is set but none of [{}] are installed",
1156            valid_candidates.join(", ")
1157        ),
1158        None => return None,
1159    };
1160
1161    if has_config_default_harness {
1162        warning.push_str("; falling through to settings.default_harness");
1163    } else if has_link_constraints {
1164        warning.push_str("; linked harness constraints prevent unrelated fallback");
1165    } else {
1166        warning.push_str("; settings.default_harness is unset, falling through to hardcoded `pi`");
1167    }
1168
1169    Some(warning)
1170}
1171
1172#[cfg(test)]
1173mod tests {
1174    use super::*;
1175
1176    fn installed(names: &[&str]) -> HashSet<String> {
1177        names.iter().map(|name| (*name).to_string()).collect()
1178    }
1179
1180    fn always_authed(_: &str) -> bool {
1181        true
1182    }
1183
1184    fn never_authed(_: &str) -> bool {
1185        false
1186    }
1187
1188    type ProbeInputs<'a> = (
1189        Option<&'a OpenCodeProbeResult>,
1190        Option<&'a PiProbeResult>,
1191        Option<&'a CursorProbeResult>,
1192    );
1193
1194    fn routing_input<'a>(
1195        model_id: &'a str,
1196        provider_for_order: Option<&'a str>,
1197        settings_harness_order: Option<&'a [String]>,
1198        config_default_harness: Option<&'a str>,
1199        installed_harnesses: &'a HashSet<String>,
1200        linked_harnesses: Option<&'a [String]>,
1201        probe_inputs: ProbeInputs<'a>,
1202    ) -> RoutingInput<'a> {
1203        routing_input_with_catalog(
1204            model_id,
1205            provider_for_order,
1206            settings_harness_order,
1207            config_default_harness,
1208            installed_harnesses,
1209            linked_harnesses,
1210            None,
1211            probe_inputs,
1212        )
1213    }
1214
1215    #[allow(clippy::too_many_arguments)]
1216    fn routing_input_with_catalog<'a>(
1217        model_id: &'a str,
1218        provider_for_order: Option<&'a str>,
1219        settings_harness_order: Option<&'a [String]>,
1220        config_default_harness: Option<&'a str>,
1221        installed_harnesses: &'a HashSet<String>,
1222        linked_harnesses: Option<&'a [String]>,
1223        catalog_model_slugs: Option<&'a [String]>,
1224        probe_inputs: ProbeInputs<'a>,
1225    ) -> RoutingInput<'a> {
1226        let (opencode_probe_result, pi_probe_result, cursor_probe_result) = probe_inputs;
1227        RoutingInput {
1228            model_id,
1229            provider_for_order,
1230            provider_constraint: None,
1231            settings_provider_order: None,
1232            settings_harness_order,
1233            config_default_harness,
1234            installed_harnesses,
1235            linked_harnesses,
1236            opencode_probe_result,
1237            pi_probe_result,
1238            cursor_probe_result,
1239            catalog_model_slugs,
1240        }
1241    }
1242
1243    #[test]
1244    fn native_match_with_auth_returns_confirmed() {
1245        let installed = installed(&["claude"]);
1246        let input = routing_input(
1247            "claude-opus-4-7",
1248            Some("anthropic"),
1249            None,
1250            None,
1251            &installed,
1252            None,
1253            (None, None, None),
1254        );
1255
1256        let trace = evaluate_candidates_with_auth(&input, always_authed);
1257
1258        assert_eq!(trace.source, RouteSource::Provider);
1259        assert_eq!(trace.selection_kind, SelectionKind::Auto);
1260        assert_eq!(trace.harness, "claude");
1261        assert_eq!(trace.match_evidence, MatchEvidence::Confirmed);
1262        assert_eq!(trace.candidates_tried, vec!["claude".to_string()]);
1263    }
1264
1265    #[test]
1266    fn catalog_native_match_without_explicit_provider() {
1267        let installed = installed(&["claude", "pi"]);
1268        let catalog = vec!["anthropic/claude-opus-4-6".to_string()];
1269        let harness_order = vec!["claude".to_string(), "pi".to_string()];
1270        let input = routing_input_with_catalog(
1271            "claude-opus-4-6",
1272            None,
1273            Some(&harness_order),
1274            None,
1275            &installed,
1276            None,
1277            Some(&catalog),
1278            (None, None, None),
1279        );
1280
1281        let trace = evaluate_candidates_with_auth(&input, always_authed);
1282
1283        assert_eq!(trace.harness, "claude");
1284        assert_eq!(trace.selection_kind, SelectionKind::Auto);
1285        assert_eq!(trace.match_evidence, MatchEvidence::Confirmed);
1286        assert_eq!(
1287            trace
1288                .assessments
1289                .iter()
1290                .find(|assessment| assessment.harness == "claude")
1291                .and_then(|assessment| assessment.chosen_slug.as_deref()),
1292            Some("anthropic/claude-opus-4-6")
1293        );
1294    }
1295
1296    #[test]
1297    fn linked_fallback_skips_pi_incompatible() {
1298        let installed = installed(&["claude", "pi"]);
1299        let catalog = vec!["anthropic/claude-opus-4-6".to_string()];
1300        let harness_order = vec!["pi".to_string(), "claude".to_string()];
1301        let linked = vec!["pi".to_string(), "claude".to_string()];
1302        let pi_probe = PiProbeResult {
1303            compatible: false,
1304            model_slugs: HashSet::new(),
1305            ..PiProbeResult::default()
1306        };
1307        let input = routing_input_with_catalog(
1308            "claude-opus-4-6",
1309            None,
1310            Some(&harness_order),
1311            None,
1312            &installed,
1313            Some(&linked),
1314            Some(&catalog),
1315            (None, Some(&pi_probe), None),
1316        );
1317
1318        let trace = evaluate_candidates_with_auth(&input, never_authed);
1319
1320        assert_eq!(trace.harness, "claude");
1321        assert_eq!(trace.selection_kind, SelectionKind::LinkedFallback);
1322        assert!(
1323            trace
1324                .diagnostics
1325                .iter()
1326                .any(|diagnostic| diagnostic.contains("skipped incompatible candidates"))
1327        );
1328        assert_eq!(
1329            trace
1330                .assessments
1331                .iter()
1332                .find(|assessment| assessment.harness == "pi")
1333                .and_then(|assessment| assessment.skip_reason),
1334            Some("pi_incompatible")
1335        );
1336    }
1337
1338    #[test]
1339    fn native_match_without_auth_falls_through() {
1340        let installed = installed(&["claude", "pi"]);
1341        let input = routing_input(
1342            "claude-opus-4-7",
1343            Some("anthropic"),
1344            None,
1345            None,
1346            &installed,
1347            None,
1348            (None, None, None),
1349        );
1350
1351        let trace = evaluate_candidates_with_auth(&input, never_authed);
1352
1353        assert_eq!(trace.harness, "pi");
1354        assert_eq!(trace.selection_kind, SelectionKind::Auto);
1355        assert_eq!(trace.match_evidence, MatchEvidence::Passthrough);
1356        assert_eq!(trace.candidates_tried[0], "claude");
1357        assert_eq!(trace.candidates_tried[1], "pi");
1358        assert_eq!(
1359            trace
1360                .assessments
1361                .first()
1362                .and_then(|assessment| assessment.skip_reason),
1363            Some("native_auth_unavailable")
1364        );
1365    }
1366
1367    #[test]
1368    fn pi_or_cursor_installed_returns_passthrough() {
1369        let installed = installed(&["cursor"]);
1370        let input = routing_input(
1371            "gemini-2.5-pro",
1372            Some("google"),
1373            None,
1374            None,
1375            &installed,
1376            None,
1377            (None, None, None),
1378        );
1379
1380        let trace = evaluate_candidates_with_auth(&input, never_authed);
1381
1382        assert_eq!(trace.harness, "cursor");
1383        assert_eq!(trace.match_evidence, MatchEvidence::Passthrough);
1384    }
1385
1386    #[test]
1387    fn cursor_with_no_probe_falls_back_to_passthrough() {
1388        let installed = installed(&["cursor"]);
1389        let input = routing_input(
1390            "gpt-5.5",
1391            Some("openai"),
1392            None,
1393            None,
1394            &installed,
1395            None,
1396            (None, None, None),
1397        );
1398
1399        let trace = evaluate_candidates_with_auth(&input, never_authed);
1400        assert_eq!(trace.harness, "cursor");
1401        assert_eq!(trace.match_evidence, MatchEvidence::Passthrough);
1402    }
1403
1404    #[test]
1405    fn cursor_prefix_match_returns_confirmed_with_candidate_slugs() {
1406        let installed = installed(&["cursor"]);
1407        let cursor_probe = CursorProbeResult {
1408            slugs: vec!["gpt-5.5-high".to_string(), "gpt-5.5-low".to_string()],
1409            model_probe_success: true,
1410            error: None,
1411        };
1412        let input = routing_input(
1413            "gpt-5.5",
1414            Some("openai"),
1415            None,
1416            None,
1417            &installed,
1418            None,
1419            (None, None, Some(&cursor_probe)),
1420        );
1421
1422        let trace = evaluate_candidates_with_auth(&input, never_authed);
1423        assert_eq!(trace.harness, "cursor");
1424        assert_eq!(trace.match_evidence, MatchEvidence::Confirmed);
1425        let cursor_assessment = trace
1426            .assessments
1427            .iter()
1428            .find(|assessment| assessment.harness == "cursor")
1429            .expect("cursor assessment should exist");
1430        assert_eq!(
1431            cursor_assessment.candidate_slugs,
1432            vec!["gpt-5.5-high".to_string(), "gpt-5.5-low".to_string()]
1433        );
1434        assert_eq!(cursor_assessment.chosen_slug.as_deref(), Some("gpt-5.5"));
1435    }
1436
1437    #[test]
1438    fn cursor_exact_match_returns_confirmed() {
1439        let installed = installed(&["cursor"]);
1440        let cursor_probe = CursorProbeResult {
1441            slugs: vec!["gpt-5.5".to_string(), "gpt-5.5-high".to_string()],
1442            model_probe_success: true,
1443            error: None,
1444        };
1445        let input = routing_input(
1446            "gpt-5.5",
1447            Some("openai"),
1448            None,
1449            None,
1450            &installed,
1451            None,
1452            (None, None, Some(&cursor_probe)),
1453        );
1454
1455        let trace = evaluate_candidates_with_auth(&input, never_authed);
1456        assert_eq!(trace.harness, "cursor");
1457        assert_eq!(trace.match_evidence, MatchEvidence::Confirmed);
1458        let cursor_assessment = trace
1459            .assessments
1460            .iter()
1461            .find(|assessment| assessment.harness == "cursor")
1462            .expect("cursor assessment should exist");
1463        assert_eq!(
1464            cursor_assessment.candidate_slugs,
1465            vec!["gpt-5.5".to_string()]
1466        );
1467        assert_eq!(cursor_assessment.chosen_slug.as_deref(), Some("gpt-5.5"));
1468    }
1469
1470    #[test]
1471    fn cursor_no_match_falls_through() {
1472        let installed = installed(&["cursor"]);
1473        let cursor_probe = CursorProbeResult {
1474            slugs: vec!["claude-opus-4-7-high".to_string()],
1475            model_probe_success: true,
1476            error: None,
1477        };
1478        let input = routing_input(
1479            "gpt-5.5",
1480            Some("openai"),
1481            None,
1482            None,
1483            &installed,
1484            None,
1485            (None, None, Some(&cursor_probe)),
1486        );
1487
1488        let trace = evaluate_candidates_with_auth(&input, never_authed);
1489        assert_eq!(trace.harness, "pi");
1490        assert_eq!(trace.selection_kind, SelectionKind::HardcodedDefault);
1491        assert_eq!(
1492            trace
1493                .assessments
1494                .iter()
1495                .find(|assessment| assessment.harness == "cursor")
1496                .and_then(|assessment| assessment.skip_reason),
1497            Some("no_model_match")
1498        );
1499    }
1500
1501    #[test]
1502    fn compatible_pi_probe_returns_confirmed() {
1503        let installed = installed(&["pi"]);
1504        let pi_probe = PiProbeResult {
1505            compatible: true,
1506            model_slugs: HashSet::from(["google/gemini-2.5-pro".to_string()]),
1507            ..PiProbeResult::default()
1508        };
1509        let input = routing_input(
1510            "gemini-2.5-pro",
1511            Some("google"),
1512            None,
1513            None,
1514            &installed,
1515            None,
1516            (None, Some(&pi_probe), None),
1517        );
1518
1519        let trace = evaluate_candidates_with_auth(&input, never_authed);
1520
1521        assert_eq!(trace.harness, "pi");
1522        assert_eq!(trace.match_evidence, MatchEvidence::Confirmed);
1523    }
1524
1525    #[test]
1526    fn provider_constraint_accepts_variant_provider_name() {
1527        let installed = installed(&["pi", "opencode"]);
1528        let pi_probe = PiProbeResult {
1529            compatible: true,
1530            model_slugs: HashSet::from(["openai-codex/gpt-5.4-mini".to_string()]),
1531            ..PiProbeResult::default()
1532        };
1533        let opencode_probe = OpenCodeProbeResult {
1534            model_slugs: vec!["openai/gpt-5.4-mini".to_string()],
1535            model_probe_success: true,
1536            error: None,
1537        };
1538        let input = RoutingInput {
1539            model_id: "gpt-5.4-mini",
1540            provider_for_order: Some("openai"),
1541            provider_constraint: Some("openai"),
1542            settings_provider_order: None,
1543            settings_harness_order: None,
1544            config_default_harness: None,
1545            installed_harnesses: &installed,
1546            linked_harnesses: None,
1547            opencode_probe_result: Some(&opencode_probe),
1548            pi_probe_result: Some(&pi_probe),
1549            cursor_probe_result: None,
1550            catalog_model_slugs: None,
1551        };
1552
1553        let trace = evaluate_candidates_with_auth(&input, never_authed);
1554
1555        assert_eq!(trace.harness, "pi");
1556        assert_eq!(trace.match_evidence, MatchEvidence::Constrained);
1557        assert_eq!(
1558            trace
1559                .assessments
1560                .iter()
1561                .find(|assessment| assessment.harness == "pi")
1562                .and_then(|assessment| assessment.chosen_slug.as_deref()),
1563            Some("openai-codex/gpt-5.4-mini")
1564        );
1565    }
1566
1567    #[test]
1568    fn bare_direct_model_prefers_unknown_provider_ladder_and_pi_slug() {
1569        let installed = installed(&["codex", "pi", "opencode"]);
1570        let pi_probe = PiProbeResult {
1571            compatible: true,
1572            model_slugs: HashSet::from(["openai-codex/gpt-5.4".to_string()]),
1573            ..PiProbeResult::default()
1574        };
1575        let input = RoutingInput {
1576            model_id: "gpt-5.4",
1577            provider_for_order: None,
1578            provider_constraint: None,
1579            settings_provider_order: None,
1580            settings_harness_order: None,
1581            config_default_harness: None,
1582            installed_harnesses: &installed,
1583            linked_harnesses: None,
1584            opencode_probe_result: None,
1585            pi_probe_result: Some(&pi_probe),
1586            cursor_probe_result: None,
1587            catalog_model_slugs: None,
1588        };
1589
1590        let trace = evaluate_candidates_with_auth(&input, always_authed);
1591
1592        assert_eq!(trace.harness, "pi");
1593        assert_eq!(trace.match_evidence, MatchEvidence::Confirmed);
1594        assert_eq!(trace.candidates_tried, vec!["pi".to_string()]);
1595        assert_eq!(
1596            trace
1597                .assessments
1598                .iter()
1599                .find(|assessment| assessment.harness == "pi")
1600                .and_then(|assessment| assessment.chosen_slug.as_deref()),
1601            Some("openai-codex/gpt-5.4")
1602        );
1603    }
1604
1605    #[test]
1606    fn provider_order_ranking_is_lenient_for_known_variants() {
1607        let provider_order = vec!["openai".to_string(), "anthropic".to_string()];
1608        assert_eq!(
1609            probe_match::provider_order_rank("openai-codex", &provider_order),
1610            0
1611        );
1612        assert_eq!(
1613            probe_match::provider_order_rank("anthropic-claude", &provider_order),
1614            1
1615        );
1616        assert_eq!(
1617            probe_match::provider_order_rank("openrouter", &provider_order),
1618            usize::MAX
1619        );
1620    }
1621
1622    #[test]
1623    fn unknown_provider_order_entries_warn_but_do_not_block_routing() {
1624        let installed = installed(&["opencode"]);
1625        let provider_order = vec!["future-provider".to_string()];
1626        let probe = OpenCodeProbeResult {
1627            model_slugs: vec!["openai/gpt-5.4-mini".to_string()],
1628            model_probe_success: true,
1629            error: None,
1630        };
1631        let input = RoutingInput {
1632            model_id: "gpt-5.4-mini",
1633            provider_for_order: Some("openai"),
1634            provider_constraint: None,
1635            settings_provider_order: Some(&provider_order),
1636            settings_harness_order: None,
1637            config_default_harness: None,
1638            installed_harnesses: &installed,
1639            linked_harnesses: None,
1640            opencode_probe_result: Some(&probe),
1641            pi_probe_result: None,
1642            cursor_probe_result: None,
1643            catalog_model_slugs: None,
1644        };
1645
1646        let trace = evaluate_candidates_with_auth(&input, never_authed);
1647
1648        assert_eq!(trace.harness, "opencode");
1649        assert_eq!(trace.match_evidence, MatchEvidence::Confirmed);
1650        assert!(trace.diagnostics.iter().any(|diagnostic| {
1651            diagnostic
1652                .contains("settings.provider_order contains unknown provider `future-provider`")
1653        }));
1654    }
1655
1656    #[test]
1657    fn incompatible_pi_probe_skips_to_next_candidate() {
1658        let installed = installed(&["pi", "cursor"]);
1659        let pi_probe = PiProbeResult {
1660            compatible: false,
1661            ..PiProbeResult::default()
1662        };
1663        let input = routing_input(
1664            "gemini-2.5-pro",
1665            Some("google"),
1666            None,
1667            None,
1668            &installed,
1669            None,
1670            (None, Some(&pi_probe), None),
1671        );
1672
1673        let trace = evaluate_candidates_with_auth(&input, never_authed);
1674
1675        assert_eq!(trace.harness, "cursor");
1676        assert_eq!(
1677            trace
1678                .assessments
1679                .iter()
1680                .find(|assessment| assessment.harness == "pi")
1681                .and_then(|assessment| assessment.skip_reason),
1682            Some("pi_incompatible")
1683        );
1684    }
1685
1686    #[test]
1687    fn opencode_positive_probe_returns_likely() {
1688        let installed = installed(&["opencode"]);
1689        let probe = OpenCodeProbeResult {
1690            model_slugs: vec!["openai/gpt-5".to_string()],
1691            model_probe_success: true,
1692            error: None,
1693        };
1694        let input = routing_input(
1695            "gpt-5",
1696            Some("openai"),
1697            None,
1698            None,
1699            &installed,
1700            None,
1701            (Some(&probe), None, None),
1702        );
1703
1704        let trace = evaluate_candidates_with_auth(&input, never_authed);
1705
1706        assert_eq!(trace.harness, "opencode");
1707        assert_eq!(trace.match_evidence, MatchEvidence::Confirmed);
1708    }
1709
1710    #[test]
1711    fn opencode_negative_probe_falls_through() {
1712        let installed = installed(&["opencode", "cursor"]);
1713        let probe = OpenCodeProbeResult {
1714            model_slugs: Vec::new(),
1715            model_probe_success: true,
1716            error: None,
1717        };
1718        let input = routing_input(
1719            "gpt-5",
1720            Some("openai"),
1721            None,
1722            None,
1723            &installed,
1724            None,
1725            (Some(&probe), None, None),
1726        );
1727
1728        let trace = evaluate_candidates_with_auth(&input, never_authed);
1729
1730        assert_eq!(trace.harness, "cursor");
1731        assert_eq!(trace.match_evidence, MatchEvidence::Passthrough);
1732        assert_eq!(
1733            trace
1734                .assessments
1735                .iter()
1736                .find(|assessment| assessment.harness == "opencode")
1737                .and_then(|assessment| assessment.skip_reason),
1738            Some("no_model_match")
1739        );
1740    }
1741
1742    #[test]
1743    fn link_filtering_reduces_candidates() {
1744        let installed = installed(&["codex", "pi"]);
1745        let linked_harnesses = vec!["pi".to_string()];
1746        let input = routing_input(
1747            "gpt-5",
1748            Some("openai"),
1749            None,
1750            None,
1751            &installed,
1752            Some(&linked_harnesses),
1753            (None, None, None),
1754        );
1755
1756        let trace = evaluate_candidates_with_auth(&input, always_authed);
1757
1758        assert_eq!(trace.harness, "pi");
1759        assert_eq!(trace.candidates_tried, vec!["pi"]);
1760    }
1761
1762    #[test]
1763    fn settings_harness_order_overrides_provider_order() {
1764        let installed = installed(&["codex", "pi"]);
1765        let order = vec!["pi".to_string(), "codex".to_string()];
1766        let input = routing_input(
1767            "gpt-5",
1768            Some("openai"),
1769            Some(&order),
1770            None,
1771            &installed,
1772            None,
1773            (None, None, None),
1774        );
1775
1776        let trace = evaluate_candidates_with_auth(&input, always_authed);
1777
1778        assert_eq!(trace.source, RouteSource::ConfigOrder);
1779        assert_eq!(trace.harness, "codex");
1780        assert_eq!(trace.harness_order_position, Some(1));
1781        assert_eq!(trace.match_evidence, MatchEvidence::Confirmed);
1782    }
1783
1784    #[test]
1785    fn empty_harness_order_falls_through_to_provider() {
1786        let installed = installed(&["codex"]);
1787        let order: Vec<String> = Vec::new();
1788        let input = routing_input(
1789            "gpt-5",
1790            Some("openai"),
1791            Some(&order),
1792            None,
1793            &installed,
1794            None,
1795            (None, None, None),
1796        );
1797
1798        let trace = evaluate_candidates_with_auth(&input, always_authed);
1799
1800        assert_eq!(trace.source, RouteSource::Provider);
1801        assert_eq!(trace.harness, "codex");
1802        assert!(
1803            trace
1804                .diagnostics
1805                .iter()
1806                .any(|diagnostic| diagnostic.contains("settings.harness_order is empty"))
1807        );
1808    }
1809
1810    #[test]
1811    fn uses_config_default_fallback() {
1812        let installed = installed(&[]);
1813        let input = routing_input(
1814            "gpt-5",
1815            Some("openai"),
1816            None,
1817            Some("Pi"),
1818            &installed,
1819            None,
1820            (None, None, None),
1821        );
1822
1823        let trace = evaluate_candidates_with_auth(&input, never_authed);
1824
1825        assert_eq!(trace.source, RouteSource::ConfigDefault);
1826        assert_eq!(trace.selection_kind, SelectionKind::ConfigDefault);
1827        assert_eq!(trace.harness, "pi");
1828        assert_eq!(trace.match_evidence, MatchEvidence::Passthrough);
1829    }
1830
1831    #[test]
1832    fn uses_hardcoded_pi_fallback_with_warning() {
1833        let installed = installed(&[]);
1834        let input = routing_input(
1835            "model",
1836            None,
1837            None,
1838            None,
1839            &installed,
1840            None,
1841            (None, None, None),
1842        );
1843
1844        let trace = evaluate_candidates_with_auth(&input, never_authed);
1845
1846        assert_eq!(trace.source, RouteSource::HardcodedDefault);
1847        assert_eq!(trace.selection_kind, SelectionKind::HardcodedDefault);
1848        assert_eq!(trace.harness, "pi");
1849        assert!(
1850            trace
1851                .diagnostics
1852                .iter()
1853                .any(|diagnostic| { diagnostic.contains("defaulting to `pi`") })
1854        );
1855    }
1856
1857    #[test]
1858    fn linked_constraints_apply_to_default_and_hardcoded_fallbacks() {
1859        let installed = installed(&["codex"]);
1860        let linked_harnesses = vec!["claude".to_string()];
1861
1862        let with_config_default = routing_input(
1863            "gpt-5",
1864            Some("openai"),
1865            None,
1866            Some("pi"),
1867            &installed,
1868            Some(&linked_harnesses),
1869            (None, None, None),
1870        );
1871        let with_default_trace = evaluate_candidates_with_auth(&with_config_default, never_authed);
1872        assert_eq!(with_default_trace.source, RouteSource::Provider);
1873        assert_eq!(
1874            with_default_trace.selection_kind,
1875            SelectionKind::LinkedFallback
1876        );
1877        assert_eq!(with_default_trace.harness, "claude");
1878        assert_eq!(with_default_trace.candidates_tried, vec!["claude"]);
1879        assert!(with_default_trace.diagnostics.iter().any(|diagnostic| {
1880            diagnostic.contains(
1881                "settings.default_harness is excluded by known linked harness constraints",
1882            )
1883        }));
1884
1885        let without_config_default = routing_input(
1886            "gpt-5",
1887            Some("openai"),
1888            None,
1889            None,
1890            &installed,
1891            Some(&linked_harnesses),
1892            (None, None, None),
1893        );
1894        let hardcoded_trace = evaluate_candidates_with_auth(&without_config_default, never_authed);
1895        assert_eq!(hardcoded_trace.source, RouteSource::Provider);
1896        assert_eq!(
1897            hardcoded_trace.selection_kind,
1898            SelectionKind::LinkedFallback
1899        );
1900        assert_eq!(hardcoded_trace.harness, "claude");
1901        assert!(
1902            hardcoded_trace
1903                .diagnostics
1904                .iter()
1905                .any(|diagnostic| { diagnostic.contains("selecting linked harness `claude`") })
1906        );
1907    }
1908
1909    #[test]
1910    fn linked_default_harness_is_allowed_when_linked() {
1911        let installed = installed(&[]);
1912        let linked_harnesses = vec!["pi".to_string()];
1913        let trace = evaluate_candidates_with_auth(
1914            &routing_input(
1915                "gpt-5",
1916                Some("openai"),
1917                None,
1918                Some("pi"),
1919                &installed,
1920                Some(&linked_harnesses),
1921                (None, None, None),
1922            ),
1923            never_authed,
1924        );
1925
1926        assert_eq!(trace.source, RouteSource::ConfigDefault);
1927        assert_eq!(trace.harness, "pi");
1928    }
1929
1930    #[test]
1931    fn fixed_harness_evaluation_has_no_fallback() {
1932        let installed = installed(&[]);
1933        let input = routing_input(
1934            "gpt-5",
1935            Some("openai"),
1936            None,
1937            Some("pi"),
1938            &installed,
1939            None,
1940            (None, None, None),
1941        );
1942        let assessment = evaluate_fixed_harness_with_auth(&input, "codex", never_authed);
1943
1944        assert_eq!(assessment.harness, "codex");
1945        assert!(!assessment.installed);
1946        assert_eq!(assessment.match_evidence, None);
1947        assert_eq!(assessment.skip_reason, Some("not_installed"));
1948    }
1949
1950    #[test]
1951    fn fixed_native_harness_enforces_provider_constraint() {
1952        let installed = installed(&["codex"]);
1953        let input = RoutingInput {
1954            model_id: "gpt-5",
1955            provider_for_order: Some("openai"),
1956            provider_constraint: Some("anthropic"),
1957            settings_provider_order: None,
1958            settings_harness_order: None,
1959            config_default_harness: None,
1960            installed_harnesses: &installed,
1961            linked_harnesses: None,
1962            opencode_probe_result: None,
1963            pi_probe_result: None,
1964            cursor_probe_result: None,
1965            catalog_model_slugs: None,
1966        };
1967
1968        let assessment = evaluate_fixed_harness_with_auth(&input, "codex", always_authed);
1969
1970        assert_eq!(assessment.harness, "codex");
1971        assert!(assessment.installed);
1972        assert_eq!(assessment.match_evidence, None);
1973        assert_eq!(
1974            assessment.skip_reason,
1975            Some("provider_constraint_unsatisfied")
1976        );
1977    }
1978
1979    #[test]
1980    fn fixed_native_codex_accepts_openai_codex_provider_variant() {
1981        let installed = installed(&["codex"]);
1982        let input = RoutingInput {
1983            model_id: "gpt-5",
1984            provider_for_order: Some("openai-codex"),
1985            provider_constraint: Some("openai-codex"),
1986            settings_provider_order: None,
1987            settings_harness_order: None,
1988            config_default_harness: None,
1989            installed_harnesses: &installed,
1990            linked_harnesses: None,
1991            opencode_probe_result: None,
1992            pi_probe_result: None,
1993            cursor_probe_result: None,
1994            catalog_model_slugs: None,
1995        };
1996
1997        let assessment = evaluate_fixed_harness_with_auth(&input, "codex", always_authed);
1998
1999        assert_eq!(assessment.harness, "codex");
2000        assert!(assessment.installed);
2001        assert_eq!(assessment.match_evidence, Some(MatchEvidence::Constrained));
2002        assert_eq!(assessment.skip_reason, None);
2003    }
2004
2005    #[test]
2006    fn fixed_native_claude_accepts_anthropic_claude_provider_variant() {
2007        let installed = installed(&["claude"]);
2008        let input = RoutingInput {
2009            model_id: "claude-opus-4-7",
2010            provider_for_order: Some("anthropic-claude"),
2011            provider_constraint: Some("anthropic-claude"),
2012            settings_provider_order: None,
2013            settings_harness_order: None,
2014            config_default_harness: None,
2015            installed_harnesses: &installed,
2016            linked_harnesses: None,
2017            opencode_probe_result: None,
2018            pi_probe_result: None,
2019            cursor_probe_result: None,
2020            catalog_model_slugs: None,
2021        };
2022
2023        let assessment = evaluate_fixed_harness_with_auth(&input, "claude", always_authed);
2024
2025        assert_eq!(assessment.harness, "claude");
2026        assert!(assessment.installed);
2027        assert_eq!(assessment.match_evidence, Some(MatchEvidence::Constrained));
2028        assert_eq!(assessment.skip_reason, None);
2029    }
2030
2031    #[test]
2032    fn selected_chosen_slug_evidence_prefers_selected_harness_assessment() {
2033        let trace = RoutingTrace {
2034            source: RouteSource::Provider,
2035            selection_kind: SelectionKind::Auto,
2036            match_evidence: MatchEvidence::Confirmed,
2037            harness: "pi".to_string(),
2038            harness_order_position: None,
2039            candidates_tried: vec!["pi".to_string()],
2040            assessments: vec![
2041                CandidateAssessment {
2042                    harness: "opencode".to_string(),
2043                    installed: true,
2044                    candidate_slugs: vec!["openai/gpt-5.4-mini".to_string()],
2045                    filtered_slugs: vec!["openai/gpt-5.4-mini".to_string()],
2046                    chosen_slug: Some("openai/gpt-5.4-mini".to_string()),
2047                    chosen_model: Some("gpt-5.4-mini".to_string()),
2048                    match_evidence: Some(MatchEvidence::Confirmed),
2049                    skip_reason: None,
2050                },
2051                CandidateAssessment {
2052                    harness: "pi".to_string(),
2053                    installed: true,
2054                    candidate_slugs: vec!["openai/gpt-5.4-mini".to_string()],
2055                    filtered_slugs: vec!["openai/gpt-5.4-mini".to_string()],
2056                    chosen_slug: Some("openai/gpt-5.4-mini".to_string()),
2057                    chosen_model: Some("gpt-5.4-mini".to_string()),
2058                    match_evidence: Some(MatchEvidence::Constrained),
2059                    skip_reason: None,
2060                },
2061            ],
2062            diagnostics: vec!["diag".to_string()],
2063        };
2064
2065        let selected = trace
2066            .selected_chosen_slug_evidence()
2067            .expect("selected slug evidence should be present");
2068        assert_eq!(selected.slug, "openai/gpt-5.4-mini");
2069        assert_eq!(selected.match_evidence, Some(MatchEvidence::Constrained));
2070        assert_eq!(trace.selected_harness(), "pi");
2071        assert_eq!(trace.selected_selection_kind(), SelectionKind::Auto);
2072        assert_eq!(trace.selected_match_evidence(), MatchEvidence::Confirmed);
2073        assert_eq!(trace.selected_diagnostics(), vec!["diag".to_string()]);
2074    }
2075
2076    #[test]
2077    fn constrained_slug_selection_prefers_exact_provider_over_variant() {
2078        let installed = installed(&["pi"]);
2079        let pi_probe = PiProbeResult {
2080            compatible: true,
2081            model_slugs: HashSet::from([
2082                "openai-codex/gpt-5.4-mini".to_string(),
2083                "openai/gpt-5.4-mini".to_string(),
2084            ]),
2085            ..PiProbeResult::default()
2086        };
2087        let input = RoutingInput {
2088            model_id: "gpt-5.4-mini",
2089            provider_for_order: Some("openai"),
2090            provider_constraint: Some("openai"),
2091            settings_provider_order: None,
2092            settings_harness_order: None,
2093            config_default_harness: None,
2094            installed_harnesses: &installed,
2095            linked_harnesses: None,
2096            opencode_probe_result: None,
2097            pi_probe_result: Some(&pi_probe),
2098            cursor_probe_result: None,
2099            catalog_model_slugs: None,
2100        };
2101
2102        let trace = evaluate_candidates_with_auth(&input, always_authed);
2103        assert_eq!(trace.harness, "pi");
2104        assert_eq!(
2105            trace
2106                .selected_chosen_slug_evidence()
2107                .expect("selected chosen slug evidence")
2108                .slug,
2109            "openai/gpt-5.4-mini"
2110        );
2111    }
2112
2113    #[test]
2114    fn unconstrained_slug_selection_prefers_openai_codex_variant_for_pi() {
2115        let installed = installed(&["pi"]);
2116        let pi_probe = PiProbeResult {
2117            compatible: true,
2118            model_slugs: HashSet::from([
2119                "openai-codex/gpt-5.4-mini".to_string(),
2120                "openai/gpt-5.4-mini".to_string(),
2121            ]),
2122            ..PiProbeResult::default()
2123        };
2124        let input = RoutingInput {
2125            model_id: "gpt-5.4-mini",
2126            provider_for_order: Some("openai"),
2127            provider_constraint: None,
2128            settings_provider_order: None,
2129            settings_harness_order: None,
2130            config_default_harness: None,
2131            installed_harnesses: &installed,
2132            linked_harnesses: None,
2133            opencode_probe_result: None,
2134            pi_probe_result: Some(&pi_probe),
2135            cursor_probe_result: None,
2136            catalog_model_slugs: None,
2137        };
2138
2139        let trace = evaluate_candidates_with_auth(&input, always_authed);
2140        assert_eq!(trace.harness, "pi");
2141        assert_eq!(
2142            trace
2143                .selected_chosen_slug_evidence()
2144                .expect("selected chosen slug evidence")
2145                .slug,
2146            "openai-codex/gpt-5.4-mini"
2147        );
2148    }
2149}