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 if input.model_id.trim().is_empty() {
312        filter_candidates_by_links(
313            models::harness::VALID_HARNESSES
314                .iter()
315                .map(|harness| (*harness).to_string())
316                .collect(),
317            linked_harnesses_set.as_ref(),
318        )
319        .into_iter()
320        .map(|harness| (harness, None))
321        .collect::<Vec<_>>()
322    } else {
323        let provider_for_order = input.provider_for_order.unwrap_or("unknown");
324        filter_candidates_by_links(
325            models::harness::harness_candidates_for_provider(provider_for_order),
326            linked_harnesses_set.as_ref(),
327        )
328        .into_iter()
329        .map(|harness| (harness, None))
330        .collect::<Vec<_>>()
331    };
332
333    let mut candidates_tried = Vec::new();
334    let mut assessments = Vec::new();
335
336    for (harness, harness_order_position) in candidates {
337        let assessment = candidate_match_evidence_with_auth(
338            input,
339            &harness,
340            Some(parsed_provider_order.as_slice()),
341            &auth_check,
342        );
343
344        candidates_tried.push(harness.clone());
345        let match_evidence = assessment.match_evidence;
346        assessments.push(assessment);
347
348        if let Some(match_evidence) = match_evidence {
349            return RoutingTrace {
350                source: candidate_source,
351                selection_kind: SelectionKind::Auto,
352                match_evidence,
353                harness,
354                harness_order_position,
355                candidates_tried,
356                assessments,
357                diagnostics,
358            };
359        }
360    }
361
362    if input.settings_harness_order.is_some()
363        && let Some(warning) = format_harness_order_fallback_warning(
364            harness_order_failure.as_ref(),
365            effective_config_default_harness.is_some(),
366            has_link_constraints,
367        )
368    {
369        diagnostics.push(warning);
370    }
371
372    if let Some(harness) = effective_config_default_harness {
373        return RoutingTrace {
374            source: RouteSource::ConfigDefault,
375            selection_kind: SelectionKind::ConfigDefault,
376            match_evidence: MatchEvidence::Passthrough,
377            harness,
378            harness_order_position: None,
379            candidates_tried,
380            assessments,
381            diagnostics,
382        };
383    }
384
385    if let Some(known_links) = linked_harnesses {
386        let harness = known_links
387            .first()
388            .expect("linked_harnesses is non-empty")
389            .clone();
390        diagnostics.push(format!(
391            "known linked harness constraints left no eligible auto-routing candidates; selecting linked harness `{harness}` without unrelated fallback"
392        ));
393        candidates_tried.push(harness.clone());
394
395        return RoutingTrace {
396            source: candidate_source,
397            selection_kind: SelectionKind::LinkedFallback,
398            match_evidence: MatchEvidence::Passthrough,
399            harness,
400            harness_order_position: None,
401            candidates_tried,
402            assessments,
403            diagnostics,
404        };
405    }
406
407    diagnostics
408        .push("harness not set by CLI/profile/alias/provider/config; defaulting to `pi`".into());
409
410    RoutingTrace {
411        source: RouteSource::HardcodedDefault,
412        selection_kind: SelectionKind::HardcodedDefault,
413        match_evidence: MatchEvidence::Passthrough,
414        harness: "pi".to_string(),
415        harness_order_position: None,
416        candidates_tried,
417        assessments,
418        diagnostics,
419    }
420}
421
422/// Normalize and validate config default_harness. Returns normalized name or None with warning.
423pub fn normalize_config_default_harness(
424    config_default_harness: Option<&str>,
425    warnings: &mut Vec<String>,
426) -> Option<String> {
427    match config_default_harness {
428        Some(value) => match models::harness::normalize_harness_name(value) {
429            Some(valid) => Some(valid),
430            None => {
431                warnings.push(format!(
432                    "settings.default_harness `{value}` is invalid; expected one of: {}",
433                    models::harness::VALID_HARNESSES.join(", ")
434                ));
435                None
436            }
437        },
438        None => None,
439    }
440}
441
442fn filter_candidate_pairs_by_links(
443    candidates: &mut Vec<(String, Option<usize>)>,
444    linked_harnesses: Option<&HashSet<&str>>,
445) {
446    if let Some(linked_harnesses) = linked_harnesses {
447        candidates.retain(|(harness, _)| linked_harnesses.contains(harness.as_str()));
448    }
449}
450
451fn filter_candidates_by_links(
452    candidates: Vec<String>,
453    linked_harnesses: Option<&HashSet<&str>>,
454) -> Vec<String> {
455    let Some(linked_harnesses) = linked_harnesses else {
456        return candidates;
457    };
458
459    candidates
460        .into_iter()
461        .filter(|harness| linked_harnesses.contains(harness.as_str()))
462        .collect()
463}
464
465fn candidate_match_evidence_with_auth<F>(
466    input: &RoutingInput<'_>,
467    harness: &str,
468    provider_order: Option<&[String]>,
469    auth_check: &F,
470) -> CandidateAssessment
471where
472    F: Fn(&str) -> bool,
473{
474    if !input.installed_harnesses.contains(harness) {
475        return CandidateAssessment {
476            harness: harness.to_string(),
477            installed: false,
478            candidate_slugs: Vec::new(),
479            filtered_slugs: Vec::new(),
480            chosen_slug: None,
481            chosen_model: None,
482            match_evidence: None,
483            skip_reason: Some("not_installed"),
484        };
485    }
486
487    if is_native_harness(harness)
488        && provider_constraint_excludes_native_harness(input.provider_constraint, harness)
489    {
490        return CandidateAssessment {
491            harness: harness.to_string(),
492            installed: true,
493            candidate_slugs: Vec::new(),
494            filtered_slugs: Vec::new(),
495            chosen_slug: None,
496            chosen_model: None,
497            match_evidence: None,
498            skip_reason: Some("provider_constraint_unsatisfied"),
499        };
500    }
501
502    if input.model_id.trim().is_empty() {
503        return CandidateAssessment {
504            harness: harness.to_string(),
505            installed: true,
506            candidate_slugs: Vec::new(),
507            filtered_slugs: Vec::new(),
508            chosen_slug: None,
509            chosen_model: None,
510            match_evidence: Some(MatchEvidence::Passthrough),
511            skip_reason: None,
512        };
513    }
514
515    if is_native_match(input.provider_for_order, harness) {
516        if auth_check(harness) {
517            return CandidateAssessment {
518                harness: harness.to_string(),
519                installed: true,
520                candidate_slugs: Vec::new(),
521                filtered_slugs: Vec::new(),
522                chosen_slug: None,
523                chosen_model: Some(input.model_id.to_string()),
524                match_evidence: Some(match_evidence_for_match(input.provider_constraint)),
525                skip_reason: None,
526            };
527        }
528
529        return CandidateAssessment {
530            harness: harness.to_string(),
531            installed: true,
532            candidate_slugs: Vec::new(),
533            filtered_slugs: Vec::new(),
534            chosen_slug: None,
535            chosen_model: None,
536            match_evidence: None,
537            skip_reason: Some("native_auth_unavailable"),
538        };
539    }
540
541    if harness == "opencode" {
542        let Some(opencode_probe) = input.opencode_probe_result else {
543            return CandidateAssessment {
544                harness: harness.to_string(),
545                installed: true,
546                candidate_slugs: Vec::new(),
547                filtered_slugs: Vec::new(),
548                chosen_slug: None,
549                chosen_model: None,
550                match_evidence: Some(MatchEvidence::Passthrough),
551                skip_reason: None,
552            };
553        };
554        if !opencode_probe.model_probe_success {
555            return CandidateAssessment {
556                harness: harness.to_string(),
557                installed: true,
558                candidate_slugs: Vec::new(),
559                filtered_slugs: Vec::new(),
560                chosen_slug: None,
561                chosen_model: None,
562                match_evidence: Some(MatchEvidence::Passthrough),
563                skip_reason: None,
564            };
565        }
566
567        let selection = select_probe_slug(
568            input.model_id,
569            input.provider_constraint,
570            input.provider_for_order,
571            provider_order,
572            opencode_probe.model_slugs.iter().map(String::as_str),
573        );
574
575        if let Some(chosen_slug) = selection.chosen_slug.clone() {
576            return CandidateAssessment {
577                harness: harness.to_string(),
578                installed: true,
579                candidate_slugs: selection.candidate_slugs,
580                filtered_slugs: selection.filtered_slugs,
581                chosen_model: slug::parse(&chosen_slug).map(|parts| parts.model_id.to_string()),
582                chosen_slug: Some(chosen_slug),
583                match_evidence: Some(match_evidence_for_match(input.provider_constraint)),
584                skip_reason: None,
585            };
586        }
587
588        if !selection.candidate_slugs.is_empty() {
589            return CandidateAssessment {
590                harness: harness.to_string(),
591                installed: true,
592                candidate_slugs: selection.candidate_slugs,
593                filtered_slugs: selection.filtered_slugs,
594                chosen_slug: None,
595                chosen_model: None,
596                match_evidence: None,
597                skip_reason: Some("provider_constraint_unsatisfied"),
598            };
599        }
600
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_slug: None,
607            chosen_model: None,
608            match_evidence: None,
609            skip_reason: Some("no_model_match"),
610        };
611    }
612
613    if harness == "pi" {
614        if let Some(pi_probe) = input.pi_probe_result {
615            if pi_probe.compatible {
616                let selection = select_probe_slug(
617                    input.model_id,
618                    input.provider_constraint,
619                    input.provider_for_order,
620                    provider_order,
621                    pi_probe.model_slugs.iter().map(String::as_str),
622                );
623
624                if let Some(chosen_slug) = selection.chosen_slug.clone() {
625                    return CandidateAssessment {
626                        harness: harness.to_string(),
627                        installed: true,
628                        candidate_slugs: selection.candidate_slugs,
629                        filtered_slugs: selection.filtered_slugs,
630                        chosen_model: slug::parse(&chosen_slug)
631                            .map(|parts| parts.model_id.to_string()),
632                        chosen_slug: Some(chosen_slug),
633                        match_evidence: Some(match_evidence_for_match(input.provider_constraint)),
634                        skip_reason: None,
635                    };
636                }
637
638                if !selection.candidate_slugs.is_empty() {
639                    return CandidateAssessment {
640                        harness: harness.to_string(),
641                        installed: true,
642                        candidate_slugs: selection.candidate_slugs,
643                        filtered_slugs: selection.filtered_slugs,
644                        chosen_slug: None,
645                        chosen_model: None,
646                        match_evidence: None,
647                        skip_reason: Some("provider_constraint_unsatisfied"),
648                    };
649                }
650
651                return CandidateAssessment {
652                    harness: harness.to_string(),
653                    installed: true,
654                    candidate_slugs: selection.candidate_slugs,
655                    filtered_slugs: selection.filtered_slugs,
656                    chosen_slug: None,
657                    chosen_model: None,
658                    match_evidence: None,
659                    skip_reason: Some("no_model_match"),
660                };
661            }
662            return CandidateAssessment {
663                harness: harness.to_string(),
664                installed: true,
665                candidate_slugs: Vec::new(),
666                filtered_slugs: Vec::new(),
667                chosen_slug: None,
668                chosen_model: None,
669                match_evidence: None,
670                skip_reason: Some("pi_incompatible"),
671            };
672        }
673
674        return CandidateAssessment {
675            harness: harness.to_string(),
676            installed: true,
677            candidate_slugs: Vec::new(),
678            filtered_slugs: Vec::new(),
679            chosen_slug: None,
680            chosen_model: None,
681            match_evidence: Some(MatchEvidence::Passthrough),
682            skip_reason: None,
683        };
684    }
685
686    if harness == "cursor" {
687        return CandidateAssessment {
688            harness: harness.to_string(),
689            installed: true,
690            candidate_slugs: Vec::new(),
691            filtered_slugs: Vec::new(),
692            chosen_slug: None,
693            chosen_model: None,
694            match_evidence: Some(MatchEvidence::Passthrough),
695            skip_reason: None,
696        };
697    }
698
699    CandidateAssessment {
700        harness: harness.to_string(),
701        installed: true,
702        candidate_slugs: Vec::new(),
703        filtered_slugs: Vec::new(),
704        chosen_slug: None,
705        chosen_model: None,
706        match_evidence: None,
707        skip_reason: Some("unsupported_candidate"),
708    }
709}
710
711fn native_provider_for_harness(harness: &str) -> Option<&'static str> {
712    match harness {
713        "claude" => Some("anthropic"),
714        "codex" => Some("openai"),
715        _ => None,
716    }
717}
718
719fn is_native_match(provider: Option<&str>, harness: &str) -> bool {
720    provider
721        .map(|provider| slug::provider_matches_native_harness(provider, harness))
722        .unwrap_or(false)
723}
724
725fn is_native_harness(harness: &str) -> bool {
726    matches!(harness, "claude" | "codex")
727}
728
729fn provider_constraint_excludes_native_harness(
730    provider_constraint: Option<&str>,
731    harness: &str,
732) -> bool {
733    let Some(provider_constraint) = provider_constraint else {
734        return false;
735    };
736
737    !slug::provider_matches_native_harness(provider_constraint, harness)
738}
739
740fn match_evidence_for_match(provider_constraint: Option<&str>) -> MatchEvidence {
741    if provider_constraint.is_some() {
742        MatchEvidence::Constrained
743    } else {
744        MatchEvidence::Confirmed
745    }
746}
747
748fn parse_settings_provider_order(
749    provider_order: Option<&[String]>,
750    diagnostics: &mut Vec<String>,
751) -> Vec<String> {
752    let Some(provider_order) = provider_order else {
753        return Vec::new();
754    };
755
756    provider_order
757        .iter()
758        .filter_map(|provider| {
759            let normalized = provider.trim().to_ascii_lowercase();
760            if normalized.is_empty() {
761                return None;
762            }
763            if !is_known_provider_or_variant(&normalized) {
764                diagnostics.push(format!(
765                    "settings.provider_order contains unknown provider `{provider}`; keeping it for forward-compat routing preferences"
766                ));
767            }
768            Some(normalized)
769        })
770        .collect()
771}
772
773fn is_known_provider_or_variant(provider: &str) -> bool {
774    matches!(
775        provider,
776        "anthropic"
777            | "openai"
778            | "google"
779            | "meta"
780            | "mistral"
781            | "deepseek"
782            | "cohere"
783            | "openrouter"
784            | "openai-codex"
785            | "anthropic-claude"
786    )
787}
788
789struct SlugSelection {
790    candidate_slugs: Vec<String>,
791    filtered_slugs: Vec<String>,
792    chosen_slug: Option<String>,
793}
794
795fn select_probe_slug<'a>(
796    model_id: &str,
797    provider_constraint: Option<&str>,
798    provider_for_order: Option<&str>,
799    provider_order: Option<&[String]>,
800    slugs: impl IntoIterator<Item = &'a str>,
801) -> SlugSelection {
802    let known_provider_for_order = provider_for_order.and_then(|provider| {
803        let normalized = provider.trim();
804        (!normalized.is_empty() && !normalized.eq_ignore_ascii_case("unknown"))
805            .then_some(normalized)
806    });
807    let model_matches = slug::find_model_matches(model_id, slugs)
808        .into_iter()
809        .map(|matched| (matched.provider, matched.slug))
810        .collect::<Vec<_>>();
811    let mut candidate_slugs = model_matches
812        .iter()
813        .map(|(_, slug)| slug.clone())
814        .collect::<Vec<_>>();
815    candidate_slugs.sort();
816
817    let mut constrained_matches = model_matches;
818    if let Some(constraint) = provider_constraint {
819        let normalized_constraint = constraint.trim();
820        constrained_matches.retain(|(provider, _)| {
821            slug::provider_match_tier(normalized_constraint, provider).is_some()
822        });
823    }
824    let mut filtered_slugs = constrained_matches
825        .iter()
826        .map(|(_, slug)| slug.clone())
827        .collect::<Vec<_>>();
828    filtered_slugs.sort();
829
830    let chosen_slug = if constrained_matches.is_empty() {
831        None
832    } else if let Some(constraint) = provider_constraint {
833        constrained_matches.sort_by(|(left_provider, left_slug), (right_provider, right_slug)| {
834            slug::provider_match_tier(constraint, left_provider)
835                .cmp(&slug::provider_match_tier(constraint, right_provider))
836                .then_with(|| left_slug.cmp(right_slug))
837        });
838        constrained_matches.first().map(|(_, slug)| slug.clone())
839    } else if let Some(provider_order) = provider_order {
840        if provider_order.is_empty() {
841            constrained_matches.sort_by(
842                |(left_provider, left_slug), (right_provider, right_slug)| {
843                    slug::normalize_provider(left_provider)
844                        .cmp(&slug::normalize_provider(right_provider))
845                        .then_with(|| {
846                            provider_exact_match_rank(known_provider_for_order, left_provider).cmp(
847                                &provider_exact_match_rank(
848                                    known_provider_for_order,
849                                    right_provider,
850                                ),
851                            )
852                        })
853                        .then_with(|| left_slug.cmp(right_slug))
854                },
855            );
856        } else {
857            constrained_matches.sort_by(
858                |(left_provider, left_slug), (right_provider, right_slug)| {
859                    provider_order_rank(left_provider, provider_order)
860                        .cmp(&provider_order_rank(right_provider, provider_order))
861                        .then_with(|| {
862                            provider_exact_match_rank(known_provider_for_order, left_provider).cmp(
863                                &provider_exact_match_rank(
864                                    known_provider_for_order,
865                                    right_provider,
866                                ),
867                            )
868                        })
869                        .then_with(|| left_slug.cmp(right_slug))
870                },
871            );
872        }
873        constrained_matches.first().map(|(_, slug)| slug.clone())
874    } else {
875        constrained_matches.sort_by(|(left_provider, left_slug), (right_provider, right_slug)| {
876            slug::normalize_provider(left_provider)
877                .cmp(&slug::normalize_provider(right_provider))
878                .then_with(|| {
879                    provider_exact_match_rank(known_provider_for_order, left_provider).cmp(
880                        &provider_exact_match_rank(known_provider_for_order, right_provider),
881                    )
882                })
883                .then_with(|| left_slug.cmp(right_slug))
884        });
885        constrained_matches.first().map(|(_, slug)| slug.clone())
886    };
887
888    SlugSelection {
889        candidate_slugs,
890        filtered_slugs,
891        chosen_slug,
892    }
893}
894
895fn provider_exact_match_rank(
896    known_provider_for_order: Option<&str>,
897    candidate_provider: &str,
898) -> u8 {
899    if known_provider_for_order
900        .is_some_and(|provider| slug::providers_exact_match(provider, candidate_provider))
901    {
902        0
903    } else {
904        1
905    }
906}
907
908fn provider_order_rank(provider: &str, provider_order: &[String]) -> usize {
909    let key = slug::normalize_provider(provider);
910    provider_order
911        .iter()
912        .position(|configured| slug::normalize_provider(configured) == key)
913        .unwrap_or(usize::MAX)
914}
915
916fn format_harness_order_fallback_warning(
917    harness_order_failure: Option<&HarnessOrderFailure>,
918    has_config_default_harness: bool,
919    has_link_constraints: bool,
920) -> Option<String> {
921    let mut warning = match harness_order_failure {
922        Some(HarnessOrderFailure::Empty) => "settings.harness_order is empty".to_string(),
923        Some(HarnessOrderFailure::NoneInstalled { valid_candidates }) => format!(
924            "settings.harness_order is set but none of [{}] are installed",
925            valid_candidates.join(", ")
926        ),
927        None => return None,
928    };
929
930    if has_config_default_harness {
931        warning.push_str("; falling through to settings.default_harness");
932    } else if has_link_constraints {
933        warning.push_str("; linked harness constraints prevent unrelated fallback");
934    } else {
935        warning.push_str("; settings.default_harness is unset, falling through to hardcoded `pi`");
936    }
937
938    Some(warning)
939}
940
941#[cfg(test)]
942mod tests {
943    use super::*;
944
945    fn installed(names: &[&str]) -> HashSet<String> {
946        names.iter().map(|name| (*name).to_string()).collect()
947    }
948
949    fn always_authed(_: &str) -> bool {
950        true
951    }
952
953    fn never_authed(_: &str) -> bool {
954        false
955    }
956
957    type ProbeInputs<'a> = (Option<&'a OpenCodeProbeResult>, Option<&'a PiProbeResult>);
958
959    fn routing_input<'a>(
960        model_id: &'a str,
961        provider_for_order: Option<&'a str>,
962        settings_harness_order: Option<&'a [String]>,
963        config_default_harness: Option<&'a str>,
964        installed_harnesses: &'a HashSet<String>,
965        linked_harnesses: Option<&'a [String]>,
966        probe_inputs: ProbeInputs<'a>,
967    ) -> RoutingInput<'a> {
968        let (opencode_probe_result, pi_probe_result) = probe_inputs;
969        RoutingInput {
970            model_id,
971            provider_for_order,
972            provider_constraint: None,
973            settings_provider_order: None,
974            settings_harness_order,
975            config_default_harness,
976            installed_harnesses,
977            linked_harnesses,
978            opencode_probe_result,
979            pi_probe_result,
980        }
981    }
982
983    #[test]
984    fn native_match_with_auth_returns_confirmed() {
985        let installed = installed(&["claude"]);
986        let input = routing_input(
987            "claude-opus-4-7",
988            Some("anthropic"),
989            None,
990            None,
991            &installed,
992            None,
993            (None, None),
994        );
995
996        let trace = evaluate_candidates_with_auth(&input, always_authed);
997
998        assert_eq!(trace.source, RouteSource::Provider);
999        assert_eq!(trace.selection_kind, SelectionKind::Auto);
1000        assert_eq!(trace.harness, "claude");
1001        assert_eq!(trace.match_evidence, MatchEvidence::Confirmed);
1002        assert_eq!(trace.candidates_tried, vec!["claude".to_string()]);
1003    }
1004
1005    #[test]
1006    fn native_match_without_auth_falls_through() {
1007        let installed = installed(&["claude", "pi"]);
1008        let input = routing_input(
1009            "claude-opus-4-7",
1010            Some("anthropic"),
1011            None,
1012            None,
1013            &installed,
1014            None,
1015            (None, None),
1016        );
1017
1018        let trace = evaluate_candidates_with_auth(&input, never_authed);
1019
1020        assert_eq!(trace.harness, "pi");
1021        assert_eq!(trace.selection_kind, SelectionKind::Auto);
1022        assert_eq!(trace.match_evidence, MatchEvidence::Passthrough);
1023        assert_eq!(trace.candidates_tried, vec!["claude", "pi"]);
1024        assert_eq!(
1025            trace
1026                .assessments
1027                .first()
1028                .and_then(|assessment| assessment.skip_reason),
1029            Some("native_auth_unavailable")
1030        );
1031    }
1032
1033    #[test]
1034    fn pi_or_cursor_installed_returns_passthrough() {
1035        let installed = installed(&["cursor"]);
1036        let input = routing_input(
1037            "gemini-2.5-pro",
1038            Some("google"),
1039            None,
1040            None,
1041            &installed,
1042            None,
1043            (None, None),
1044        );
1045
1046        let trace = evaluate_candidates_with_auth(&input, never_authed);
1047
1048        assert_eq!(trace.harness, "cursor");
1049        assert_eq!(trace.match_evidence, MatchEvidence::Passthrough);
1050    }
1051
1052    #[test]
1053    fn compatible_pi_probe_returns_confirmed() {
1054        let installed = installed(&["pi"]);
1055        let pi_probe = PiProbeResult {
1056            compatible: true,
1057            model_slugs: HashSet::from(["google/gemini-2.5-pro".to_string()]),
1058            ..PiProbeResult::default()
1059        };
1060        let input = routing_input(
1061            "gemini-2.5-pro",
1062            Some("google"),
1063            None,
1064            None,
1065            &installed,
1066            None,
1067            (None, Some(&pi_probe)),
1068        );
1069
1070        let trace = evaluate_candidates_with_auth(&input, never_authed);
1071
1072        assert_eq!(trace.harness, "pi");
1073        assert_eq!(trace.match_evidence, MatchEvidence::Confirmed);
1074    }
1075
1076    #[test]
1077    fn provider_constraint_accepts_variant_provider_name() {
1078        let installed = installed(&["pi", "opencode"]);
1079        let pi_probe = PiProbeResult {
1080            compatible: true,
1081            model_slugs: HashSet::from(["openai-codex/gpt-5.4-mini".to_string()]),
1082            ..PiProbeResult::default()
1083        };
1084        let opencode_probe = OpenCodeProbeResult {
1085            model_slugs: vec!["openai/gpt-5.4-mini".to_string()],
1086            model_probe_success: true,
1087            error: None,
1088        };
1089        let input = RoutingInput {
1090            model_id: "gpt-5.4-mini",
1091            provider_for_order: Some("openai"),
1092            provider_constraint: Some("openai"),
1093            settings_provider_order: None,
1094            settings_harness_order: None,
1095            config_default_harness: None,
1096            installed_harnesses: &installed,
1097            linked_harnesses: None,
1098            opencode_probe_result: Some(&opencode_probe),
1099            pi_probe_result: Some(&pi_probe),
1100        };
1101
1102        let trace = evaluate_candidates_with_auth(&input, never_authed);
1103
1104        assert_eq!(trace.harness, "pi");
1105        assert_eq!(trace.match_evidence, MatchEvidence::Constrained);
1106        assert_eq!(
1107            trace
1108                .assessments
1109                .iter()
1110                .find(|assessment| assessment.harness == "pi")
1111                .and_then(|assessment| assessment.chosen_slug.as_deref()),
1112            Some("openai-codex/gpt-5.4-mini")
1113        );
1114    }
1115
1116    #[test]
1117    fn bare_direct_model_prefers_unknown_provider_ladder_and_pi_slug() {
1118        let installed = installed(&["codex", "pi", "opencode"]);
1119        let pi_probe = PiProbeResult {
1120            compatible: true,
1121            model_slugs: HashSet::from(["openai-codex/gpt-5.4".to_string()]),
1122            ..PiProbeResult::default()
1123        };
1124        let input = RoutingInput {
1125            model_id: "gpt-5.4",
1126            provider_for_order: None,
1127            provider_constraint: None,
1128            settings_provider_order: None,
1129            settings_harness_order: None,
1130            config_default_harness: None,
1131            installed_harnesses: &installed,
1132            linked_harnesses: None,
1133            opencode_probe_result: None,
1134            pi_probe_result: Some(&pi_probe),
1135        };
1136
1137        let trace = evaluate_candidates_with_auth(&input, always_authed);
1138
1139        assert_eq!(trace.harness, "pi");
1140        assert_eq!(trace.match_evidence, MatchEvidence::Confirmed);
1141        assert_eq!(trace.candidates_tried, vec!["pi".to_string()]);
1142        assert_eq!(
1143            trace
1144                .assessments
1145                .iter()
1146                .find(|assessment| assessment.harness == "pi")
1147                .and_then(|assessment| assessment.chosen_slug.as_deref()),
1148            Some("openai-codex/gpt-5.4")
1149        );
1150    }
1151
1152    #[test]
1153    fn provider_order_ranking_is_lenient_for_known_variants() {
1154        let provider_order = vec!["openai".to_string(), "anthropic".to_string()];
1155        assert_eq!(provider_order_rank("openai-codex", &provider_order), 0);
1156        assert_eq!(provider_order_rank("anthropic-claude", &provider_order), 1);
1157        assert_eq!(
1158            provider_order_rank("openrouter", &provider_order),
1159            usize::MAX
1160        );
1161    }
1162
1163    #[test]
1164    fn unknown_provider_order_entries_warn_but_do_not_block_routing() {
1165        let installed = installed(&["opencode"]);
1166        let provider_order = vec!["future-provider".to_string()];
1167        let probe = OpenCodeProbeResult {
1168            model_slugs: vec!["openai/gpt-5.4-mini".to_string()],
1169            model_probe_success: true,
1170            error: None,
1171        };
1172        let input = RoutingInput {
1173            model_id: "gpt-5.4-mini",
1174            provider_for_order: Some("openai"),
1175            provider_constraint: None,
1176            settings_provider_order: Some(&provider_order),
1177            settings_harness_order: None,
1178            config_default_harness: None,
1179            installed_harnesses: &installed,
1180            linked_harnesses: None,
1181            opencode_probe_result: Some(&probe),
1182            pi_probe_result: None,
1183        };
1184
1185        let trace = evaluate_candidates_with_auth(&input, never_authed);
1186
1187        assert_eq!(trace.harness, "opencode");
1188        assert_eq!(trace.match_evidence, MatchEvidence::Confirmed);
1189        assert!(trace.diagnostics.iter().any(|diagnostic| {
1190            diagnostic
1191                .contains("settings.provider_order contains unknown provider `future-provider`")
1192        }));
1193    }
1194
1195    #[test]
1196    fn incompatible_pi_probe_skips_to_next_candidate() {
1197        let installed = installed(&["pi", "cursor"]);
1198        let pi_probe = PiProbeResult {
1199            compatible: false,
1200            ..PiProbeResult::default()
1201        };
1202        let input = routing_input(
1203            "gemini-2.5-pro",
1204            Some("google"),
1205            None,
1206            None,
1207            &installed,
1208            None,
1209            (None, Some(&pi_probe)),
1210        );
1211
1212        let trace = evaluate_candidates_with_auth(&input, never_authed);
1213
1214        assert_eq!(trace.harness, "cursor");
1215        assert_eq!(
1216            trace
1217                .assessments
1218                .iter()
1219                .find(|assessment| assessment.harness == "pi")
1220                .and_then(|assessment| assessment.skip_reason),
1221            Some("pi_incompatible")
1222        );
1223    }
1224
1225    #[test]
1226    fn opencode_positive_probe_returns_likely() {
1227        let installed = installed(&["opencode"]);
1228        let probe = OpenCodeProbeResult {
1229            model_slugs: vec!["openai/gpt-5".to_string()],
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, "opencode");
1246        assert_eq!(trace.match_evidence, MatchEvidence::Confirmed);
1247    }
1248
1249    #[test]
1250    fn opencode_negative_probe_falls_through() {
1251        let installed = installed(&["opencode", "cursor"]);
1252        let probe = OpenCodeProbeResult {
1253            model_slugs: Vec::new(),
1254            model_probe_success: true,
1255            error: None,
1256        };
1257        let input = routing_input(
1258            "gpt-5",
1259            Some("openai"),
1260            None,
1261            None,
1262            &installed,
1263            None,
1264            (Some(&probe), None),
1265        );
1266
1267        let trace = evaluate_candidates_with_auth(&input, never_authed);
1268
1269        assert_eq!(trace.harness, "cursor");
1270        assert_eq!(trace.match_evidence, MatchEvidence::Passthrough);
1271        assert_eq!(
1272            trace
1273                .assessments
1274                .iter()
1275                .find(|assessment| assessment.harness == "opencode")
1276                .and_then(|assessment| assessment.skip_reason),
1277            Some("no_model_match")
1278        );
1279    }
1280
1281    #[test]
1282    fn link_filtering_reduces_candidates() {
1283        let installed = installed(&["codex", "pi"]);
1284        let linked_harnesses = vec!["pi".to_string()];
1285        let input = routing_input(
1286            "gpt-5",
1287            Some("openai"),
1288            None,
1289            None,
1290            &installed,
1291            Some(&linked_harnesses),
1292            (None, None),
1293        );
1294
1295        let trace = evaluate_candidates_with_auth(&input, always_authed);
1296
1297        assert_eq!(trace.harness, "pi");
1298        assert_eq!(trace.candidates_tried, vec!["pi"]);
1299    }
1300
1301    #[test]
1302    fn settings_harness_order_overrides_provider_order() {
1303        let installed = installed(&["codex", "pi"]);
1304        let order = vec!["pi".to_string(), "codex".to_string()];
1305        let input = routing_input(
1306            "gpt-5",
1307            Some("openai"),
1308            Some(&order),
1309            None,
1310            &installed,
1311            None,
1312            (None, None),
1313        );
1314
1315        let trace = evaluate_candidates_with_auth(&input, always_authed);
1316
1317        assert_eq!(trace.source, RouteSource::ConfigOrder);
1318        assert_eq!(trace.harness, "pi");
1319        assert_eq!(trace.harness_order_position, Some(0));
1320    }
1321
1322    #[test]
1323    fn empty_harness_order_falls_through_to_provider() {
1324        let installed = installed(&["codex"]);
1325        let order: Vec<String> = Vec::new();
1326        let input = routing_input(
1327            "gpt-5",
1328            Some("openai"),
1329            Some(&order),
1330            None,
1331            &installed,
1332            None,
1333            (None, None),
1334        );
1335
1336        let trace = evaluate_candidates_with_auth(&input, always_authed);
1337
1338        assert_eq!(trace.source, RouteSource::Provider);
1339        assert_eq!(trace.harness, "codex");
1340        assert!(
1341            trace
1342                .diagnostics
1343                .iter()
1344                .any(|diagnostic| diagnostic.contains("settings.harness_order is empty"))
1345        );
1346    }
1347
1348    #[test]
1349    fn uses_config_default_fallback() {
1350        let installed = installed(&[]);
1351        let input = routing_input(
1352            "gpt-5",
1353            Some("openai"),
1354            None,
1355            Some("Pi"),
1356            &installed,
1357            None,
1358            (None, None),
1359        );
1360
1361        let trace = evaluate_candidates_with_auth(&input, never_authed);
1362
1363        assert_eq!(trace.source, RouteSource::ConfigDefault);
1364        assert_eq!(trace.selection_kind, SelectionKind::ConfigDefault);
1365        assert_eq!(trace.harness, "pi");
1366        assert_eq!(trace.match_evidence, MatchEvidence::Passthrough);
1367    }
1368
1369    #[test]
1370    fn uses_hardcoded_pi_fallback_with_warning() {
1371        let installed = installed(&[]);
1372        let input = routing_input("model", None, None, None, &installed, None, (None, None));
1373
1374        let trace = evaluate_candidates_with_auth(&input, never_authed);
1375
1376        assert_eq!(trace.source, RouteSource::HardcodedDefault);
1377        assert_eq!(trace.selection_kind, SelectionKind::HardcodedDefault);
1378        assert_eq!(trace.harness, "pi");
1379        assert!(
1380            trace
1381                .diagnostics
1382                .iter()
1383                .any(|diagnostic| { diagnostic.contains("defaulting to `pi`") })
1384        );
1385    }
1386
1387    #[test]
1388    fn linked_constraints_apply_to_default_and_hardcoded_fallbacks() {
1389        let installed = installed(&["codex"]);
1390        let linked_harnesses = vec!["claude".to_string()];
1391
1392        let with_config_default = routing_input(
1393            "gpt-5",
1394            Some("openai"),
1395            None,
1396            Some("pi"),
1397            &installed,
1398            Some(&linked_harnesses),
1399            (None, None),
1400        );
1401        let with_default_trace = evaluate_candidates_with_auth(&with_config_default, never_authed);
1402        assert_eq!(with_default_trace.source, RouteSource::Provider);
1403        assert_eq!(
1404            with_default_trace.selection_kind,
1405            SelectionKind::LinkedFallback
1406        );
1407        assert_eq!(with_default_trace.harness, "claude");
1408        assert_eq!(with_default_trace.candidates_tried, vec!["claude"]);
1409        assert!(with_default_trace.diagnostics.iter().any(|diagnostic| {
1410            diagnostic.contains(
1411                "settings.default_harness is excluded by known linked harness constraints",
1412            )
1413        }));
1414
1415        let without_config_default = routing_input(
1416            "gpt-5",
1417            Some("openai"),
1418            None,
1419            None,
1420            &installed,
1421            Some(&linked_harnesses),
1422            (None, None),
1423        );
1424        let hardcoded_trace = evaluate_candidates_with_auth(&without_config_default, never_authed);
1425        assert_eq!(hardcoded_trace.source, RouteSource::Provider);
1426        assert_eq!(
1427            hardcoded_trace.selection_kind,
1428            SelectionKind::LinkedFallback
1429        );
1430        assert_eq!(hardcoded_trace.harness, "claude");
1431        assert!(
1432            hardcoded_trace
1433                .diagnostics
1434                .iter()
1435                .any(|diagnostic| { diagnostic.contains("without unrelated fallback") })
1436        );
1437    }
1438
1439    #[test]
1440    fn linked_default_harness_is_allowed_when_linked() {
1441        let installed = installed(&[]);
1442        let linked_harnesses = vec!["pi".to_string()];
1443        let trace = evaluate_candidates_with_auth(
1444            &routing_input(
1445                "gpt-5",
1446                Some("openai"),
1447                None,
1448                Some("pi"),
1449                &installed,
1450                Some(&linked_harnesses),
1451                (None, None),
1452            ),
1453            never_authed,
1454        );
1455
1456        assert_eq!(trace.source, RouteSource::ConfigDefault);
1457        assert_eq!(trace.harness, "pi");
1458    }
1459
1460    #[test]
1461    fn fixed_harness_evaluation_has_no_fallback() {
1462        let installed = installed(&[]);
1463        let input = routing_input(
1464            "gpt-5",
1465            Some("openai"),
1466            None,
1467            Some("pi"),
1468            &installed,
1469            None,
1470            (None, None),
1471        );
1472        let assessment = evaluate_fixed_harness_with_auth(&input, "codex", never_authed);
1473
1474        assert_eq!(assessment.harness, "codex");
1475        assert!(!assessment.installed);
1476        assert_eq!(assessment.match_evidence, None);
1477        assert_eq!(assessment.skip_reason, Some("not_installed"));
1478    }
1479
1480    #[test]
1481    fn fixed_native_harness_enforces_provider_constraint() {
1482        let installed = installed(&["codex"]);
1483        let input = RoutingInput {
1484            model_id: "gpt-5",
1485            provider_for_order: Some("openai"),
1486            provider_constraint: Some("anthropic"),
1487            settings_provider_order: None,
1488            settings_harness_order: None,
1489            config_default_harness: None,
1490            installed_harnesses: &installed,
1491            linked_harnesses: None,
1492            opencode_probe_result: None,
1493            pi_probe_result: None,
1494        };
1495
1496        let assessment = evaluate_fixed_harness_with_auth(&input, "codex", always_authed);
1497
1498        assert_eq!(assessment.harness, "codex");
1499        assert!(assessment.installed);
1500        assert_eq!(assessment.match_evidence, None);
1501        assert_eq!(
1502            assessment.skip_reason,
1503            Some("provider_constraint_unsatisfied")
1504        );
1505    }
1506
1507    #[test]
1508    fn fixed_native_codex_accepts_openai_codex_provider_variant() {
1509        let installed = installed(&["codex"]);
1510        let input = RoutingInput {
1511            model_id: "gpt-5",
1512            provider_for_order: Some("openai-codex"),
1513            provider_constraint: Some("openai-codex"),
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, "codex", always_authed);
1524
1525        assert_eq!(assessment.harness, "codex");
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 fixed_native_claude_accepts_anthropic_claude_provider_variant() {
1533        let installed = installed(&["claude"]);
1534        let input = RoutingInput {
1535            model_id: "claude-opus-4-7",
1536            provider_for_order: Some("anthropic-claude"),
1537            provider_constraint: Some("anthropic-claude"),
1538            settings_provider_order: None,
1539            settings_harness_order: None,
1540            config_default_harness: None,
1541            installed_harnesses: &installed,
1542            linked_harnesses: None,
1543            opencode_probe_result: None,
1544            pi_probe_result: None,
1545        };
1546
1547        let assessment = evaluate_fixed_harness_with_auth(&input, "claude", always_authed);
1548
1549        assert_eq!(assessment.harness, "claude");
1550        assert!(assessment.installed);
1551        assert_eq!(assessment.match_evidence, Some(MatchEvidence::Constrained));
1552        assert_eq!(assessment.skip_reason, None);
1553    }
1554
1555    #[test]
1556    fn selected_chosen_slug_evidence_prefers_selected_harness_assessment() {
1557        let trace = RoutingTrace {
1558            source: RouteSource::Provider,
1559            selection_kind: SelectionKind::Auto,
1560            match_evidence: MatchEvidence::Confirmed,
1561            harness: "pi".to_string(),
1562            harness_order_position: None,
1563            candidates_tried: vec!["pi".to_string()],
1564            assessments: vec![
1565                CandidateAssessment {
1566                    harness: "opencode".to_string(),
1567                    installed: true,
1568                    candidate_slugs: vec!["openai/gpt-5.4-mini".to_string()],
1569                    filtered_slugs: vec!["openai/gpt-5.4-mini".to_string()],
1570                    chosen_slug: Some("openai/gpt-5.4-mini".to_string()),
1571                    chosen_model: Some("gpt-5.4-mini".to_string()),
1572                    match_evidence: Some(MatchEvidence::Confirmed),
1573                    skip_reason: None,
1574                },
1575                CandidateAssessment {
1576                    harness: "pi".to_string(),
1577                    installed: true,
1578                    candidate_slugs: vec!["openai/gpt-5.4-mini".to_string()],
1579                    filtered_slugs: vec!["openai/gpt-5.4-mini".to_string()],
1580                    chosen_slug: Some("openai/gpt-5.4-mini".to_string()),
1581                    chosen_model: Some("gpt-5.4-mini".to_string()),
1582                    match_evidence: Some(MatchEvidence::Constrained),
1583                    skip_reason: None,
1584                },
1585            ],
1586            diagnostics: vec!["diag".to_string()],
1587        };
1588
1589        let selected = trace
1590            .selected_chosen_slug_evidence()
1591            .expect("selected slug evidence should be present");
1592        assert_eq!(selected.slug, "openai/gpt-5.4-mini");
1593        assert_eq!(selected.match_evidence, Some(MatchEvidence::Constrained));
1594        assert_eq!(trace.selected_harness(), "pi");
1595        assert_eq!(trace.selected_selection_kind(), SelectionKind::Auto);
1596        assert_eq!(trace.selected_match_evidence(), MatchEvidence::Confirmed);
1597        assert_eq!(trace.selected_diagnostics(), vec!["diag".to_string()]);
1598    }
1599
1600    #[test]
1601    fn constrained_slug_selection_prefers_exact_provider_over_variant() {
1602        let installed = installed(&["pi"]);
1603        let pi_probe = PiProbeResult {
1604            compatible: true,
1605            model_slugs: HashSet::from([
1606                "openai-codex/gpt-5.4-mini".to_string(),
1607                "openai/gpt-5.4-mini".to_string(),
1608            ]),
1609            ..PiProbeResult::default()
1610        };
1611        let input = RoutingInput {
1612            model_id: "gpt-5.4-mini",
1613            provider_for_order: Some("openai"),
1614            provider_constraint: Some("openai"),
1615            settings_provider_order: None,
1616            settings_harness_order: None,
1617            config_default_harness: None,
1618            installed_harnesses: &installed,
1619            linked_harnesses: None,
1620            opencode_probe_result: None,
1621            pi_probe_result: Some(&pi_probe),
1622        };
1623
1624        let trace = evaluate_candidates_with_auth(&input, always_authed);
1625        assert_eq!(trace.harness, "pi");
1626        assert_eq!(
1627            trace
1628                .selected_chosen_slug_evidence()
1629                .expect("selected chosen slug evidence")
1630                .slug,
1631            "openai/gpt-5.4-mini"
1632        );
1633    }
1634
1635    #[test]
1636    fn unconstrained_slug_selection_prefers_exact_provider_over_variant_when_known() {
1637        let installed = installed(&["pi"]);
1638        let pi_probe = PiProbeResult {
1639            compatible: true,
1640            model_slugs: HashSet::from([
1641                "openai-codex/gpt-5.4-mini".to_string(),
1642                "openai/gpt-5.4-mini".to_string(),
1643            ]),
1644            ..PiProbeResult::default()
1645        };
1646        let input = RoutingInput {
1647            model_id: "gpt-5.4-mini",
1648            provider_for_order: Some("openai"),
1649            provider_constraint: None,
1650            settings_provider_order: None,
1651            settings_harness_order: None,
1652            config_default_harness: None,
1653            installed_harnesses: &installed,
1654            linked_harnesses: None,
1655            opencode_probe_result: None,
1656            pi_probe_result: Some(&pi_probe),
1657        };
1658
1659        let trace = evaluate_candidates_with_auth(&input, always_authed);
1660        assert_eq!(trace.harness, "pi");
1661        assert_eq!(
1662            trace
1663                .selected_chosen_slug_evidence()
1664                .expect("selected chosen slug evidence")
1665                .slug,
1666            "openai/gpt-5.4-mini"
1667        );
1668    }
1669}