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