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 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
422pub 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}