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#[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#[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#[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#[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#[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
153pub 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
167pub fn evaluate_candidates(input: &RoutingInput<'_>) -> RoutingTrace {
170 evaluate_candidates_with_auth(input, models::harness::native_harness_authenticated)
171}
172
173pub 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
194pub 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
411pub 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}