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