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