Skip to main content

mars_agents/routing/
mod.rs

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