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