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