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