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