1use serde::{Deserialize, Serialize};
35use thiserror::Error;
36
37use crate::fleet_exact::{FrozenRoute, ReasoningTier, RequestedReasoning};
38use crate::fleet_preflight::{EndpointIdentity, PreflightedRoute};
39use crate::reasoning_router::{
40 CapturedReasoningRouter, REASONING_ROUTER_SERVICE_KIND, RouterCallReasoning,
41};
42use crate::redaction::redact_for_disclosure;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum ProviderReasoningControl {
57 None,
59 EnabledDisabled,
62 Tiers,
64 NativeAdaptive,
67}
68
69impl ProviderReasoningControl {
70 #[must_use]
71 pub const fn as_str(self) -> &'static str {
72 match self {
73 Self::None => "none",
74 Self::EnabledDisabled => "enabled_disabled",
75 Self::Tiers => "tiers",
76 Self::NativeAdaptive => "native_adaptive",
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
88pub struct ReasoningCapability {
89 pub control: ProviderReasoningControl,
91 pub min_tier: Option<ReasoningTier>,
94 pub max_tier: Option<ReasoningTier>,
96 #[serde(default, skip_serializing_if = "Option::is_none")]
112 pub wire_tiers: Option<[ReasoningTier; 5]>,
113}
114
115const fn tier_index(tier: ReasoningTier) -> usize {
117 match tier {
118 ReasoningTier::Off => 0,
119 ReasoningTier::Low => 1,
120 ReasoningTier::Medium => 2,
121 ReasoningTier::High => 3,
122 ReasoningTier::Max => 4,
123 }
124}
125
126pub const FAITHFUL_WIRE_TIERS: [ReasoningTier; 5] = [
128 ReasoningTier::Off,
129 ReasoningTier::Low,
130 ReasoningTier::Medium,
131 ReasoningTier::High,
132 ReasoningTier::Max,
133];
134
135impl ReasoningCapability {
136 #[must_use]
138 pub const fn none() -> Self {
139 Self {
140 control: ProviderReasoningControl::None,
141 min_tier: None,
142 max_tier: None,
143 wire_tiers: None,
144 }
145 }
146
147 #[must_use]
149 pub const fn tiered() -> Self {
150 Self {
151 control: ProviderReasoningControl::Tiers,
152 min_tier: None,
153 max_tier: None,
154 wire_tiers: None,
155 }
156 }
157
158 #[must_use]
162 pub const fn enabled_disabled() -> Self {
163 Self {
164 control: ProviderReasoningControl::EnabledDisabled,
165 min_tier: None,
166 max_tier: None,
167 wire_tiers: None,
168 }
169 }
170
171 #[must_use]
173 pub const fn native_adaptive() -> Self {
174 Self {
175 control: ProviderReasoningControl::NativeAdaptive,
176 min_tier: None,
177 max_tier: None,
178 wire_tiers: None,
179 }
180 }
181
182 #[must_use]
187 pub fn with_wire_tiers(mut self, wire_tiers: [ReasoningTier; 5]) -> Self {
188 self.wire_tiers = (wire_tiers != FAITHFUL_WIRE_TIERS).then_some(wire_tiers);
189 self
190 }
191
192 #[must_use]
195 pub fn wire_tier(&self, tier: ReasoningTier) -> ReasoningTier {
196 self.wire_tiers.map_or(tier, |wire| wire[tier_index(tier)])
197 }
198
199 #[must_use]
201 pub const fn supports_thinking(&self) -> bool {
202 !matches!(self.control, ProviderReasoningControl::None)
203 }
204
205 #[must_use]
207 pub const fn supports_native_adaptive(&self) -> bool {
208 matches!(self.control, ProviderReasoningControl::NativeAdaptive)
209 }
210
211 #[must_use]
220 pub fn normalize(&self, tier: ReasoningTier) -> (ReasoningTier, bool) {
221 if !self.supports_thinking() {
222 return (ReasoningTier::Off, tier != ReasoningTier::Off);
223 }
224 let mut effective = self.wire_tier(tier);
225 if let Some(min) = self.min_tier
226 && effective < min
227 {
228 effective = min;
229 }
230 if let Some(max) = self.max_tier
231 && effective > max
232 {
233 effective = max;
234 }
235 (effective, effective != tier)
236 }
237
238 #[must_use]
240 pub const fn provider_effective(&self, tier: ReasoningTier) -> ProviderEffectiveReasoning {
241 match self.control {
242 ProviderReasoningControl::None => ProviderEffectiveReasoning::Disabled,
243 ProviderReasoningControl::EnabledDisabled => match tier {
244 ReasoningTier::Off => ProviderEffectiveReasoning::Disabled,
245 _ => ProviderEffectiveReasoning::Enabled,
246 },
247 ProviderReasoningControl::Tiers => ProviderEffectiveReasoning::Tier(tier),
248 ProviderReasoningControl::NativeAdaptive => ProviderEffectiveReasoning::NativeAdaptive,
249 }
250 }
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
256#[serde(rename_all = "snake_case", tag = "kind", content = "tier")]
257pub enum ProviderEffectiveReasoning {
258 Disabled,
260 Enabled,
263 Tier(ReasoningTier),
265 NativeAdaptive,
267}
268
269impl ProviderEffectiveReasoning {
270 #[must_use]
271 pub const fn label(self) -> &'static str {
272 match self {
273 Self::Disabled => "disabled",
274 Self::Enabled => "enabled",
275 Self::Tier(tier) => tier.as_str(),
276 Self::NativeAdaptive => "native_adaptive",
277 }
278 }
279}
280
281pub const ROUTER_CALL_REASONING: RouterCallReasoning = RouterCallReasoning::Off;
286
287#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
296pub struct RouterCallDisclosure {
297 pub requested: String,
299 pub effective: String,
301 pub provider_control: String,
303 pub provider_effective: String,
305 #[serde(default)]
307 pub capability_normalized: bool,
308}
309
310impl RouterCallDisclosure {
311 #[must_use]
313 pub fn receipt(&self) -> String {
314 format!(
315 "router_call_requested={} router_call_effective={} router_call_provider_control={} \
316 router_call_provider_effective={}",
317 self.requested, self.effective, self.provider_control, self.provider_effective,
318 )
319 }
320}
321
322#[derive(Debug, Clone, PartialEq, Eq)]
324pub struct RouterCallPlan {
325 pub tier: ReasoningTier,
327 pub disclosure: RouterCallDisclosure,
329}
330
331#[must_use]
338pub fn router_call_plan(
339 requested: RouterCallReasoning,
340 capability: &ReasoningCapability,
341) -> RouterCallPlan {
342 let (tier, capability_normalized) = capability.normalize(requested.tier());
343 RouterCallPlan {
344 tier,
345 disclosure: RouterCallDisclosure {
346 requested: requested.as_str().to_string(),
347 effective: tier.as_str().to_string(),
348 provider_control: capability.control.as_str().to_string(),
349 provider_effective: capability.provider_effective(tier).label().to_string(),
350 capability_normalized,
351 },
352 }
353}
354
355#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
362pub struct RouterIdentity {
363 pub id: String,
365 #[serde(default = "legacy_origin")]
367 pub origin: String,
368 #[serde(default = "service_kind", alias = "role")]
371 pub service_kind: String,
372 #[serde(default)]
374 pub legacy_inline: bool,
375 pub provider: String,
377 pub model: String,
379 #[serde(default, skip_serializing_if = "Option::is_none")]
381 pub endpoint: Option<EndpointIdentity>,
382 #[serde(default, skip_serializing_if = "Option::is_none")]
384 pub call: Option<RouterCallDisclosure>,
385}
386
387fn service_kind() -> String {
388 REASONING_ROUTER_SERVICE_KIND.to_string()
389}
390
391fn legacy_origin() -> String {
392 crate::reasoning_router::LEGACY_INLINE_ROUTER_ORIGIN.to_string()
393}
394
395impl RouterIdentity {
396 #[must_use]
398 pub fn from_captured(
399 captured: &CapturedReasoningRouter,
400 route: Option<&PreflightedRoute>,
401 call: Option<RouterCallDisclosure>,
402 ) -> Self {
403 Self {
404 id: captured.id.clone(),
405 origin: captured.origin.clone(),
406 service_kind: captured.service_kind.clone(),
407 legacy_inline: captured.legacy_inline,
408 provider: route.map_or_else(
409 || captured.route.provider.clone(),
410 |route| route.provider_id.clone(),
411 ),
412 model: route.map_or_else(
413 || captured.route.model.clone(),
414 |route| route.wire_model.clone(),
415 ),
416 endpoint: route.map(|route| route.endpoint.clone()),
417 call,
418 }
419 }
420
421 #[must_use]
423 pub fn new(provider: impl Into<String>, model: impl Into<String>) -> Self {
424 Self {
425 id: "router".to_string(),
426 origin: legacy_origin(),
427 service_kind: service_kind(),
428 legacy_inline: true,
429 provider: provider.into(),
430 model: model.into(),
431 endpoint: None,
432 call: None,
433 }
434 }
435
436 #[must_use]
438 pub fn qualified(&self) -> String {
439 format!("{}/{}", self.origin, self.id)
440 }
441
442 #[must_use]
445 pub fn label(&self) -> String {
446 format!(
447 "{}:{} {}/{}",
448 self.service_kind,
449 self.qualified(),
450 self.provider,
451 self.model
452 )
453 }
454}
455
456#[derive(Debug, Clone, PartialEq, Eq)]
458pub enum RouterAvailability {
459 Absent,
461 Unavailable { reason: String },
464 Ready,
466}
467
468#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
470#[serde(rename_all = "snake_case", tag = "kind", content = "tier")]
471pub enum EffectiveReasoning {
472 Tier(ReasoningTier),
474 NativeAdaptive,
476}
477
478impl EffectiveReasoning {
479 #[must_use]
480 pub fn label(self) -> &'static str {
481 match self {
482 Self::Tier(tier) => tier.as_str(),
483 Self::NativeAdaptive => "native_adaptive",
484 }
485 }
486
487 #[must_use]
489 pub const fn tier(self) -> Option<ReasoningTier> {
490 match self {
491 Self::Tier(tier) => Some(tier),
492 Self::NativeAdaptive => None,
493 }
494 }
495}
496
497#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
500#[serde(rename_all = "snake_case")]
501pub enum EffectiveReasoningSource {
502 MemberExplicit,
504 ProviderNativeAdaptive,
510 FleetRouter,
512 LegacyHeuristic,
514 SessionInherited,
516}
517
518impl EffectiveReasoningSource {
519 #[must_use]
520 pub const fn as_str(self) -> &'static str {
521 match self {
522 Self::MemberExplicit => "member_explicit",
523 Self::ProviderNativeAdaptive => "provider_native_adaptive",
524 Self::FleetRouter => "fleet_router",
525 Self::LegacyHeuristic => "legacy_heuristic",
526 Self::SessionInherited => "session_inherited",
527 }
528 }
529}
530
531#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
535pub struct ResolvedReasoning {
536 requested: RequestedReasoning,
537 effective: EffectiveReasoning,
538 provider_control: ProviderReasoningControl,
539 provider_effective: ProviderEffectiveReasoning,
540 source: EffectiveReasoningSource,
541 capability_normalized: bool,
542 #[serde(default, skip_serializing_if = "Option::is_none")]
545 router: Option<RouterIdentity>,
546}
547
548impl ResolvedReasoning {
549 fn new(
550 requested: RequestedReasoning,
551 effective: EffectiveReasoning,
552 capability: &ReasoningCapability,
553 source: EffectiveReasoningSource,
554 capability_normalized: bool,
555 ) -> Self {
556 let provider_effective = match effective {
557 EffectiveReasoning::Tier(tier) => capability.provider_effective(tier),
558 EffectiveReasoning::NativeAdaptive => ProviderEffectiveReasoning::NativeAdaptive,
559 };
560 Self {
561 requested,
562 effective,
563 provider_control: capability.control,
564 provider_effective,
565 source,
566 capability_normalized,
567 router: None,
568 }
569 }
570
571 fn with_router(mut self, router: RouterIdentity) -> Self {
572 self.router = Some(router);
573 self
574 }
575
576 #[must_use]
578 pub fn router(&self) -> Option<&RouterIdentity> {
579 self.router.as_ref()
580 }
581
582 #[must_use]
583 pub const fn requested(&self) -> RequestedReasoning {
584 self.requested
585 }
586
587 #[must_use]
590 pub const fn effective(&self) -> EffectiveReasoning {
591 self.effective
592 }
593
594 #[must_use]
596 pub const fn provider_control(&self) -> ProviderReasoningControl {
597 self.provider_control
598 }
599
600 #[must_use]
604 pub const fn provider_effective(&self) -> ProviderEffectiveReasoning {
605 self.provider_effective
606 }
607
608 #[must_use]
609 pub const fn source(&self) -> EffectiveReasoningSource {
610 self.source
611 }
612
613 #[must_use]
615 pub const fn capability_normalized(&self) -> bool {
616 self.capability_normalized
617 }
618
619 #[must_use]
622 pub fn receipt(&self) -> String {
623 let mut line = format!(
624 "requested={} selected={} provider_control={} provider_effective={} source={}",
625 self.requested.as_str(),
626 self.effective.label(),
627 self.provider_control.as_str(),
628 self.provider_effective.label(),
629 self.source.as_str(),
630 );
631 if let Some(router) = &self.router {
632 line.push_str(&format!(" router={}", router.label()));
633 if let Some(call) = &router.call {
634 line.push(' ');
635 line.push_str(&call.receipt());
636 }
637 }
638 line
639 }
640}
641
642pub const ROUTER_SUMMARY_MAX_CHARS: usize = 600;
651
652pub const ROUTING_SCOPE: &str = "bounded_redacted_task_shape";
654
655#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
662#[serde(rename_all = "snake_case")]
663pub enum TaskShape {
664 Read,
666 Edit,
668 Diagnose,
670 Unclassified,
672}
673
674impl TaskShape {
675 #[must_use]
676 pub const fn as_str(self) -> &'static str {
677 match self {
678 Self::Read => "read",
679 Self::Edit => "edit",
680 Self::Diagnose => "diagnose",
681 Self::Unclassified => "unclassified",
682 }
683 }
684
685 #[must_use]
687 pub fn classify(text: &str) -> Self {
688 let lowered = text.to_ascii_lowercase();
689 let has = |needles: &[&str]| needles.iter().any(|needle| lowered.contains(needle));
690 if has(&[
691 "debug",
692 "why does",
693 "root cause",
694 "failing",
695 "flake",
696 "crash",
697 ]) {
698 Self::Diagnose
699 } else if has(&[
700 "edit",
701 "implement",
702 "refactor",
703 "fix",
704 "add ",
705 "rewrite",
706 "migrate",
707 ]) {
708 Self::Edit
709 } else if has(&["read", "review", "summarize", "audit", "inspect", "explain"]) {
710 Self::Read
711 } else {
712 Self::Unclassified
713 }
714 }
715}
716
717#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
723pub struct RoutingDisclosure {
724 #[serde(default)]
726 pub transmitted_bytes: usize,
727 #[serde(default)]
729 pub transmitted_chars: usize,
730 #[serde(default)]
732 pub original_chars: usize,
733 #[serde(default)]
735 pub truncated: bool,
736 #[serde(default)]
739 pub content_hash: String,
740 #[serde(default)]
742 pub redacted: bool,
743 #[serde(default)]
745 pub redactions: Vec<String>,
746 #[serde(default)]
748 pub scope: String,
749 #[serde(default)]
751 pub task_shape: String,
752 #[serde(default)]
754 pub cross_provider_inference: bool,
755}
756
757impl RoutingDisclosure {
758 #[must_use]
760 pub fn receipt(&self) -> String {
761 format!(
762 "routing_summary_bytes={} chars={} truncated={} hash={} redacted={} \
763 cross_provider={}",
764 self.transmitted_bytes,
765 self.transmitted_chars,
766 self.truncated,
767 self.content_hash,
768 self.redacted,
769 self.cross_provider_inference,
770 )
771 }
772}
773
774#[derive(Debug, Clone, PartialEq, Eq)]
780pub struct RoutingPayload {
781 text: String,
782 disclosure: RoutingDisclosure,
783}
784
785impl RoutingPayload {
786 #[must_use]
789 pub fn text(&self) -> &str {
790 &self.text
791 }
792
793 #[must_use]
795 pub fn disclosure(&self) -> &RoutingDisclosure {
796 &self.disclosure
797 }
798
799 #[must_use]
801 pub fn into_disclosure(self) -> RoutingDisclosure {
802 self.disclosure
803 }
804
805 #[must_use]
808 pub fn with_cross_provider(mut self, cross_provider: bool) -> Self {
809 self.disclosure.cross_provider_inference = cross_provider;
810 self
811 }
812}
813
814#[must_use]
830pub fn bounded_routing_payload(task: &str) -> RoutingPayload {
831 let mut sanitized = String::with_capacity(task.len().min(ROUTER_SUMMARY_MAX_CHARS * 2));
832 let mut pending_space = false;
833 for ch in task.chars() {
834 let mapped = match ch {
835 ch if ch.is_control() || ch.is_whitespace() => {
836 pending_space = !sanitized.is_empty();
837 continue;
838 }
839 '`' => '\'',
843 '{' => '(',
844 '}' => ')',
845 other => other,
846 };
847 if pending_space {
848 sanitized.push(' ');
849 pending_space = false;
850 }
851 sanitized.push(mapped);
852 }
853
854 let redaction = redact_for_disclosure(&sanitized);
855 let redacted = redaction.redacted();
856 let redactions = redaction.kinds();
857 let cleaned = redaction.into_text();
858
859 let original_chars = cleaned.chars().count();
860 let truncated = original_chars > ROUTER_SUMMARY_MAX_CHARS;
861 let text = if truncated {
862 cleaned
863 .chars()
864 .take(ROUTER_SUMMARY_MAX_CHARS)
865 .collect::<String>()
866 .trim_end()
867 .to_string()
868 } else {
869 cleaned
870 };
871
872 let task_shape = TaskShape::classify(&text);
873
874 RoutingPayload {
875 disclosure: RoutingDisclosure {
876 transmitted_bytes: text.len(),
877 transmitted_chars: text.chars().count(),
878 original_chars,
879 truncated,
880 content_hash: crate::named_fleet::sha256_label(text.as_bytes()),
881 redacted,
882 redactions,
883 scope: ROUTING_SCOPE.to_string(),
884 task_shape: task_shape.as_str().to_string(),
885 cross_provider_inference: false,
886 },
887 text,
888 }
889}
890
891#[derive(Debug, Clone, PartialEq, Eq)]
897pub struct RouterCallInput {
898 pub fleet: String,
899 pub member_id: String,
900 pub frozen: FrozenRoute,
901 pub payload: RoutingPayload,
904}
905
906pub const ROUTER_MAX_OUTPUT_TOKENS: u32 = 32;
910
911#[must_use]
918pub fn router_system_prompt(input: &RouterCallInput) -> String {
919 format!(
920 "You are the reasoning router for the `{fleet}` fleet. You are a reasoning-only service, \
921not a fleet member: the worker's provider and model are already frozen and you cannot change \
922them, choose a different member, or alter tools or permissions.\n\
923Worker member: {member}\n\
924Frozen provider: {provider}\n\
925Frozen model: {model}\n\
926The next message is a bounded, redacted description of the task's shape. Judge only how hard the \
927already-chosen model should think about it.\n\n\
928Reply with exactly this JSON object and nothing else: \
929{{\"reasoning\":\"off|low|medium|high|max\"}}. \
930Emit one object only — no second object, no repeated key, no text before or after it. \
931No other key is permitted — not a rationale, not an explanation, and above all not a \
932provider, model, route, member, or fleet field. Any extra key rejects your answer and \
933fails the run. Do not answer \"auto\".",
934 fleet = input.fleet,
935 member = input.member_id,
936 provider = input.frozen.provider,
937 model = input.frozen.model,
938 )
939}
940
941#[must_use]
946pub fn router_user_message(input: &RouterCallInput) -> String {
947 input.payload.text().to_string()
948}
949
950#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
952pub struct RouterDecision {
953 pub reasoning: ReasoningTier,
954}
955
956pub const ROUTER_REASONING_FIELD: &str = "reasoning";
958
959const ROUTER_FORBIDDEN_FIELDS: &[&str] = &[
962 "provider",
963 "provider_id",
964 "provider_kind",
965 "model",
966 "model_id",
967 "wire_model",
968 "wire_model_id",
969 "route",
970 "model_route",
971 "endpoint",
972 "fleet",
973 "member",
974 "member_id",
975 "role",
976 "tools",
977 "allowed_tools",
978 "permissions",
979];
980
981pub fn parse_router_decision(raw: &str) -> Result<RouterDecision, RouterDecisionError> {
984 let repaired = strip_router_code_fence(raw);
992
993 let mut stream = serde_json::Deserializer::from_str(repaired).into_iter::<RouterObject>();
1005 let object = match stream.next() {
1006 Some(Ok(object)) => object,
1007 Some(Err(error)) => return Err(RouterDecisionError::Parse(error.to_string())),
1008 None => return Err(RouterDecisionError::Parse("router output was empty".into())),
1009 };
1010 let consumed = stream.byte_offset();
1011 if !repaired[consumed..].trim().is_empty() {
1012 return Err(RouterDecisionError::TrailingContent {
1013 trailing: trailing_excerpt(&repaired[consumed..]),
1014 });
1015 }
1016
1017 let entries = &object.0;
1018
1019 for (index, (field, _)) in entries.iter().enumerate() {
1022 if entries[..index]
1023 .iter()
1024 .any(|(earlier, _)| earlier.eq_ignore_ascii_case(field))
1025 {
1026 return Err(RouterDecisionError::DuplicateField {
1027 field: field.clone(),
1028 });
1029 }
1030 }
1031
1032 if let Some((field, _)) = entries.iter().find(|(field, _)| {
1038 ROUTER_FORBIDDEN_FIELDS
1039 .iter()
1040 .any(|forbidden| field.as_str().eq_ignore_ascii_case(forbidden))
1041 }) {
1042 return Err(RouterDecisionError::RouteMutationAttempt {
1043 field: field.clone(),
1044 });
1045 }
1046 if let Some((field, _)) = entries
1047 .iter()
1048 .find(|(field, _)| field.as_str() != ROUTER_REASONING_FIELD)
1049 {
1050 return Err(RouterDecisionError::UnknownField {
1051 field: field.clone(),
1052 });
1053 }
1054
1055 let reasoning = entries
1056 .iter()
1057 .find(|(field, _)| field == ROUTER_REASONING_FIELD)
1058 .and_then(|(_, value)| value.as_str())
1059 .ok_or(RouterDecisionError::MissingReasoning)?;
1060
1061 if reasoning.trim().eq_ignore_ascii_case("auto") {
1062 return Err(RouterDecisionError::AutoReasoning);
1063 }
1064
1065 let reasoning =
1066 ReasoningTier::parse(reasoning).ok_or_else(|| RouterDecisionError::InvalidReasoning {
1067 value: reasoning.trim().to_string(),
1068 })?;
1069
1070 Ok(RouterDecision { reasoning })
1071}
1072
1073struct RouterObject(Vec<(String, serde_json::Value)>);
1078
1079impl<'de> Deserialize<'de> for RouterObject {
1080 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1081 where
1082 D: serde::Deserializer<'de>,
1083 {
1084 struct ObjectVisitor;
1085
1086 impl<'de> serde::de::Visitor<'de> for ObjectVisitor {
1087 type Value = RouterObject;
1088
1089 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1090 formatter.write_str("a JSON object")
1091 }
1092
1093 fn visit_map<A>(self, mut map: A) -> Result<RouterObject, A::Error>
1094 where
1095 A: serde::de::MapAccess<'de>,
1096 {
1097 let mut entries = Vec::new();
1098 while let Some((key, value)) = map.next_entry::<String, serde_json::Value>()? {
1099 entries.push((key, value));
1100 }
1101 Ok(RouterObject(entries))
1102 }
1103 }
1104
1105 deserializer.deserialize_map(ObjectVisitor)
1106 }
1107}
1108
1109pub fn resolve_exact_member_reasoning(
1116 member_id: &str,
1117 frozen: &FrozenRoute,
1118 requested: RequestedReasoning,
1119 capability: &ReasoningCapability,
1120 router: &RouterAvailability,
1121 decision: Option<&RouterDecision>,
1122 router_identity: Option<&RouterIdentity>,
1123) -> Result<ResolvedReasoning, ReasoningResolveError> {
1124 let _ = frozen;
1125
1126 if let Some(tier) = requested.tier() {
1127 let (effective, capability_normalized) = capability.normalize(tier);
1130 return Ok(ResolvedReasoning::new(
1131 requested,
1132 EffectiveReasoning::Tier(effective),
1133 capability,
1134 EffectiveReasoningSource::MemberExplicit,
1135 capability_normalized,
1136 ));
1137 }
1138
1139 match router {
1145 RouterAvailability::Absent => Err(ReasoningResolveError::RouterRequired {
1146 member: member_id.to_string(),
1147 reason: "this fleet references no reasoning router".to_string(),
1148 }),
1149 RouterAvailability::Unavailable { reason } => {
1150 Err(ReasoningResolveError::RouterUnavailable {
1151 member: member_id.to_string(),
1152 reason: reason.clone(),
1153 })
1154 }
1155 RouterAvailability::Ready => {
1156 let decision =
1157 decision.ok_or_else(|| ReasoningResolveError::RouterDecisionMissing {
1158 member: member_id.to_string(),
1159 })?;
1160 let identity =
1161 router_identity.ok_or_else(|| ReasoningResolveError::RouterIdentityMissing {
1162 member: member_id.to_string(),
1163 })?;
1164 let (effective, capability_normalized) = capability.normalize(decision.reasoning);
1165 Ok(ResolvedReasoning::new(
1166 requested,
1167 EffectiveReasoning::Tier(effective),
1168 capability,
1169 EffectiveReasoningSource::FleetRouter,
1170 capability_normalized,
1171 )
1172 .with_router(identity.clone()))
1173 }
1174 }
1175}
1176
1177#[must_use]
1183pub fn resolve_legacy_reasoning(
1184 requested: RequestedReasoning,
1185 capability: &ReasoningCapability,
1186 heuristic_tier: ReasoningTier,
1187) -> ResolvedReasoning {
1188 let (tier, source) = match requested.tier() {
1189 Some(tier) => (tier, EffectiveReasoningSource::MemberExplicit),
1190 None => (heuristic_tier, EffectiveReasoningSource::LegacyHeuristic),
1191 };
1192 let (effective, capability_normalized) = capability.normalize(tier);
1193 ResolvedReasoning::new(
1194 requested,
1195 EffectiveReasoning::Tier(effective),
1196 capability,
1197 source,
1198 capability_normalized,
1199 )
1200}
1201
1202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1220pub struct FleetTaskReceipt {
1221 pub fleet: String,
1223 #[serde(default)]
1225 pub schema_kind: String,
1226 #[serde(default)]
1227 pub schema_revision: u32,
1228 #[serde(default)]
1230 pub content_hash: String,
1231 pub member_id: String,
1233 pub member_role: String,
1235 #[serde(default, skip_serializing_if = "Option::is_none")]
1248 pub posture_role: Option<String>,
1249 #[serde(default, skip_serializing_if = "Option::is_none")]
1265 pub authority_fingerprint: Option<String>,
1266 pub provider: String,
1268 pub model: String,
1270 #[serde(default, skip_serializing_if = "Option::is_none")]
1273 pub declared_model: Option<String>,
1274 #[serde(default, skip_serializing_if = "Option::is_none")]
1276 pub endpoint: Option<EndpointIdentity>,
1277 pub requested_reasoning: String,
1279 pub effective_reasoning: String,
1281 #[serde(default)]
1283 pub provider_control: String,
1284 pub provider_effective_reasoning: String,
1286 pub selection_source: String,
1288 #[serde(default)]
1290 pub capability_normalized: bool,
1291 #[serde(default, skip_serializing_if = "Option::is_none")]
1293 pub router: Option<RouterIdentity>,
1294 #[serde(default, skip_serializing_if = "Option::is_none")]
1297 pub routing_summary: Option<RoutingDisclosure>,
1298 #[serde(default)]
1301 pub member_network_tool: bool,
1302 #[serde(default)]
1305 pub cross_provider_inference: bool,
1306 #[serde(default)]
1308 pub transport: String,
1309}
1310
1311#[must_use]
1323pub fn transport_disclosure(
1324 router_called: bool,
1325 member_network_tool: bool,
1326 cross_provider: bool,
1327) -> String {
1328 let tool_clause = if member_network_tool {
1329 "the member also holds a model-visible network tool"
1330 } else {
1331 "the member holds no model-visible network tool"
1332 };
1333 let mut line = format!("Host-owned provider inference over the network; {tool_clause}.");
1334 if router_called {
1335 line.push_str(" A bounded, redacted routing summary was also sent to the fleet's ");
1336 if cross_provider {
1337 line.push_str("reasoning router, which runs on a different provider than this member.");
1338 } else {
1339 line.push_str("reasoning router, which runs on the same provider as this member.");
1340 }
1341 }
1342 line
1343}
1344
1345impl FleetTaskReceipt {
1346 #[must_use]
1349 #[allow(clippy::too_many_arguments)]
1350 pub fn new(
1351 fleet: impl Into<String>,
1352 schema_kind: impl Into<String>,
1353 schema_revision: u32,
1354 content_hash: impl Into<String>,
1355 member_id: impl Into<String>,
1356 member_role: impl Into<String>,
1357 route: &PreflightedRoute,
1358 resolved: &ResolvedReasoning,
1359 routing_summary: Option<RoutingDisclosure>,
1360 member_network_tool: bool,
1361 ) -> Self {
1362 let router = resolved.router().cloned();
1363 let router_called = router.is_some();
1364 let cross_provider = routing_summary
1365 .as_ref()
1366 .is_some_and(|summary| summary.cross_provider_inference);
1367 Self {
1368 fleet: fleet.into(),
1369 schema_kind: schema_kind.into(),
1370 schema_revision,
1371 content_hash: content_hash.into(),
1372 member_id: member_id.into(),
1373 member_role: member_role.into(),
1374 posture_role: None,
1375 authority_fingerprint: None,
1376 provider: route.provider_id.clone(),
1377 model: route.wire_model.clone(),
1378 declared_model: route
1379 .model_canonicalized()
1380 .then(|| route.declared_model.clone()),
1381 endpoint: Some(route.endpoint.clone()),
1382 requested_reasoning: resolved.requested().as_str().to_string(),
1383 effective_reasoning: resolved.effective().label().to_string(),
1384 provider_control: resolved.provider_control().as_str().to_string(),
1385 provider_effective_reasoning: resolved.provider_effective().label().to_string(),
1386 selection_source: resolved.source().as_str().to_string(),
1387 capability_normalized: resolved.capability_normalized(),
1388 router,
1389 routing_summary,
1390 member_network_tool,
1391 cross_provider_inference: cross_provider,
1392 transport: transport_disclosure(router_called, member_network_tool, cross_provider),
1393 }
1394 }
1395
1396 #[must_use]
1402 pub fn with_posture_role(mut self, posture_role: impl Into<String>) -> Self {
1403 let posture_role = posture_role.into();
1404 self.posture_role = (posture_role != self.member_role).then_some(posture_role);
1405 self
1406 }
1407
1408 #[must_use]
1415 pub fn with_authority_fingerprint(mut self, fingerprint: impl Into<String>) -> Self {
1416 self.authority_fingerprint = Some(fingerprint.into());
1417 self
1418 }
1419
1420 #[must_use]
1422 pub fn line(&self) -> String {
1423 let mut line = format!(
1424 "fleet={} member={} (role {}) route={}/{} requested={} effective={} \
1425 provider_control={} provider_effective={} source={}",
1426 self.fleet,
1427 self.member_id,
1428 self.member_role,
1429 self.provider,
1430 self.model,
1431 self.requested_reasoning,
1432 self.effective_reasoning,
1433 self.provider_control,
1434 self.provider_effective_reasoning,
1435 self.selection_source,
1436 );
1437 if let Some(posture) = &self.posture_role {
1438 line.push_str(&format!(" posture={posture}"));
1439 }
1440 if let Some(router) = &self.router {
1441 line.push_str(&format!(" router={}", router.label()));
1442 if let Some(call) = &router.call {
1443 line.push(' ');
1444 line.push_str(&call.receipt());
1445 }
1446 }
1447 if let Some(summary) = &self.routing_summary {
1448 line.push_str(&format!(" {}", summary.receipt()));
1449 }
1450 line
1451 }
1452}
1453
1454#[derive(Debug, Clone, PartialEq, Eq, Error)]
1455pub enum ReasoningResolveError {
1456 #[error(
1457 "fleet member `{member}` requests reasoning `auto`, and {reason}. Attach a reasoning \
1458 router to this fleet (`reasoning_router = \"<name>\"`) or pin an explicit reasoning tier."
1459 )]
1460 RouterRequired { member: String, reason: String },
1461 #[error(
1462 "fleet member `{member}` requests reasoning `auto` but the fleet's reasoning router is \
1463 unavailable: {reason}. Fix the router profile or pin an explicit reasoning tier."
1464 )]
1465 RouterUnavailable { member: String, reason: String },
1466 #[error("fleet member `{member}` requires a router decision that was not supplied")]
1467 RouterDecisionMissing { member: String },
1468 #[error(
1469 "fleet member `{member}` took a router decision with no router identity; a receipt must \
1470 be able to name which reasoning router chose the tier"
1471 )]
1472 RouterIdentityMissing { member: String },
1473}
1474
1475#[derive(Debug, Clone, PartialEq, Eq, Error)]
1476pub enum RouterDecisionError {
1477 #[error("router output was not parseable JSON: {0}")]
1478 Parse(String),
1479 #[error(
1480 "router output contains `{field}`; a reasoning router may only choose a reasoning tier \
1481 and can never move an already frozen provider/model route, member, role, or permission"
1482 )]
1483 RouteMutationAttempt { field: String },
1484 #[error(
1485 "router output contains `{field}`; a reasoning router has exactly one job and may emit \
1486 only `reasoning`"
1487 )]
1488 UnknownField { field: String },
1489 #[error(
1490 "router output names `{field}` more than once; a reasoning router must make exactly one \
1491 concrete choice, and a repeated key is two answers wearing one name"
1492 )]
1493 DuplicateField { field: String },
1494 #[error("router output has no `reasoning` field")]
1495 MissingReasoning,
1496 #[error("router chose `auto`, which is not a concrete reasoning tier")]
1497 AutoReasoning,
1498 #[error("router chose invalid reasoning `{value}`")]
1499 InvalidReasoning { value: String },
1500 #[error(
1501 "router output has content after its JSON object (`{trailing}`); a router must emit \
1502 exactly one object and nothing else"
1503 )]
1504 TrailingContent { trailing: String },
1505}
1506
1507fn strip_router_code_fence(raw: &str) -> &str {
1513 let trimmed = raw.trim();
1514 trimmed
1515 .strip_prefix("```json")
1516 .or_else(|| trimmed.strip_prefix("```"))
1517 .and_then(|value| value.strip_suffix("```"))
1518 .map_or(trimmed, str::trim)
1519}
1520
1521fn trailing_excerpt(rest: &str) -> String {
1524 let cleaned: String = rest
1525 .trim()
1526 .chars()
1527 .map(|ch| if ch.is_control() { ' ' } else { ch })
1528 .take(60)
1529 .collect();
1530 cleaned.trim().to_string()
1531}
1532
1533#[cfg(test)]
1534mod tests {
1535 use super::*;
1536 use crate::fleet_preflight::CredentialReadiness;
1537
1538 fn frozen() -> FrozenRoute {
1539 FrozenRoute {
1540 provider: "zai".to_string(),
1541 model: "glm-5".to_string(),
1542 }
1543 }
1544
1545 fn preflighted() -> PreflightedRoute {
1546 PreflightedRoute {
1547 member_id: "implementer".to_string(),
1548 provider_id: "zai".to_string(),
1549 provider_config_id: None,
1550 provider_kind: "zai".to_string(),
1551 declared_model: "glm-5".to_string(),
1552 wire_model: "glm-5".to_string(),
1553 endpoint: EndpointIdentity::from_base_url("https://api.z.ai/api/paas/v4"),
1554 credential: CredentialReadiness::Configured,
1555 capability: ReasoningCapability::tiered(),
1556 }
1557 }
1558
1559 fn router_identity() -> RouterIdentity {
1560 RouterIdentity {
1561 id: "luna-low".to_string(),
1562 origin: "workspace".to_string(),
1563 service_kind: REASONING_ROUTER_SERVICE_KIND.to_string(),
1564 legacy_inline: false,
1565 provider: "openai".to_string(),
1566 model: "gpt-5.6-luna".to_string(),
1567 endpoint: Some(EndpointIdentity::from_base_url("https://api.openai.com/v1")),
1568 call: Some(
1569 router_call_plan(RouterCallReasoning::Low, &ReasoningCapability::tiered())
1570 .disclosure,
1571 ),
1572 }
1573 }
1574
1575 #[test]
1576 fn explicit_tier_resolves_without_a_router() {
1577 let resolved = resolve_exact_member_reasoning(
1578 "implementer",
1579 &frozen(),
1580 RequestedReasoning::High,
1581 &ReasoningCapability::tiered(),
1582 &RouterAvailability::Absent,
1583 None,
1584 None,
1585 )
1586 .expect("explicit tiers never need a router");
1587
1588 assert_eq!(resolved.requested(), RequestedReasoning::High);
1589 assert_eq!(
1590 resolved.effective(),
1591 EffectiveReasoning::Tier(ReasoningTier::High)
1592 );
1593 assert_eq!(resolved.source(), EffectiveReasoningSource::MemberExplicit);
1594 assert!(
1595 resolved.router().is_none(),
1596 "manual reasoning uses no router"
1597 );
1598 assert!(!resolved.capability_normalized());
1599 }
1600
1601 #[test]
1602 fn auto_without_a_router_fails_before_work_starts() {
1603 let err = resolve_exact_member_reasoning(
1604 "implementer",
1605 &frozen(),
1606 RequestedReasoning::Auto,
1607 &ReasoningCapability::tiered(),
1608 &RouterAvailability::Absent,
1609 None,
1610 None,
1611 )
1612 .expect_err("auto must fail closed without a router");
1613
1614 assert!(matches!(err, ReasoningResolveError::RouterRequired { .. }));
1615 let message = err.to_string();
1616 assert!(message.contains("implementer"), "{message}");
1617 assert!(message.contains("reasoning_router"), "{message}");
1618 }
1619
1620 #[test]
1621 fn auto_with_an_unavailable_router_fails_closed_too() {
1622 let err = resolve_exact_member_reasoning(
1623 "implementer",
1624 &frozen(),
1625 RequestedReasoning::Auto,
1626 &ReasoningCapability::tiered(),
1627 &RouterAvailability::Unavailable {
1628 reason: "no credentials for provider `openai`".to_string(),
1629 },
1630 None,
1631 None,
1632 )
1633 .expect_err("unavailable router must fail closed");
1634
1635 assert!(matches!(
1636 err,
1637 ReasoningResolveError::RouterUnavailable { .. }
1638 ));
1639 }
1640
1641 #[test]
1642 fn a_ready_router_decides_only_reasoning_on_a_frozen_route() {
1643 let decision =
1644 parse_router_decision(r#"{"reasoning":"max"}"#).expect("valid router decision");
1645
1646 let worker = frozen();
1647 let resolved = resolve_exact_member_reasoning(
1648 "implementer",
1649 &worker,
1650 RequestedReasoning::Auto,
1651 &ReasoningCapability::tiered(),
1652 &RouterAvailability::Ready,
1653 Some(&decision),
1654 Some(&router_identity()),
1655 )
1656 .expect("ready router resolves auto");
1657
1658 assert_eq!(resolved.requested(), RequestedReasoning::Auto);
1659 assert_eq!(
1660 resolved.effective(),
1661 EffectiveReasoning::Tier(ReasoningTier::Max)
1662 );
1663 assert_eq!(resolved.source(), EffectiveReasoningSource::FleetRouter);
1664 assert_eq!(worker.provider, "zai");
1666 assert_eq!(worker.model, "glm-5");
1667 }
1668
1669 #[test]
1670 fn router_output_that_names_a_route_member_or_permission_is_rejected() {
1671 for raw in [
1672 r#"{"reasoning":"high","provider":"deepseek"}"#,
1673 r#"{"reasoning":"high","model":"glm-5-turbo"}"#,
1674 r#"{"reasoning":"high","model_route":"faster"}"#,
1675 r#"{"reasoning":"high","member_id":"someone-else"}"#,
1676 r#"{"reasoning":"high","role":"builder"}"#,
1677 r#"{"reasoning":"high","allowed_tools":["shell"]}"#,
1678 r#"{"reasoning":"high","permissions":"full"}"#,
1679 ] {
1680 let err = parse_router_decision(raw).expect_err("route fields must be rejected");
1681 assert!(
1682 matches!(err, RouterDecisionError::RouteMutationAttempt { .. }),
1683 "raw={raw} err={err:?}"
1684 );
1685 }
1686 }
1687
1688 #[test]
1689 fn router_may_not_answer_auto_or_garbage() {
1690 assert!(matches!(
1691 parse_router_decision(r#"{"reasoning":"auto"}"#).expect_err("auto"),
1692 RouterDecisionError::AutoReasoning
1693 ));
1694 assert!(matches!(
1695 parse_router_decision(r#"{"reasoning":"turbo"}"#).expect_err("garbage"),
1696 RouterDecisionError::InvalidReasoning { .. }
1697 ));
1698 assert!(matches!(
1699 parse_router_decision("{}").expect_err("missing"),
1700 RouterDecisionError::MissingReasoning
1701 ));
1702 }
1703
1704 #[test]
1707 fn router_output_rejects_every_unknown_field_including_rationale() {
1708 for raw in [
1709 r#"{"reasoning":"high","rationale":"multi-file refactor"}"#,
1710 r#"{"reasoning":"high","confidence":0.9}"#,
1711 r#"{"reasoning":"high","notes":"just in case"}"#,
1712 r#"{"thinking":"high"}"#,
1713 ] {
1714 let err = parse_router_decision(raw).expect_err("strict output");
1715 assert!(
1716 matches!(err, RouterDecisionError::UnknownField { .. }),
1717 "raw={raw} err={err:?}"
1718 );
1719 }
1720
1721 let only = parse_router_decision(r#"{"reasoning":"low"}"#).expect("sole field accepted");
1722 assert_eq!(only.reasoning, ReasoningTier::Low);
1723 }
1724
1725 #[test]
1729 fn a_duplicated_reasoning_key_is_rejected_not_last_write_wins() {
1730 for raw in [
1731 r#"{"reasoning":"off","reasoning":"max"}"#,
1732 r#"{"reasoning":"max","reasoning":"max"}"#,
1733 r#"{"reasoning":"low","Reasoning":"max"}"#,
1734 ] {
1735 let err = parse_router_decision(raw).expect_err("duplicate key");
1736 assert!(
1737 matches!(err, RouterDecisionError::DuplicateField { .. }),
1738 "raw={raw} err={err:?}"
1739 );
1740 }
1741
1742 assert_eq!(
1744 parse_router_decision(r#"{"reasoning":"off"}"#)
1745 .expect("single key")
1746 .reasoning,
1747 ReasoningTier::Off
1748 );
1749 }
1750
1751 #[test]
1754 fn a_duplicate_is_reported_even_next_to_other_violations() {
1755 let err = parse_router_decision(r#"{"reasoning":"off","reasoning":"max","provider":"x"}"#)
1756 .expect_err("duplicate first");
1757 assert!(
1758 matches!(err, RouterDecisionError::DuplicateField { .. }),
1759 "{err:?}"
1760 );
1761 }
1762
1763 #[test]
1766 fn a_route_mutation_keeps_its_distinct_error_next_to_chatty_keys() {
1767 for raw in [
1768 r#"{"aaa_note":"x","reasoning":"high","provider":"deepseek"}"#,
1769 r#"{"provider":"deepseek","zzz_note":"x","reasoning":"high"}"#,
1770 ] {
1771 let err = parse_router_decision(raw).expect_err("route mutation");
1772 assert!(
1773 matches!(
1774 err,
1775 RouterDecisionError::RouteMutationAttempt { ref field } if field == "provider"
1776 ),
1777 "raw={raw} err={err:?}"
1778 );
1779 }
1780 }
1781
1782 #[test]
1785 fn a_native_adaptive_route_still_requires_the_router_for_auto() {
1786 let err = resolve_exact_member_reasoning(
1787 "implementer",
1788 &frozen(),
1789 RequestedReasoning::Auto,
1790 &ReasoningCapability::native_adaptive(),
1791 &RouterAvailability::Absent,
1792 None,
1793 None,
1794 )
1795 .expect_err("auto must reach the router even on a native-adaptive route");
1796 assert!(matches!(err, ReasoningResolveError::RouterRequired { .. }));
1797
1798 let decision = parse_router_decision(r#"{"reasoning":"low"}"#).expect("decision");
1799 let resolved = resolve_exact_member_reasoning(
1800 "implementer",
1801 &frozen(),
1802 RequestedReasoning::Auto,
1803 &ReasoningCapability::native_adaptive(),
1804 &RouterAvailability::Ready,
1805 Some(&decision),
1806 Some(&router_identity()),
1807 )
1808 .expect("router decides");
1809 assert_eq!(resolved.source(), EffectiveReasoningSource::FleetRouter);
1810 assert_eq!(
1811 resolved.provider_effective(),
1812 ProviderEffectiveReasoning::NativeAdaptive,
1813 "the route's real control is still reported, just not used as a bypass"
1814 );
1815 }
1816
1817 #[test]
1819 fn a_valid_object_followed_by_trailing_content_is_rejected() {
1820 for raw in [
1821 r#"{"reasoning":"high"} and I'd also suggest switching models"#,
1822 r#"{"reasoning":"high"}{"reasoning":"off"}"#,
1823 "{\"reasoning\":\"high\"}\n{\"reasoning\":\"max\"}",
1824 r#"{"reasoning":"high"} {"provider":"deepseek"}"#,
1825 ] {
1826 let err = parse_router_decision(raw).expect_err("one object and nothing else");
1827 assert!(
1828 matches!(err, RouterDecisionError::TrailingContent { .. }),
1829 "raw={raw} err={err:?}"
1830 );
1831 }
1832
1833 assert_eq!(
1836 parse_router_decision(" {\"reasoning\":\"low\"}\n\n")
1837 .expect("whitespace is fine")
1838 .reasoning,
1839 ReasoningTier::Low
1840 );
1841 assert_eq!(
1842 parse_router_decision("```json\n{\"reasoning\":\"max\"}\n```")
1843 .expect("a fence is formatting")
1844 .reasoning,
1845 ReasoningTier::Max
1846 );
1847 }
1848
1849 #[test]
1852 fn a_router_configured_low_is_called_at_low_and_receipts_it() {
1853 let plan = router_call_plan(RouterCallReasoning::Low, &ReasoningCapability::tiered());
1854
1855 assert_eq!(plan.tier, ReasoningTier::Low);
1856 assert_eq!(plan.disclosure.requested, "low");
1857 assert_eq!(plan.disclosure.effective, "low");
1858 assert_eq!(plan.disclosure.provider_control, "tiers");
1859 assert_eq!(plan.disclosure.provider_effective, "low");
1860 assert!(!plan.disclosure.capability_normalized);
1861
1862 let receipt = plan.disclosure.receipt();
1863 assert!(receipt.contains("router_call_requested=low"), "{receipt}");
1864 assert!(receipt.contains("router_call_effective=low"), "{receipt}");
1865 assert!(
1866 receipt.contains("router_call_provider_effective=low"),
1867 "{receipt}"
1868 );
1869 }
1870
1871 #[test]
1872 fn a_router_configured_off_stays_off() {
1873 let plan = router_call_plan(RouterCallReasoning::Off, &ReasoningCapability::tiered());
1874 assert_eq!(plan.tier, ReasoningTier::Off);
1875 assert_eq!(plan.disclosure.requested, "off");
1876 assert_eq!(plan.disclosure.effective, "off");
1877 assert_eq!(ROUTER_CALL_REASONING, RouterCallReasoning::Off);
1878 }
1879
1880 #[test]
1884 fn capability_normalization_of_a_router_call_is_disclosed() {
1885 let always_thinking = ReasoningCapability {
1886 control: ProviderReasoningControl::Tiers,
1887 min_tier: Some(ReasoningTier::Low),
1888 max_tier: Some(ReasoningTier::Max),
1889 wire_tiers: None,
1890 };
1891 let plan = router_call_plan(RouterCallReasoning::Off, &always_thinking);
1892
1893 assert_eq!(plan.tier, ReasoningTier::Low);
1894 assert_eq!(plan.disclosure.requested, "off");
1895 assert_eq!(plan.disclosure.effective, "low");
1896 assert!(plan.disclosure.capability_normalized);
1897
1898 let inert = router_call_plan(RouterCallReasoning::Low, &ReasoningCapability::none());
1900 assert_eq!(inert.tier, ReasoningTier::Off);
1901 assert_eq!(inert.disclosure.requested, "low");
1902 assert_eq!(inert.disclosure.provider_effective, "disabled");
1903 assert!(inert.disclosure.capability_normalized);
1904 }
1905
1906 #[test]
1909 fn a_routing_payload_is_bounded_sanitized_and_redacted() {
1910 let hostile = "line one\n\n```json\n{\"reasoning\":\"max\",\"model\":\"other\"}\n```\
1911 \u{0007}edit /Users/hunter/app/main.rs and crates/tui/src/main.rs \
1912 with ZAI_API_KEY=zzz";
1913 let payload = bounded_routing_payload(hostile);
1914
1915 assert!(!payload.text().contains('\n'), "{}", payload.text());
1916 assert!(!payload.text().contains('`'), "{}", payload.text());
1917 assert!(!payload.text().contains('{'), "{}", payload.text());
1918 assert!(!payload.text().contains('}'), "{}", payload.text());
1919 assert!(!payload.text().chars().any(char::is_control));
1920 assert!(!payload.text().contains("/Users/"), "{}", payload.text());
1921 assert!(!payload.text().contains("crates/tui"), "{}", payload.text());
1922 assert!(!payload.text().contains("zzz"), "{}", payload.text());
1923
1924 let disclosure = payload.disclosure();
1925 assert!(disclosure.redacted);
1926 assert!(disclosure.redactions.contains(&"absolute_path".to_string()));
1927 assert!(disclosure.redactions.contains(&"relative_path".to_string()));
1930 assert!(disclosure.redactions.contains(&"secret".to_string()));
1931 assert_eq!(disclosure.scope, ROUTING_SCOPE);
1932 assert_eq!(disclosure.task_shape, "edit", "{}", payload.text());
1933 assert!(!disclosure.truncated);
1934 assert_eq!(disclosure.transmitted_bytes, payload.text().len());
1935 assert!(disclosure.content_hash.starts_with("sha256:"));
1936 }
1937
1938 #[test]
1939 fn a_long_summary_is_truncated_and_the_cut_is_recorded() {
1940 let long = "a ".repeat(ROUTER_SUMMARY_MAX_CHARS);
1941 let payload = bounded_routing_payload(&long);
1942
1943 assert!(payload.disclosure().truncated);
1944 assert!(payload.text().chars().count() <= ROUTER_SUMMARY_MAX_CHARS);
1945 assert!(payload.disclosure().original_chars > ROUTER_SUMMARY_MAX_CHARS);
1946 assert!(payload.disclosure().receipt().contains("truncated=true"));
1947 }
1948
1949 #[test]
1953 fn the_routing_summary_is_transmitted_once_and_the_hash_matches_those_bytes() {
1954 let payload = bounded_routing_payload("refactor the parser across three crates");
1955 let disclosure = payload.disclosure().clone();
1956 let input = RouterCallInput {
1957 fleet: "workspace/glm-pair".to_string(),
1958 member_id: "implementer".to_string(),
1959 frozen: frozen(),
1960 payload,
1961 };
1962
1963 let system = router_system_prompt(&input);
1964 let user = router_user_message(&input);
1965
1966 assert!(
1967 !system.contains("refactor the parser"),
1968 "the system prompt must carry no task content: {system}"
1969 );
1970 assert!(system.contains("The next message is a bounded"), "{system}");
1971 assert_eq!(user, "refactor the parser across three crates");
1972
1973 assert_eq!(disclosure.transmitted_bytes, user.len());
1975 assert_eq!(disclosure.transmitted_chars, user.chars().count());
1976 assert_eq!(
1977 disclosure.content_hash,
1978 crate::named_fleet::sha256_label(user.as_bytes())
1979 );
1980
1981 let combined = format!("{system}\n{user}");
1983 assert_eq!(
1984 combined
1985 .matches("refactor the parser across three crates")
1986 .count(),
1987 1,
1988 "the summary must appear once across the whole request: {combined}"
1989 );
1990 }
1991
1992 #[test]
1993 fn the_router_prompt_states_the_frozen_route_and_forbids_moving_it() {
1994 let input = RouterCallInput {
1995 fleet: "workspace/glm-pair".to_string(),
1996 member_id: "implementer".to_string(),
1997 frozen: frozen(),
1998 payload: bounded_routing_payload("land a fix"),
1999 };
2000 let prompt = router_system_prompt(&input);
2001
2002 assert!(prompt.contains("already frozen"), "{prompt}");
2003 assert!(prompt.contains("glm-5"), "{prompt}");
2004 assert!(prompt.contains("fails the run"), "{prompt}");
2005 assert!(prompt.contains("reasoning-only service"), "{prompt}");
2006 assert!(prompt.contains("no repeated key"), "{prompt}");
2007 assert!(
2008 !prompt.to_ascii_lowercase().contains("rationale")
2009 || prompt.contains("not a rationale"),
2010 "the prompt must not invite a rationale: {prompt}"
2011 );
2012 }
2013
2014 #[test]
2018 fn a_receipt_discloses_everything_and_stores_no_content() {
2019 let decision = parse_router_decision(r#"{"reasoning":"max"}"#).expect("decision");
2020 let identity = router_identity();
2021 assert_eq!(identity.service_kind, "reasoning_router");
2022
2023 let resolved = resolve_exact_member_reasoning(
2024 "implementer",
2025 &frozen(),
2026 RequestedReasoning::Auto,
2027 &ReasoningCapability::enabled_disabled(),
2028 &RouterAvailability::Ready,
2029 Some(&decision),
2030 Some(&identity),
2031 )
2032 .expect("resolve");
2033
2034 let summary = bounded_routing_payload("land a fix in /Users/hunter/app")
2035 .with_cross_provider(true)
2036 .into_disclosure();
2037
2038 let receipt = FleetTaskReceipt::new(
2039 "workspace/glm-pair",
2040 "exact",
2041 1,
2042 "sha256:abc",
2043 "implementer",
2044 "builder",
2045 &preflighted(),
2046 &resolved,
2047 Some(summary),
2048 false,
2049 );
2050
2051 assert_eq!(receipt.member_id, "implementer");
2052 assert_eq!(receipt.member_role, "builder");
2053 assert_eq!(receipt.provider, "zai");
2054 assert_eq!(receipt.model, "glm-5");
2055 assert_eq!(receipt.requested_reasoning, "auto");
2056 assert_eq!(receipt.effective_reasoning, "max");
2057 assert_eq!(receipt.provider_effective_reasoning, "enabled");
2059 assert_eq!(receipt.provider_control, "enabled_disabled");
2060 assert_eq!(receipt.selection_source, "fleet_router");
2061 assert!(receipt.cross_provider_inference);
2062
2063 let router = receipt.router.as_ref().expect("router identity");
2064 assert_eq!(router.service_kind, "reasoning_router");
2065 assert_eq!(router.qualified(), "workspace/luna-low");
2066 assert_eq!(router.provider, "openai");
2067 assert_eq!(router.model, "gpt-5.6-luna");
2068 let call = router.call.as_ref().expect("call disclosure");
2069 assert_eq!(call.requested, "low");
2070 assert_eq!(call.effective, "low");
2071 assert_eq!(call.provider_effective, "low");
2072
2073 let disclosure = receipt.routing_summary.as_ref().expect("disclosure");
2075 assert!(disclosure.transmitted_bytes > 0);
2076 assert!(disclosure.content_hash.starts_with("sha256:"));
2077 assert!(disclosure.redacted);
2078
2079 let json = serde_json::to_string(&receipt).expect("serialize");
2080 assert!(
2081 !json.contains("land a fix"),
2082 "a receipt must never store task text: {json}"
2083 );
2084 assert!(!json.contains("/Users/"), "{json}");
2085 assert!(
2086 !json.contains("\"text\""),
2087 "a receipt must have no text field at all: {json}"
2088 );
2089 let lowered = json.to_ascii_lowercase();
2090 for forbidden in ["api_key", "secret\"", "bearer", "base_url"] {
2091 assert!(!lowered.contains(forbidden), "{forbidden} in {json}");
2092 }
2093
2094 let line = receipt.line();
2095 for expected in [
2096 "requested=auto",
2097 "effective=max",
2098 "provider_effective=enabled",
2099 "source=fleet_router",
2100 "router=reasoning_router:workspace/luna-low openai/gpt-5.6-luna",
2101 "router_call_requested=low",
2102 "cross_provider=true",
2103 ] {
2104 assert!(line.contains(expected), "{expected} missing from {line}");
2105 }
2106 assert!(
2107 !line.contains("land a fix"),
2108 "the visible line must not echo task text: {line}"
2109 );
2110
2111 let back: FleetTaskReceipt = serde_json::from_str(&json).expect("round-trip");
2112 assert_eq!(back, receipt);
2113 }
2114
2115 #[test]
2119 fn transport_disclosure_follows_the_member_network_tool_truth() {
2120 let without = transport_disclosure(false, false, false);
2121 assert!(
2122 without.contains("holds no model-visible network tool"),
2123 "{without}"
2124 );
2125 assert!(
2126 without.contains("Host-owned provider inference"),
2127 "{without}"
2128 );
2129
2130 let with = transport_disclosure(false, true, false);
2131 assert!(
2132 with.contains("also holds a model-visible network tool"),
2133 "a member that holds one must not be described as holding none: {with}"
2134 );
2135 assert!(
2136 !with.contains("holds no model-visible network tool"),
2137 "{with}"
2138 );
2139
2140 let cross = transport_disclosure(true, true, true);
2141 assert!(cross.contains("different provider"), "{cross}");
2142 let same = transport_disclosure(true, false, false);
2143 assert!(same.contains("same provider"), "{same}");
2144 }
2145
2146 #[test]
2148 fn a_network_capable_members_receipt_does_not_claim_it_has_no_network_tool() {
2149 let resolved = resolve_exact_member_reasoning(
2150 "implementer",
2151 &frozen(),
2152 RequestedReasoning::High,
2153 &ReasoningCapability::tiered(),
2154 &RouterAvailability::Absent,
2155 None,
2156 None,
2157 )
2158 .expect("resolve");
2159
2160 let receipt = FleetTaskReceipt::new(
2161 "workspace/glm-pair",
2162 "exact",
2163 1,
2164 "sha256:abc",
2165 "implementer",
2166 "builder",
2167 &preflighted(),
2168 &resolved,
2169 None,
2170 true,
2171 );
2172
2173 assert!(receipt.member_network_tool);
2174 assert!(
2175 receipt
2176 .transport
2177 .contains("also holds a model-visible network tool"),
2178 "{}",
2179 receipt.transport
2180 );
2181 assert!(!receipt.cross_provider_inference);
2182 assert!(receipt.routing_summary.is_none());
2183 }
2184
2185 #[test]
2190 fn a_receipt_keeps_the_semantic_role_and_the_permission_posture_apart() {
2191 let resolved = resolve_exact_member_reasoning(
2192 "auditor",
2193 &frozen(),
2194 RequestedReasoning::High,
2195 &ReasoningCapability::tiered(),
2196 &RouterAvailability::Absent,
2197 None,
2198 None,
2199 )
2200 .expect("resolve");
2201
2202 let receipt = FleetTaskReceipt::new(
2203 "workspace/glm-pair",
2204 "exact",
2205 1,
2206 "sha256:abc",
2207 "auditor",
2208 "auditor",
2209 &preflighted(),
2210 &resolved,
2211 None,
2212 false,
2213 )
2214 .with_posture_role("scout");
2215
2216 assert_eq!(receipt.member_role, "auditor");
2217 assert_eq!(receipt.posture_role.as_deref(), Some("scout"));
2218 let line = receipt.line();
2219 assert!(line.contains("(role auditor)"), "{line}");
2220 assert!(line.contains("posture=scout"), "{line}");
2221
2222 let json = serde_json::to_string(&receipt).expect("serialize");
2223 let back: FleetTaskReceipt = serde_json::from_str(&json).expect("round-trip");
2224 assert_eq!(back, receipt);
2225
2226 let same = FleetTaskReceipt::new(
2229 "workspace/glm-pair",
2230 "exact",
2231 1,
2232 "sha256:abc",
2233 "implementer",
2234 "builder",
2235 &preflighted(),
2236 &resolved,
2237 None,
2238 false,
2239 )
2240 .with_posture_role("builder");
2241 assert_eq!(same.posture_role, None);
2242 assert!(!same.line().contains("posture="), "{}", same.line());
2243 assert!(
2244 !serde_json::to_string(&same)
2245 .expect("serialize")
2246 .contains("posture_role")
2247 );
2248 }
2249
2250 #[test]
2253 fn a_receipt_records_the_canonical_wire_model_and_the_declared_one() {
2254 let mut route = preflighted();
2255 route.wire_model = "glm-5-20260101".to_string();
2256
2257 let resolved = resolve_exact_member_reasoning(
2258 "implementer",
2259 &route.frozen(),
2260 RequestedReasoning::Low,
2261 &ReasoningCapability::tiered(),
2262 &RouterAvailability::Absent,
2263 None,
2264 None,
2265 )
2266 .expect("resolve");
2267
2268 let receipt = FleetTaskReceipt::new(
2269 "workspace/glm-pair",
2270 "exact",
2271 1,
2272 "sha256:abc",
2273 "implementer",
2274 "builder",
2275 &route,
2276 &resolved,
2277 None,
2278 false,
2279 );
2280
2281 assert_eq!(receipt.model, "glm-5-20260101");
2282 assert_eq!(receipt.declared_model.as_deref(), Some("glm-5"));
2283 assert_eq!(
2284 receipt.endpoint.as_ref().expect("endpoint").host,
2285 "api.z.ai"
2286 );
2287 }
2288
2289 #[test]
2292 fn older_receipts_and_journals_still_deserialize() {
2293 let legacy = r#"{
2294 "fleet": "workspace/glm-pair",
2295 "member_id": "implementer",
2296 "member_role": "builder",
2297 "provider": "zai",
2298 "model": "glm-5",
2299 "requested_reasoning": "high",
2300 "effective_reasoning": "high",
2301 "provider_effective_reasoning": "enabled",
2302 "selection_source": "member_explicit"
2303 }"#;
2304 let receipt: FleetTaskReceipt = serde_json::from_str(legacy).expect("serde defaults");
2305 assert!(receipt.router.is_none());
2306 assert!(receipt.routing_summary.is_none());
2307 assert_eq!(receipt.schema_revision, 0);
2308 assert!(!receipt.cross_provider_inference);
2309
2310 let with_text = r#"{
2313 "fleet": "workspace/glm-pair",
2314 "member_id": "implementer",
2315 "member_role": "builder",
2316 "provider": "zai",
2317 "model": "glm-5",
2318 "requested_reasoning": "auto",
2319 "effective_reasoning": "max",
2320 "provider_effective_reasoning": "enabled",
2321 "selection_source": "fleet_router",
2322 "router": {"id":"router","role":"router","provider":"zai","model":"glm-5-turbo"},
2323 "routing_summary": {"text":"land a fix","original_chars":10,"truncated":false}
2324 }"#;
2325 let older: FleetTaskReceipt = serde_json::from_str(with_text).expect("serde defaults");
2326 let summary = older.routing_summary.as_ref().expect("summary");
2327 assert_eq!(summary.original_chars, 10);
2328 assert!(!summary.truncated);
2329 assert_eq!(summary.transmitted_bytes, 0, "unknown in an old journal");
2330 let router = older.router.as_ref().expect("router");
2331 assert_eq!(
2332 router.service_kind, "router",
2333 "the old `role` field aliases in"
2334 );
2335 assert_eq!(router.origin, "legacy_inline");
2336 }
2337
2338 #[test]
2339 fn capability_normalization_is_recorded_not_hidden() {
2340 let capped = ReasoningCapability {
2341 control: ProviderReasoningControl::Tiers,
2342 min_tier: Some(ReasoningTier::Low),
2343 max_tier: Some(ReasoningTier::High),
2344 wire_tiers: None,
2345 };
2346
2347 let raised = resolve_exact_member_reasoning(
2348 "w",
2349 &frozen(),
2350 RequestedReasoning::Off,
2351 &capped,
2352 &RouterAvailability::Absent,
2353 None,
2354 None,
2355 )
2356 .expect("resolve");
2357 assert_eq!(
2358 raised.effective(),
2359 EffectiveReasoning::Tier(ReasoningTier::Low)
2360 );
2361 assert!(raised.capability_normalized());
2362 assert_eq!(
2363 raised.requested(),
2364 RequestedReasoning::Off,
2365 "requested is preserved"
2366 );
2367
2368 let lowered = resolve_exact_member_reasoning(
2369 "w",
2370 &frozen(),
2371 RequestedReasoning::Max,
2372 &capped,
2373 &RouterAvailability::Absent,
2374 None,
2375 None,
2376 )
2377 .expect("resolve");
2378 assert_eq!(
2379 lowered.effective(),
2380 EffectiveReasoning::Tier(ReasoningTier::High)
2381 );
2382 assert!(lowered.capability_normalized());
2383
2384 let thinkless = resolve_exact_member_reasoning(
2385 "w",
2386 &frozen(),
2387 RequestedReasoning::Max,
2388 &ReasoningCapability::none(),
2389 &RouterAvailability::Absent,
2390 None,
2391 None,
2392 )
2393 .expect("resolve");
2394 assert_eq!(
2395 thinkless.effective(),
2396 EffectiveReasoning::Tier(ReasoningTier::Off)
2397 );
2398 }
2399
2400 #[test]
2401 fn legacy_auto_keeps_its_local_heuristic() {
2402 let resolved = resolve_legacy_reasoning(
2403 RequestedReasoning::Auto,
2404 &ReasoningCapability::tiered(),
2405 ReasoningTier::High,
2406 );
2407
2408 assert_eq!(resolved.requested(), RequestedReasoning::Auto);
2409 assert_eq!(
2410 resolved.effective(),
2411 EffectiveReasoning::Tier(ReasoningTier::High)
2412 );
2413 assert_eq!(resolved.source(), EffectiveReasoningSource::LegacyHeuristic);
2414 }
2415
2416 #[test]
2421 fn glm_style_routes_report_enabled_control_not_distinct_high_and_max() {
2422 let glm = ReasoningCapability::enabled_disabled();
2423
2424 let high = resolve_exact_member_reasoning(
2425 "implementer",
2426 &frozen(),
2427 RequestedReasoning::High,
2428 &glm,
2429 &RouterAvailability::Absent,
2430 None,
2431 None,
2432 )
2433 .expect("resolve");
2434 let max = resolve_exact_member_reasoning(
2435 "implementer",
2436 &frozen(),
2437 RequestedReasoning::Max,
2438 &glm,
2439 &RouterAvailability::Absent,
2440 None,
2441 None,
2442 )
2443 .expect("resolve");
2444
2445 assert_eq!(
2446 high.effective(),
2447 EffectiveReasoning::Tier(ReasoningTier::High)
2448 );
2449 assert_eq!(
2450 max.effective(),
2451 EffectiveReasoning::Tier(ReasoningTier::Max)
2452 );
2453 assert_eq!(
2454 high.provider_effective(),
2455 ProviderEffectiveReasoning::Enabled
2456 );
2457 assert_eq!(max.provider_effective(), high.provider_effective());
2458 assert_eq!(
2459 high.provider_control(),
2460 ProviderReasoningControl::EnabledDisabled
2461 );
2462
2463 let off = resolve_exact_member_reasoning(
2464 "implementer",
2465 &frozen(),
2466 RequestedReasoning::Off,
2467 &glm,
2468 &RouterAvailability::Absent,
2469 None,
2470 None,
2471 )
2472 .expect("resolve");
2473 assert_eq!(
2474 off.provider_effective(),
2475 ProviderEffectiveReasoning::Disabled,
2476 "off is the one distinction a GLM route can actually express"
2477 );
2478 }
2479
2480 #[test]
2482 fn a_tiered_route_reports_each_tier_as_its_own_provider_effective_control() {
2483 let tiered = ReasoningCapability::tiered();
2484 let mut seen = Vec::new();
2485 for requested in [
2486 RequestedReasoning::Low,
2487 RequestedReasoning::High,
2488 RequestedReasoning::Max,
2489 ] {
2490 let resolved = resolve_exact_member_reasoning(
2491 "w",
2492 &frozen(),
2493 requested,
2494 &tiered,
2495 &RouterAvailability::Absent,
2496 None,
2497 None,
2498 )
2499 .expect("resolve");
2500 seen.push(resolved.provider_effective());
2501 }
2502 assert_eq!(
2503 seen,
2504 vec![
2505 ProviderEffectiveReasoning::Tier(ReasoningTier::Low),
2506 ProviderEffectiveReasoning::Tier(ReasoningTier::High),
2507 ProviderEffectiveReasoning::Tier(ReasoningTier::Max),
2508 ]
2509 );
2510 }
2511
2512 #[test]
2514 fn no_default_capability_claims_provider_native_adaptive() {
2515 for capability in [
2516 ReasoningCapability::none(),
2517 ReasoningCapability::tiered(),
2518 ReasoningCapability::enabled_disabled(),
2519 ] {
2520 assert!(
2521 !capability.supports_native_adaptive(),
2522 "{capability:?} must not claim native adaptive"
2523 );
2524 }
2525 assert!(ReasoningCapability::native_adaptive().supports_native_adaptive());
2526 }
2527
2528 #[test]
2534 fn a_route_that_collapses_interior_tiers_receipts_the_tier_that_was_sent() {
2535 let collapsing = ReasoningCapability::tiered().with_wire_tiers([
2536 ReasoningTier::Off,
2537 ReasoningTier::High,
2538 ReasoningTier::High,
2539 ReasoningTier::High,
2540 ReasoningTier::Max,
2541 ]);
2542
2543 let low = resolve_exact_member_reasoning(
2544 "implementer",
2545 &frozen(),
2546 RequestedReasoning::Low,
2547 &collapsing,
2548 &RouterAvailability::Absent,
2549 None,
2550 None,
2551 )
2552 .expect("resolve");
2553
2554 assert_eq!(
2555 low.requested(),
2556 RequestedReasoning::Low,
2557 "requested survives"
2558 );
2559 assert_eq!(
2560 low.effective(),
2561 EffectiveReasoning::Tier(ReasoningTier::High),
2562 "the route sends high, so the receipt must say high"
2563 );
2564 assert!(low.capability_normalized(), "the move is recorded");
2565 assert_eq!(
2566 low.provider_effective(),
2567 ProviderEffectiveReasoning::Tier(ReasoningTier::High)
2568 );
2569 assert!(
2570 !low.receipt().contains("selected=low"),
2571 "a receipt must not name a tier the wire never carried: {}",
2572 low.receipt()
2573 );
2574
2575 let off = resolve_exact_member_reasoning(
2578 "implementer",
2579 &frozen(),
2580 RequestedReasoning::Off,
2581 &collapsing,
2582 &RouterAvailability::Absent,
2583 None,
2584 None,
2585 )
2586 .expect("resolve");
2587 assert_eq!(
2588 off.effective(),
2589 EffectiveReasoning::Tier(ReasoningTier::Off)
2590 );
2591 assert!(!off.capability_normalized());
2592 }
2593
2594 #[test]
2598 fn a_faithful_wire_map_is_not_recorded_and_older_capabilities_deserialize() {
2599 let faithful = ReasoningCapability::tiered().with_wire_tiers(FAITHFUL_WIRE_TIERS);
2600 assert_eq!(faithful.wire_tiers, None);
2601 assert_eq!(
2602 faithful.normalize(ReasoningTier::Low),
2603 (ReasoningTier::Low, false)
2604 );
2605 assert_eq!(
2606 faithful.wire_tier(ReasoningTier::Medium),
2607 ReasoningTier::Medium
2608 );
2609
2610 let older: ReasoningCapability =
2611 serde_json::from_str(r#"{"control":"tiers","min_tier":null,"max_tier":null}"#)
2612 .expect("serde default");
2613 assert_eq!(older, ReasoningCapability::tiered());
2614
2615 let collapsing = ReasoningCapability::tiered().with_wire_tiers([
2616 ReasoningTier::Off,
2617 ReasoningTier::High,
2618 ReasoningTier::High,
2619 ReasoningTier::High,
2620 ReasoningTier::Max,
2621 ]);
2622 let json = serde_json::to_string(&collapsing).expect("serialize");
2623 let back: ReasoningCapability = serde_json::from_str(&json).expect("round-trip");
2624 assert_eq!(back, collapsing);
2625 }
2626
2627 #[test]
2631 fn a_router_call_on_a_collapsing_route_discloses_the_tier_it_actually_ran_at() {
2632 let collapsing = ReasoningCapability::tiered().with_wire_tiers([
2633 ReasoningTier::Off,
2634 ReasoningTier::High,
2635 ReasoningTier::High,
2636 ReasoningTier::High,
2637 ReasoningTier::Max,
2638 ]);
2639 let plan = router_call_plan(RouterCallReasoning::Low, &collapsing);
2640
2641 assert_eq!(plan.tier, ReasoningTier::High);
2642 assert_eq!(plan.disclosure.requested, "low");
2643 assert_eq!(plan.disclosure.effective, "high");
2644 assert_eq!(plan.disclosure.provider_effective, "high");
2645 assert!(plan.disclosure.capability_normalized);
2646
2647 let off = router_call_plan(RouterCallReasoning::Off, &collapsing);
2649 assert_eq!(off.tier, ReasoningTier::Off);
2650 assert!(!off.disclosure.capability_normalized);
2651 }
2652
2653 #[test]
2654 fn task_shape_classification_is_coarse_and_content_free() {
2655 assert_eq!(
2656 TaskShape::classify("debug the flaky test"),
2657 TaskShape::Diagnose
2658 );
2659 assert_eq!(TaskShape::classify("refactor the parser"), TaskShape::Edit);
2660 assert_eq!(TaskShape::classify("review this diff"), TaskShape::Read);
2661 assert_eq!(TaskShape::classify("qqq"), TaskShape::Unclassified);
2662 }
2663}