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