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