1use serde::{Deserialize, Serialize};
7
8use crate::backend::BackendKind;
9use crate::capability::Capability;
10use crate::chat::LlmError;
11use crate::error::BackendError;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct BackendRequirements {
16 pub kind: BackendKind,
17 pub required_capabilities: Vec<Capability>,
18 pub max_cost_class: CostClass,
19 pub max_latency_ms: u32,
20 pub data_sovereignty: DataSovereignty,
21 pub compliance: ComplianceLevel,
22 pub requires_replay: bool,
23 pub requires_offline: bool,
24}
25
26impl BackendRequirements {
27 #[must_use]
28 pub fn new(kind: BackendKind) -> Self {
29 Self {
30 kind,
31 required_capabilities: Vec::new(),
32 max_cost_class: CostClass::VeryHigh,
33 max_latency_ms: 0,
34 data_sovereignty: DataSovereignty::Any,
35 compliance: ComplianceLevel::None,
36 requires_replay: false,
37 requires_offline: false,
38 }
39 }
40
41 #[must_use]
42 pub fn with_capability(mut self, capability: Capability) -> Self {
43 self.required_capabilities.push(capability);
44 self
45 }
46
47 #[must_use]
48 pub fn with_max_cost(mut self, cost: CostClass) -> Self {
49 self.max_cost_class = cost;
50 self
51 }
52
53 #[must_use]
54 pub fn with_max_latency_ms(mut self, ms: u32) -> Self {
55 self.max_latency_ms = ms;
56 self
57 }
58
59 #[must_use]
60 pub fn with_data_sovereignty(mut self, sovereignty: DataSovereignty) -> Self {
61 self.data_sovereignty = sovereignty;
62 self
63 }
64
65 #[must_use]
66 pub fn with_compliance(mut self, compliance: ComplianceLevel) -> Self {
67 self.compliance = compliance;
68 self
69 }
70
71 #[must_use]
72 pub fn with_replay(mut self) -> Self {
73 self.requires_replay = true;
74 self
75 }
76
77 #[must_use]
78 pub fn with_offline(mut self) -> Self {
79 self.requires_offline = true;
80 self
81 }
82
83 #[must_use]
84 pub fn fast_llm() -> Self {
85 Self::new(BackendKind::Llm)
86 .with_capability(Capability::TextGeneration)
87 .with_max_cost(CostClass::Low)
88 .with_max_latency_ms(2_000)
89 }
90
91 #[must_use]
92 pub fn reasoning_llm() -> Self {
93 Self::new(BackendKind::Llm)
94 .with_capability(Capability::TextGeneration)
95 .with_capability(Capability::Reasoning)
96 .with_max_cost(CostClass::High)
97 .with_max_latency_ms(30_000)
98 }
99
100 #[must_use]
101 pub fn access_policy() -> Self {
102 Self::new(BackendKind::Policy)
103 .with_capability(Capability::AccessControl)
104 .with_max_latency_ms(100)
105 }
106
107 #[must_use]
108 pub fn constraint_solver() -> Self {
109 Self::new(BackendKind::Optimization).with_capability(Capability::ConstraintSolving)
110 }
111
112 #[must_use]
113 pub fn embedding_pipeline() -> Self {
114 Self::new(BackendKind::Analytics).with_capability(Capability::Embedding)
115 }
116
117 #[must_use]
118 pub fn vector_search() -> Self {
119 Self::new(BackendKind::Search).with_capability(Capability::VectorSearch)
120 }
121}
122
123pub trait BackendSelector: Send + Sync {
125 fn select(&self, requirements: &BackendRequirements) -> Result<String, BackendError>;
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct ProviderRequest {
131 pub id: String,
133 pub required_capabilities: Vec<Capability>,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub backend_requirements: Option<BackendRequirements>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct ProviderAssignment {
148 pub request_id: String,
150 pub assignments: Vec<CapabilityAssignment>,
152 pub unmatched: Vec<Capability>,
154 pub coverage_ratio: f64,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160pub struct CapabilityAssignment {
161 pub capability: Capability,
162 pub backend_name: String,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
167pub enum Jurisdiction {
168 #[default]
169 Unrestricted,
170 Trusted,
171 SameRegion,
172 SameCountry,
173}
174
175impl Jurisdiction {
176 #[must_use]
177 pub fn satisfied_by(
178 self,
179 provider_country: &str,
180 provider_region: &str,
181 user_country: &str,
182 user_region: &str,
183 ) -> bool {
184 match self {
185 Self::Unrestricted => true,
186 Self::Trusted => is_trusted_jurisdiction(provider_region),
187 Self::SameRegion => provider_region == user_region,
188 Self::SameCountry => provider_country == user_country,
189 }
190 }
191}
192
193fn is_trusted_jurisdiction(region: &str) -> bool {
194 matches!(
195 region.to_uppercase().as_str(),
196 "EU" | "EEA" | "CH" | "UK" | "JP" | "CA" | "NZ" | "IL" | "KR" | "AR" | "UY"
197 )
198}
199
200#[derive(
202 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
203)]
204pub enum LatencyClass {
205 Realtime,
206 #[default]
207 Interactive,
208 Background,
209 Batch,
210}
211
212impl LatencyClass {
213 #[must_use]
214 pub fn max_latency_ms(self) -> u32 {
215 match self {
216 Self::Realtime => 100,
217 Self::Interactive => 2_000,
218 Self::Background => 30_000,
219 Self::Batch => 300_000,
220 }
221 }
222
223 #[must_use]
224 pub fn satisfied_by(self, provider_latency_ms: u32) -> bool {
225 provider_latency_ms <= self.max_latency_ms()
226 }
227}
228
229#[derive(
231 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize,
232)]
233pub enum CostTier {
234 Minimal,
235 #[default]
236 Standard,
237 Premium,
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
242pub enum TaskComplexity {
243 Extraction,
244 #[default]
245 Classification,
246 Reasoning,
247 Generation,
248}
249
250impl TaskComplexity {
251 #[must_use]
252 pub fn min_quality_hint(self) -> f64 {
253 match self {
254 Self::Extraction => 0.5,
255 Self::Classification => 0.6,
256 Self::Reasoning => 0.8,
257 Self::Generation => 0.7,
258 }
259 }
260
261 #[must_use]
262 pub fn requires_reasoning(self) -> bool {
263 matches!(self, Self::Reasoning)
264 }
265}
266
267#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
269#[allow(clippy::struct_excessive_bools)]
270pub struct RequiredCapabilities {
271 pub tool_use: bool,
272 pub vision: bool,
273 pub min_context_tokens: Option<usize>,
274 pub structured_output: bool,
275 pub code: bool,
276 pub multilingual: bool,
277 pub web_search: bool,
278}
279
280impl RequiredCapabilities {
281 #[must_use]
282 pub fn none() -> Self {
283 Self::default()
284 }
285
286 #[must_use]
287 pub fn with_tool_use(mut self) -> Self {
288 self.tool_use = true;
289 self
290 }
291
292 #[must_use]
293 pub fn with_vision(mut self) -> Self {
294 self.vision = true;
295 self
296 }
297
298 #[must_use]
299 pub fn with_min_context(mut self, tokens: usize) -> Self {
300 self.min_context_tokens = Some(tokens);
301 self
302 }
303
304 #[must_use]
305 pub fn with_structured_output(mut self) -> Self {
306 self.structured_output = true;
307 self
308 }
309
310 #[must_use]
311 pub fn with_code(mut self) -> Self {
312 self.code = true;
313 self
314 }
315
316 #[must_use]
317 pub fn with_multilingual(mut self) -> Self {
318 self.multilingual = true;
319 self
320 }
321
322 #[must_use]
323 pub fn with_web_search(mut self) -> Self {
324 self.web_search = true;
325 self
326 }
327}
328
329#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
331pub enum CostClass {
332 Free,
333 VeryLow,
334 Low,
335 Medium,
336 High,
337 VeryHigh,
338}
339
340impl CostClass {
341 #[must_use]
342 pub fn allowed_classes(self) -> Vec<CostClass> {
343 let all = [
344 CostClass::Free,
345 CostClass::VeryLow,
346 CostClass::Low,
347 CostClass::Medium,
348 CostClass::High,
349 CostClass::VeryHigh,
350 ];
351 all.iter().copied().filter(|&c| c <= self).collect()
352 }
353
354 #[must_use]
355 pub fn from_tier(tier: CostTier) -> Self {
356 match tier {
357 CostTier::Minimal => Self::Low,
358 CostTier::Standard => Self::Medium,
359 CostTier::Premium => Self::VeryHigh,
360 }
361 }
362}
363
364#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
366pub enum DataSovereignty {
367 Any,
368 EU,
369 US,
370 Switzerland,
371 China,
372 OnPremises,
373}
374
375impl DataSovereignty {
376 #[must_use]
377 pub fn from_jurisdiction(jurisdiction: Jurisdiction, user_region: &str) -> Self {
378 match jurisdiction {
379 Jurisdiction::Unrestricted | Jurisdiction::Trusted => Self::Any,
380 Jurisdiction::SameRegion => match user_region.to_uppercase().as_str() {
381 "EU" | "EEA" => Self::EU,
382 "CH" => Self::Switzerland,
383 "CN" => Self::China,
384 "US" => Self::US,
385 _ => Self::Any,
386 },
387 Jurisdiction::SameCountry => Self::OnPremises,
388 }
389 }
390}
391
392#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
394pub enum ComplianceLevel {
395 None,
396 GDPR,
397 HIPAA,
398 SOC2,
399 HighExplainability,
400}
401
402#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
404pub struct SelectionCriteria {
405 pub jurisdiction: Jurisdiction,
406 pub latency: LatencyClass,
407 pub cost: CostTier,
408 pub complexity: TaskComplexity,
409 pub capabilities: RequiredCapabilities,
410 pub compliance: Option<ComplianceLevel>,
411 pub user_country: Option<String>,
412 pub user_region: Option<String>,
413}
414
415impl SelectionCriteria {
416 #[must_use]
417 pub fn high_volume() -> Self {
418 Self {
419 latency: LatencyClass::Interactive,
420 cost: CostTier::Minimal,
421 complexity: TaskComplexity::Extraction,
422 ..Default::default()
423 }
424 }
425
426 #[must_use]
427 pub fn interactive() -> Self {
428 Self {
429 latency: LatencyClass::Interactive,
430 cost: CostTier::Minimal,
431 complexity: TaskComplexity::Classification,
432 ..Default::default()
433 }
434 }
435
436 #[must_use]
437 pub fn analysis() -> Self {
438 Self {
439 latency: LatencyClass::Background,
440 cost: CostTier::Premium,
441 complexity: TaskComplexity::Reasoning,
442 ..Default::default()
443 }
444 }
445
446 #[must_use]
447 pub fn batch() -> Self {
448 Self {
449 latency: LatencyClass::Batch,
450 cost: CostTier::Minimal,
451 complexity: TaskComplexity::Extraction,
452 ..Default::default()
453 }
454 }
455
456 #[must_use]
457 pub fn with_jurisdiction(mut self, jurisdiction: Jurisdiction) -> Self {
458 self.jurisdiction = jurisdiction;
459 self
460 }
461
462 #[must_use]
463 pub fn with_latency(mut self, latency: LatencyClass) -> Self {
464 self.latency = latency;
465 self
466 }
467
468 #[must_use]
469 pub fn with_cost(mut self, cost: CostTier) -> Self {
470 self.cost = cost;
471 self
472 }
473
474 #[must_use]
475 pub fn with_complexity(mut self, complexity: TaskComplexity) -> Self {
476 self.complexity = complexity;
477 self
478 }
479
480 #[must_use]
481 pub fn with_capabilities(mut self, capabilities: RequiredCapabilities) -> Self {
482 self.capabilities = capabilities;
483 self
484 }
485
486 #[must_use]
487 pub fn with_compliance(mut self, compliance: ComplianceLevel) -> Self {
488 self.compliance = Some(compliance);
489 self
490 }
491
492 #[must_use]
493 pub fn with_user_location(
494 mut self,
495 country: impl Into<String>,
496 region: impl Into<String>,
497 ) -> Self {
498 self.user_country = Some(country.into());
499 self.user_region = Some(region.into());
500 self
501 }
502
503 #[must_use]
504 pub fn to_agent_requirements(&self) -> AgentRequirements {
505 let user_region = self.user_region.as_deref().unwrap_or("US");
506 AgentRequirements {
507 max_cost_class: CostClass::from_tier(self.cost),
508 max_latency_ms: self.latency.max_latency_ms(),
509 requires_reasoning: self.complexity.requires_reasoning(),
510 requires_web_search: self.capabilities.web_search,
511 requires_tool_use: self.capabilities.tool_use,
512 requires_vision: self.capabilities.vision,
513 min_context_tokens: self.capabilities.min_context_tokens,
514 requires_structured_output: self.capabilities.structured_output,
515 requires_code: self.capabilities.code,
516 min_quality: self.complexity.min_quality_hint(),
517 data_sovereignty: DataSovereignty::from_jurisdiction(self.jurisdiction, user_region),
518 compliance: self.compliance.unwrap_or(ComplianceLevel::None),
519 requires_multilingual: self.capabilities.multilingual,
520 }
521 }
522}
523
524#[derive(Debug, Clone, PartialEq)]
526pub struct AgentRequirements {
527 pub max_cost_class: CostClass,
528 pub max_latency_ms: u32,
529 pub requires_reasoning: bool,
530 pub requires_web_search: bool,
531 pub requires_tool_use: bool,
532 pub requires_vision: bool,
533 pub min_context_tokens: Option<usize>,
534 pub requires_structured_output: bool,
535 pub requires_code: bool,
536 pub min_quality: f64,
537 pub data_sovereignty: DataSovereignty,
538 pub compliance: ComplianceLevel,
539 pub requires_multilingual: bool,
540}
541
542impl AgentRequirements {
543 #[must_use]
544 pub fn fast_cheap() -> Self {
545 Self {
546 max_cost_class: CostClass::VeryLow,
547 max_latency_ms: 2_000,
548 requires_reasoning: false,
549 requires_web_search: false,
550 requires_tool_use: false,
551 requires_vision: false,
552 min_context_tokens: None,
553 requires_structured_output: false,
554 requires_code: false,
555 min_quality: 0.6,
556 data_sovereignty: DataSovereignty::Any,
557 compliance: ComplianceLevel::None,
558 requires_multilingual: false,
559 }
560 }
561
562 #[must_use]
563 pub fn deep_research() -> Self {
564 Self {
565 max_cost_class: CostClass::High,
566 max_latency_ms: 30_000,
567 requires_reasoning: true,
568 requires_web_search: true,
569 requires_tool_use: false,
570 requires_vision: false,
571 min_context_tokens: None,
572 requires_structured_output: false,
573 requires_code: false,
574 min_quality: 0.9,
575 data_sovereignty: DataSovereignty::Any,
576 compliance: ComplianceLevel::None,
577 requires_multilingual: false,
578 }
579 }
580
581 #[must_use]
582 pub fn balanced() -> Self {
583 Self {
584 max_cost_class: CostClass::Medium,
585 max_latency_ms: 5_000,
586 requires_reasoning: false,
587 requires_web_search: false,
588 requires_tool_use: false,
589 requires_vision: false,
590 min_context_tokens: None,
591 requires_structured_output: false,
592 requires_code: false,
593 min_quality: 0.7,
594 data_sovereignty: DataSovereignty::Any,
595 compliance: ComplianceLevel::None,
596 requires_multilingual: false,
597 }
598 }
599
600 #[must_use]
601 pub fn new(max_cost_class: CostClass, max_latency_ms: u32, requires_reasoning: bool) -> Self {
602 Self {
603 max_cost_class,
604 max_latency_ms,
605 requires_reasoning,
606 requires_web_search: false,
607 requires_tool_use: false,
608 requires_vision: false,
609 min_context_tokens: None,
610 requires_structured_output: false,
611 requires_code: false,
612 min_quality: 0.7,
613 data_sovereignty: DataSovereignty::Any,
614 compliance: ComplianceLevel::None,
615 requires_multilingual: false,
616 }
617 }
618
619 #[must_use]
620 pub fn powerful() -> Self {
621 Self {
622 max_cost_class: CostClass::High,
623 max_latency_ms: 10_000,
624 requires_reasoning: true,
625 requires_web_search: false,
626 requires_tool_use: false,
627 requires_vision: false,
628 min_context_tokens: None,
629 requires_structured_output: false,
630 requires_code: false,
631 min_quality: 0.9,
632 data_sovereignty: DataSovereignty::Any,
633 compliance: ComplianceLevel::None,
634 requires_multilingual: false,
635 }
636 }
637
638 #[must_use]
639 pub fn with_quality(self, quality: f64) -> Self {
640 self.with_min_quality(quality)
641 }
642
643 #[must_use]
644 pub fn with_web_search(mut self, requires: bool) -> Self {
645 self.requires_web_search = requires;
646 self
647 }
648
649 #[must_use]
650 pub fn with_tool_use(mut self, requires: bool) -> Self {
651 self.requires_tool_use = requires;
652 self
653 }
654
655 #[must_use]
656 pub fn with_vision(mut self, requires: bool) -> Self {
657 self.requires_vision = requires;
658 self
659 }
660
661 #[must_use]
662 pub fn with_min_context(mut self, tokens: usize) -> Self {
663 self.min_context_tokens = Some(tokens);
664 self
665 }
666
667 #[must_use]
668 pub fn with_structured_output(mut self, requires: bool) -> Self {
669 self.requires_structured_output = requires;
670 self
671 }
672
673 #[must_use]
674 pub fn with_code(mut self, requires: bool) -> Self {
675 self.requires_code = requires;
676 self
677 }
678
679 #[must_use]
680 pub fn with_min_quality(mut self, quality: f64) -> Self {
681 self.min_quality = quality.clamp(0.0, 1.0);
682 self
683 }
684
685 #[must_use]
686 pub fn with_data_sovereignty(mut self, sovereignty: DataSovereignty) -> Self {
687 self.data_sovereignty = sovereignty;
688 self
689 }
690
691 #[must_use]
692 pub fn with_compliance(mut self, compliance: ComplianceLevel) -> Self {
693 self.compliance = compliance;
694 self
695 }
696
697 #[must_use]
698 pub fn with_multilingual(mut self, requires: bool) -> Self {
699 self.requires_multilingual = requires;
700 self
701 }
702
703 #[must_use]
704 pub fn from_criteria(criteria: &SelectionCriteria) -> Self {
705 criteria.to_agent_requirements()
706 }
707}
708
709pub trait ModelSelectorTrait: Send + Sync {
711 fn select(&self, requirements: &AgentRequirements) -> Result<(String, String), LlmError>;
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717
718 #[test]
719 fn cost_class_ordering() {
720 assert!(CostClass::Free < CostClass::VeryLow);
721 assert!(CostClass::VeryLow < CostClass::Low);
722 assert!(CostClass::Low < CostClass::Medium);
723 assert!(CostClass::Medium < CostClass::High);
724 assert!(CostClass::High < CostClass::VeryHigh);
725 }
726
727 #[test]
728 fn requirements_builder() {
729 let reqs = BackendRequirements::new(BackendKind::Llm)
730 .with_capability(Capability::TextGeneration)
731 .with_capability(Capability::Reasoning)
732 .with_max_cost(CostClass::Medium)
733 .with_max_latency_ms(5_000);
734
735 assert_eq!(reqs.kind, BackendKind::Llm);
736 assert_eq!(reqs.required_capabilities.len(), 2);
737 assert_eq!(reqs.max_cost_class, CostClass::Medium);
738 assert_eq!(reqs.max_latency_ms, 5_000);
739 }
740
741 #[test]
742 fn selection_criteria_presets() {
743 let high_vol = SelectionCriteria::high_volume();
744 assert_eq!(high_vol.cost, CostTier::Minimal);
745 assert_eq!(high_vol.complexity, TaskComplexity::Extraction);
746
747 let analysis = SelectionCriteria::analysis();
748 assert_eq!(analysis.cost, CostTier::Premium);
749 assert_eq!(analysis.complexity, TaskComplexity::Reasoning);
750 }
751
752 #[test]
753 fn selection_criteria_to_agent_requirements() {
754 let criteria = SelectionCriteria::default()
755 .with_latency(LatencyClass::Background)
756 .with_cost(CostTier::Premium)
757 .with_complexity(TaskComplexity::Reasoning)
758 .with_capabilities(
759 RequiredCapabilities::none()
760 .with_tool_use()
761 .with_vision()
762 .with_min_context(128_000)
763 .with_structured_output()
764 .with_code(),
765 );
766 let requirements = criteria.to_agent_requirements();
767 assert_eq!(requirements.max_latency_ms, 30_000);
768 assert!(requirements.requires_reasoning);
769 assert!(requirements.min_quality >= 0.8);
770 assert!(requirements.requires_tool_use);
771 assert!(requirements.requires_vision);
772 assert_eq!(requirements.min_context_tokens, Some(128_000));
773 assert!(requirements.requires_structured_output);
774 assert!(requirements.requires_code);
775 }
776
777 #[test]
778 fn preset_constructors() {
779 let fast = BackendRequirements::fast_llm();
780 assert_eq!(fast.kind, BackendKind::Llm);
781 assert_eq!(fast.max_cost_class, CostClass::Low);
782
783 let policy = BackendRequirements::access_policy();
784 assert_eq!(policy.kind, BackendKind::Policy);
785 assert!(
786 policy
787 .required_capabilities
788 .contains(&Capability::AccessControl)
789 );
790
791 let solver = BackendRequirements::constraint_solver();
792 assert_eq!(solver.kind, BackendKind::Optimization);
793 }
794
795 #[test]
796 fn preset_reasoning_llm() {
797 let r = BackendRequirements::reasoning_llm();
798 assert_eq!(r.kind, BackendKind::Llm);
799 assert_eq!(r.max_cost_class, CostClass::High);
800 assert_eq!(r.max_latency_ms, 30_000);
801 assert!(
802 r.required_capabilities
803 .contains(&Capability::TextGeneration)
804 );
805 assert!(r.required_capabilities.contains(&Capability::Reasoning));
806 }
807
808 #[test]
809 fn preset_embedding_pipeline() {
810 let r = BackendRequirements::embedding_pipeline();
811 assert_eq!(r.kind, BackendKind::Analytics);
812 assert!(r.required_capabilities.contains(&Capability::Embedding));
813 }
814
815 #[test]
816 fn preset_vector_search() {
817 let r = BackendRequirements::vector_search();
818 assert_eq!(r.kind, BackendKind::Search);
819 assert!(r.required_capabilities.contains(&Capability::VectorSearch));
820 }
821
822 #[test]
823 fn jurisdiction_unrestricted_always_satisfied() {
824 assert!(Jurisdiction::Unrestricted.satisfied_by("US", "US", "SE", "EU"));
825 }
826
827 #[test]
828 fn jurisdiction_trusted_eu() {
829 assert!(Jurisdiction::Trusted.satisfied_by("SE", "EU", "SE", "EU"));
830 }
831
832 #[test]
833 fn jurisdiction_trusted_us_not_trusted() {
834 assert!(!Jurisdiction::Trusted.satisfied_by("US", "US", "SE", "EU"));
835 }
836
837 #[test]
838 fn jurisdiction_trusted_various() {
839 for region in &["CH", "UK", "JP", "CA", "NZ", "IL", "KR", "AR", "UY", "EEA"] {
840 assert!(
841 Jurisdiction::Trusted.satisfied_by("X", region, "Y", "Z"),
842 "expected {region} to be trusted"
843 );
844 }
845 }
846
847 #[test]
848 fn jurisdiction_same_region() {
849 assert!(Jurisdiction::SameRegion.satisfied_by("SE", "EU", "DE", "EU"));
850 assert!(!Jurisdiction::SameRegion.satisfied_by("SE", "EU", "US", "US"));
851 }
852
853 #[test]
854 fn jurisdiction_same_country() {
855 assert!(Jurisdiction::SameCountry.satisfied_by("SE", "EU", "SE", "EU"));
856 assert!(!Jurisdiction::SameCountry.satisfied_by("SE", "EU", "DE", "EU"));
857 }
858
859 #[test]
860 fn latency_class_max_values() {
861 assert_eq!(LatencyClass::Realtime.max_latency_ms(), 100);
862 assert_eq!(LatencyClass::Interactive.max_latency_ms(), 2_000);
863 assert_eq!(LatencyClass::Background.max_latency_ms(), 30_000);
864 assert_eq!(LatencyClass::Batch.max_latency_ms(), 300_000);
865 }
866
867 #[test]
868 fn latency_class_satisfied_by() {
869 assert!(LatencyClass::Realtime.satisfied_by(50));
870 assert!(LatencyClass::Realtime.satisfied_by(100));
871 assert!(!LatencyClass::Realtime.satisfied_by(101));
872
873 assert!(LatencyClass::Interactive.satisfied_by(2_000));
874 assert!(!LatencyClass::Interactive.satisfied_by(2_001));
875 }
876
877 #[test]
878 fn latency_class_ordering() {
879 assert!(LatencyClass::Realtime < LatencyClass::Interactive);
880 assert!(LatencyClass::Interactive < LatencyClass::Background);
881 assert!(LatencyClass::Background < LatencyClass::Batch);
882 }
883
884 #[test]
885 fn data_sovereignty_from_jurisdiction_unrestricted() {
886 assert_eq!(
887 DataSovereignty::from_jurisdiction(Jurisdiction::Unrestricted, "EU"),
888 DataSovereignty::Any
889 );
890 }
891
892 #[test]
893 fn data_sovereignty_from_jurisdiction_trusted() {
894 assert_eq!(
895 DataSovereignty::from_jurisdiction(Jurisdiction::Trusted, "EU"),
896 DataSovereignty::Any
897 );
898 }
899
900 #[test]
901 fn data_sovereignty_from_jurisdiction_same_region_eu() {
902 assert_eq!(
903 DataSovereignty::from_jurisdiction(Jurisdiction::SameRegion, "EU"),
904 DataSovereignty::EU
905 );
906 assert_eq!(
907 DataSovereignty::from_jurisdiction(Jurisdiction::SameRegion, "eea"),
908 DataSovereignty::EU
909 );
910 }
911
912 #[test]
913 fn data_sovereignty_from_jurisdiction_same_region_other() {
914 assert_eq!(
915 DataSovereignty::from_jurisdiction(Jurisdiction::SameRegion, "CH"),
916 DataSovereignty::Switzerland
917 );
918 assert_eq!(
919 DataSovereignty::from_jurisdiction(Jurisdiction::SameRegion, "CN"),
920 DataSovereignty::China
921 );
922 assert_eq!(
923 DataSovereignty::from_jurisdiction(Jurisdiction::SameRegion, "US"),
924 DataSovereignty::US
925 );
926 assert_eq!(
927 DataSovereignty::from_jurisdiction(Jurisdiction::SameRegion, "XX"),
928 DataSovereignty::Any
929 );
930 }
931
932 #[test]
933 fn data_sovereignty_from_jurisdiction_same_country() {
934 assert_eq!(
935 DataSovereignty::from_jurisdiction(Jurisdiction::SameCountry, "SE"),
936 DataSovereignty::OnPremises
937 );
938 }
939
940 #[test]
941 fn cost_class_allowed_contains_self() {
942 for class in [
943 CostClass::Free,
944 CostClass::VeryLow,
945 CostClass::Low,
946 CostClass::Medium,
947 CostClass::High,
948 CostClass::VeryHigh,
949 ] {
950 let allowed = class.allowed_classes();
951 assert!(
952 allowed.contains(&class),
953 "{class:?} should contain itself in allowed_classes"
954 );
955 }
956 }
957
958 #[test]
959 fn cost_class_allowed_counts() {
960 assert_eq!(CostClass::Free.allowed_classes().len(), 1);
961 assert_eq!(CostClass::VeryLow.allowed_classes().len(), 2);
962 assert_eq!(CostClass::Low.allowed_classes().len(), 3);
963 assert_eq!(CostClass::Medium.allowed_classes().len(), 4);
964 assert_eq!(CostClass::High.allowed_classes().len(), 5);
965 assert_eq!(CostClass::VeryHigh.allowed_classes().len(), 6);
966 }
967
968 #[test]
969 fn cost_class_from_tier() {
970 assert_eq!(CostClass::from_tier(CostTier::Minimal), CostClass::Low);
971 assert_eq!(CostClass::from_tier(CostTier::Standard), CostClass::Medium);
972 assert_eq!(CostClass::from_tier(CostTier::Premium), CostClass::VeryHigh);
973 }
974
975 #[test]
976 fn task_complexity_min_quality() {
977 assert!((TaskComplexity::Extraction.min_quality_hint() - 0.5).abs() < f64::EPSILON);
978 assert!((TaskComplexity::Classification.min_quality_hint() - 0.6).abs() < f64::EPSILON);
979 assert!((TaskComplexity::Reasoning.min_quality_hint() - 0.8).abs() < f64::EPSILON);
980 assert!((TaskComplexity::Generation.min_quality_hint() - 0.7).abs() < f64::EPSILON);
981 }
982
983 #[test]
984 fn task_complexity_requires_reasoning() {
985 assert!(!TaskComplexity::Extraction.requires_reasoning());
986 assert!(!TaskComplexity::Classification.requires_reasoning());
987 assert!(TaskComplexity::Reasoning.requires_reasoning());
988 assert!(!TaskComplexity::Generation.requires_reasoning());
989 }
990
991 #[test]
992 fn agent_requirements_fast_cheap() {
993 let r = AgentRequirements::fast_cheap();
994 assert_eq!(r.max_cost_class, CostClass::VeryLow);
995 assert_eq!(r.max_latency_ms, 2_000);
996 assert!(!r.requires_reasoning);
997 }
998
999 #[test]
1000 fn agent_requirements_deep_research() {
1001 let r = AgentRequirements::deep_research();
1002 assert!(r.requires_reasoning);
1003 assert!(r.requires_web_search);
1004 assert!(r.min_quality >= 0.9);
1005 }
1006
1007 #[test]
1008 fn agent_requirements_with_min_quality_clamped() {
1009 let r = AgentRequirements::fast_cheap().with_min_quality(2.0);
1010 assert!((r.min_quality - 1.0).abs() < f64::EPSILON);
1011
1012 let r = AgentRequirements::fast_cheap().with_min_quality(-1.0);
1013 assert!(r.min_quality.abs() < f64::EPSILON);
1014 }
1015
1016 #[test]
1017 fn provider_request_and_assignment_roundtrip() {
1018 let request = ProviderRequest {
1019 id: "req-1".to_string(),
1020 required_capabilities: vec![Capability::Reasoning, Capability::Scheduling],
1021 backend_requirements: None,
1022 };
1023 let assignment = ProviderAssignment {
1024 request_id: request.id.clone(),
1025 assignments: vec![CapabilityAssignment {
1026 capability: Capability::Reasoning,
1027 backend_name: "solver-a".to_string(),
1028 }],
1029 unmatched: vec![Capability::Scheduling],
1030 coverage_ratio: 0.5,
1031 };
1032
1033 let request_back: ProviderRequest =
1034 serde_json::from_str(&serde_json::to_string(&request).unwrap()).unwrap();
1035 let assignment_back: ProviderAssignment =
1036 serde_json::from_str(&serde_json::to_string(&assignment).unwrap()).unwrap();
1037
1038 assert!(request_back.backend_requirements.is_none());
1039 assert_eq!(
1040 request_back.required_capabilities,
1041 request.required_capabilities
1042 );
1043 assert_eq!(assignment_back.assignments, assignment.assignments);
1044 assert_eq!(assignment_back.unmatched, assignment.unmatched);
1045 }
1046
1047 #[test]
1048 fn provider_request_defaults_missing_backend_requirements() {
1049 let legacy_json = r#"{"id":"legacy","required_capabilities":["Reasoning"]}"#;
1050
1051 let request: ProviderRequest = serde_json::from_str(legacy_json).unwrap();
1052
1053 assert_eq!(request.id, "legacy");
1054 assert_eq!(request.required_capabilities, vec![Capability::Reasoning]);
1055 assert!(request.backend_requirements.is_none());
1056 }
1057}