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