Skip to main content

mars_agents/routing/
mod.rs

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