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