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