converge_core/model_selection.rs
1// Copyright 2024-2025 Aprio One AB, Sweden
2// Author: Kenneth Pernyer, kenneth@aprio.one
3// SPDX-License-Identifier: LicenseRef-Proprietary
4// All rights reserved. This source code is proprietary and confidential.
5// Unauthorized copying, modification, or distribution is strictly prohibited.
6
7//! Model selection based on agent requirements.
8//!
9//! This module provides orthogonal selection dimensions that users reason about:
10//!
11//! 1. **Jurisdiction** - Where can data legally reside?
12//! 2. **`LatencyClass`** - How fast do you need responses?
13//! 3. **`CostTier`** - What's your budget preference?
14//! 4. **`TaskComplexity`** - How hard is the task?
15//! 5. **`RequiredCapabilities`** - What features are needed?
16//!
17//! # Design Principles
18//!
19//! These dimensions are orthogonal - each represents a distinct concern users have.
20//! "Local" is not a dimension; it's an *outcome* that emerges when:
21//! - Jurisdiction requires same-country AND no cloud provider exists there
22//! - Latency requires real-time AND network round-trip is too slow
23//! - Control requires on-premises infrastructure
24//!
25//! # Architecture
26//!
27//! - **Core (this module)**: Abstract requirements and selection trait
28//! - **Provider crate**: Concrete selector with all provider metadata
29//!
30//! This separation ensures core remains provider-agnostic while allowing
31//! injection of provider-specific selection logic.
32
33use crate::llm::LlmError;
34
35// =============================================================================
36// DIMENSION 1: JURISDICTION
37// =============================================================================
38
39/// Data jurisdiction requirements - where can data legally reside?
40///
41/// This is about legal/regulatory constraints, not latency or control.
42/// A Swedish cloud provider satisfies `SameCountry` for Swedish users
43/// without requiring local/on-premises infrastructure.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
45pub enum Jurisdiction {
46 /// No restrictions - data can flow anywhere.
47 #[default]
48 Unrestricted,
49 /// Trusted jurisdictions only (GDPR adequacy decisions, etc.).
50 /// Includes: EU/EEA, UK, Japan, Canada, Switzerland, etc.
51 Trusted,
52 /// Same region (e.g., EU/EEA for a Swedish user).
53 SameRegion,
54 /// Same country only (e.g., Sweden for a Swedish user).
55 SameCountry,
56}
57
58impl Jurisdiction {
59 /// Returns whether `provider_jurisdiction` satisfies this requirement.
60 ///
61 /// The `user_country` and `user_region` are needed to evaluate
62 /// `SameCountry` and `SameRegion` constraints.
63 #[must_use]
64 pub fn satisfied_by(
65 self,
66 provider_country: &str,
67 provider_region: &str,
68 user_country: &str,
69 user_region: &str,
70 ) -> bool {
71 match self {
72 Self::Unrestricted => true,
73 Self::Trusted => is_trusted_jurisdiction(provider_region),
74 Self::SameRegion => provider_region == user_region,
75 Self::SameCountry => provider_country == user_country,
76 }
77 }
78}
79
80/// Returns whether a region is considered "trusted" for data transfers.
81///
82/// Based on GDPR adequacy decisions and similar frameworks.
83fn is_trusted_jurisdiction(region: &str) -> bool {
84 matches!(
85 region.to_uppercase().as_str(),
86 "EU" | "EEA" | "CH" | "UK" | "JP" | "CA" | "NZ" | "IL" | "KR" | "AR" | "UY"
87 )
88}
89
90// =============================================================================
91// DIMENSION 2: LATENCY CLASS
92// =============================================================================
93
94/// Latency class requirements - how fast do you need responses?
95///
96/// This is about user experience and system architecture, not jurisdiction.
97/// A cloud provider with low network latency can satisfy `Interactive`
98/// even if it's in a different country.
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
100pub enum LatencyClass {
101 /// Real-time responses (<100ms) - interactive UI, streaming, gaming.
102 /// Often requires local inference or edge deployment.
103 Realtime,
104 /// Interactive responses (<2s) - user is actively waiting.
105 /// Regional cloud providers typically suffice.
106 #[default]
107 Interactive,
108 /// Background processing (<30s) - async tasks, user can context-switch.
109 /// Any reliable provider works.
110 Background,
111 /// Batch processing (minutes ok) - overnight jobs, bulk operations.
112 /// Optimize for cost over speed.
113 Batch,
114}
115
116impl LatencyClass {
117 /// Returns the maximum acceptable latency in milliseconds.
118 #[must_use]
119 pub fn max_latency_ms(self) -> u32 {
120 match self {
121 Self::Realtime => 100,
122 Self::Interactive => 2000,
123 Self::Background => 30000,
124 Self::Batch => 300_000, // 5 minutes
125 }
126 }
127
128 /// Returns whether a provider's typical latency satisfies this class.
129 #[must_use]
130 pub fn satisfied_by(self, provider_latency_ms: u32) -> bool {
131 provider_latency_ms <= self.max_latency_ms()
132 }
133}
134
135// =============================================================================
136// DIMENSION 3: COST TIER
137// =============================================================================
138
139/// Cost tier preference - what's your budget constraint?
140///
141/// This is about economic optimization, not capability.
142/// `Minimal` doesn't mean low quality - it means "cheapest model
143/// that can successfully complete the task."
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
145pub enum CostTier {
146 /// Use the cheapest model that can do the job.
147 /// Good for high-volume, simple tasks.
148 Minimal,
149 /// Balance quality and cost.
150 /// Good for general-purpose usage.
151 #[default]
152 Standard,
153 /// Best quality regardless of cost.
154 /// Good for critical decisions, complex reasoning.
155 Premium,
156}
157
158// =============================================================================
159// DIMENSION 4: TASK COMPLEXITY
160// =============================================================================
161
162/// Task complexity hint - how hard is this task?
163///
164/// This helps the selector choose appropriately-sized models.
165/// A simple extraction task doesn't need a 400B parameter model.
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
167pub enum TaskComplexity {
168 /// Simple extraction - pull structured data from text.
169 /// Example: Extract names and dates from a document.
170 Extraction,
171 /// Classification - categorize input into predefined buckets.
172 /// Example: Sentiment analysis, topic classification.
173 #[default]
174 Classification,
175 /// Reasoning - multi-step logic, inference, analysis.
176 /// Example: Analyze trade-offs, identify root causes.
177 Reasoning,
178 /// Generation - create substantial new content.
179 /// Example: Write an article, generate code.
180 Generation,
181}
182
183impl TaskComplexity {
184 /// Returns minimum quality score typically needed for this complexity.
185 #[must_use]
186 pub fn min_quality_hint(self) -> f64 {
187 match self {
188 Self::Extraction => 0.5,
189 Self::Classification => 0.6,
190 Self::Reasoning => 0.8,
191 Self::Generation => 0.7,
192 }
193 }
194
195 /// Returns whether this complexity typically requires reasoning capabilities.
196 #[must_use]
197 pub fn requires_reasoning(self) -> bool {
198 matches!(self, Self::Reasoning)
199 }
200}
201
202// =============================================================================
203// DIMENSION 5: REQUIRED CAPABILITIES
204// =============================================================================
205
206/// Required model capabilities - what features does the task need?
207///
208/// These are binary feature flags. A model either has the capability or doesn't.
209#[derive(Debug, Clone, PartialEq, Default)]
210#[allow(clippy::struct_excessive_bools)]
211pub struct RequiredCapabilities {
212 /// Needs function/tool calling (structured function invocation).
213 pub tool_use: bool,
214 /// Needs vision/image understanding.
215 pub vision: bool,
216 /// Minimum context window size in tokens (None = no requirement).
217 pub min_context_tokens: Option<usize>,
218 /// Needs structured output (JSON mode, schema enforcement).
219 pub structured_output: bool,
220 /// Needs code generation/understanding.
221 pub code: bool,
222 /// Needs multilingual support.
223 pub multilingual: bool,
224 /// Needs web search / real-time information.
225 pub web_search: bool,
226}
227
228impl RequiredCapabilities {
229 /// Creates empty capability requirements.
230 #[must_use]
231 pub fn none() -> Self {
232 Self::default()
233 }
234
235 /// Requires tool/function calling.
236 #[must_use]
237 pub fn with_tool_use(mut self) -> Self {
238 self.tool_use = true;
239 self
240 }
241
242 /// Requires vision capabilities.
243 #[must_use]
244 pub fn with_vision(mut self) -> Self {
245 self.vision = true;
246 self
247 }
248
249 /// Requires minimum context window.
250 #[must_use]
251 pub fn with_min_context(mut self, tokens: usize) -> Self {
252 self.min_context_tokens = Some(tokens);
253 self
254 }
255
256 /// Requires structured output.
257 #[must_use]
258 pub fn with_structured_output(mut self) -> Self {
259 self.structured_output = true;
260 self
261 }
262
263 /// Requires code capabilities.
264 #[must_use]
265 pub fn with_code(mut self) -> Self {
266 self.code = true;
267 self
268 }
269
270 /// Requires multilingual support.
271 #[must_use]
272 pub fn with_multilingual(mut self) -> Self {
273 self.multilingual = true;
274 self
275 }
276
277 /// Requires web search.
278 #[must_use]
279 pub fn with_web_search(mut self) -> Self {
280 self.web_search = true;
281 self
282 }
283}
284
285// =============================================================================
286// LEGACY TYPES (for backward compatibility)
287// =============================================================================
288
289/// Cost classification for model selection.
290///
291/// **Note**: Consider using `CostTier` instead for new code.
292/// This is retained for backward compatibility with existing model metadata.
293#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
294pub enum CostClass {
295 /// Very low cost (e.g., Haiku, GPT-3.5 Turbo, Gemini Flash)
296 VeryLow,
297 /// Low cost (e.g., Sonnet, GPT-4 Turbo)
298 Low,
299 /// Medium cost (e.g., Opus, GPT-4)
300 Medium,
301 /// High cost (e.g., Opus-4, GPT-4o)
302 High,
303 /// Very high cost (e.g., specialized models)
304 VeryHigh,
305}
306
307impl CostClass {
308 /// Returns the maximum cost class that satisfies this requirement.
309 ///
310 /// For example, `CostClass::Low` allows `VeryLow` and Low.
311 #[must_use]
312 pub fn allowed_classes(self) -> Vec<CostClass> {
313 match self {
314 Self::VeryLow => vec![Self::VeryLow],
315 Self::Low => vec![Self::VeryLow, Self::Low],
316 Self::Medium => vec![Self::VeryLow, Self::Low, Self::Medium],
317 Self::High => vec![Self::VeryLow, Self::Low, Self::Medium, Self::High],
318 Self::VeryHigh => vec![
319 Self::VeryLow,
320 Self::Low,
321 Self::Medium,
322 Self::High,
323 Self::VeryHigh,
324 ],
325 }
326 }
327
328 /// Converts from `CostTier` preference to maximum allowed `CostClass`.
329 #[must_use]
330 pub fn from_tier(tier: CostTier) -> Self {
331 match tier {
332 CostTier::Minimal => Self::Low,
333 CostTier::Standard => Self::Medium,
334 CostTier::Premium => Self::VeryHigh,
335 }
336 }
337}
338
339/// Data sovereignty requirements.
340///
341/// **Note**: Consider using `Jurisdiction` instead for new code.
342/// This is retained for backward compatibility with existing model metadata.
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
344pub enum DataSovereignty {
345 /// No specific requirements (default).
346 Any,
347 /// Data must remain in EU/EEA.
348 EU,
349 /// Data must remain in Switzerland.
350 Switzerland,
351 /// Data must remain in China.
352 China,
353 /// Data must remain in US.
354 US,
355 /// Self-hosted or on-premises.
356 OnPremises,
357}
358
359impl DataSovereignty {
360 /// Converts from new Jurisdiction type.
361 ///
362 /// Note: This is a lossy conversion since Jurisdiction is more abstract.
363 /// Use this only for backward compatibility.
364 #[must_use]
365 pub fn from_jurisdiction(jurisdiction: Jurisdiction, user_region: &str) -> Self {
366 match jurisdiction {
367 Jurisdiction::Unrestricted | Jurisdiction::Trusted => Self::Any,
368 Jurisdiction::SameRegion => match user_region.to_uppercase().as_str() {
369 "EU" | "EEA" => Self::EU,
370 "CH" => Self::Switzerland,
371 "CN" => Self::China,
372 "US" => Self::US,
373 _ => Self::Any,
374 },
375 Jurisdiction::SameCountry => Self::OnPremises, // Approximation
376 }
377 }
378}
379
380/// Compliance and explainability requirements.
381#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
382pub enum ComplianceLevel {
383 /// No specific compliance requirements (default).
384 None,
385 /// GDPR compliance required.
386 GDPR,
387 /// SOC 2 compliance required.
388 SOC2,
389 /// HIPAA compliance required.
390 HIPAA,
391 /// High explainability (audit trails, provenance).
392 HighExplainability,
393}
394
395// =============================================================================
396// SELECTION CRITERIA (NEW - uses orthogonal dimensions)
397// =============================================================================
398
399/// Selection criteria using orthogonal dimensions.
400///
401/// This is the recommended way to specify model requirements.
402/// Each field represents an independent concern users reason about.
403///
404/// # Example
405///
406/// ```
407/// use converge_core::model_selection::{SelectionCriteria, Jurisdiction, LatencyClass, CostTier, TaskComplexity, RequiredCapabilities};
408///
409/// // Swedish fintech needing GDPR compliance
410/// let criteria = SelectionCriteria::default()
411/// .with_jurisdiction(Jurisdiction::SameRegion) // EU data residency
412/// .with_latency(LatencyClass::Interactive) // User is waiting
413/// .with_cost(CostTier::Standard) // Balance cost/quality
414/// .with_complexity(TaskComplexity::Reasoning) // Complex analysis
415/// .with_capabilities(RequiredCapabilities::none().with_structured_output());
416/// ```
417#[derive(Debug, Clone, PartialEq, Default)]
418pub struct SelectionCriteria {
419 /// Where can data legally reside?
420 pub jurisdiction: Jurisdiction,
421 /// How fast do you need responses?
422 pub latency: LatencyClass,
423 /// What's your budget preference?
424 pub cost: CostTier,
425 /// How hard is the task?
426 pub complexity: TaskComplexity,
427 /// What features are required?
428 pub capabilities: RequiredCapabilities,
429 /// Compliance requirements (optional).
430 pub compliance: Option<ComplianceLevel>,
431 /// User's country (for jurisdiction evaluation).
432 pub user_country: Option<String>,
433 /// User's region (for jurisdiction evaluation).
434 pub user_region: Option<String>,
435}
436
437impl SelectionCriteria {
438 /// Creates criteria for high-volume, simple tasks.
439 ///
440 /// Use case: Extraction, classification at scale.
441 #[must_use]
442 pub fn high_volume() -> Self {
443 Self {
444 latency: LatencyClass::Interactive,
445 cost: CostTier::Minimal,
446 complexity: TaskComplexity::Extraction,
447 ..Default::default()
448 }
449 }
450
451 /// Creates criteria for interactive user-facing tasks.
452 ///
453 /// Use case: Chat, Q&A, customer support.
454 #[must_use]
455 pub fn interactive() -> Self {
456 Self {
457 latency: LatencyClass::Interactive,
458 cost: CostTier::Standard,
459 complexity: TaskComplexity::Classification,
460 ..Default::default()
461 }
462 }
463
464 /// Creates criteria for complex analysis tasks.
465 ///
466 /// Use case: Research, strategy, multi-step reasoning.
467 #[must_use]
468 pub fn analysis() -> Self {
469 Self {
470 latency: LatencyClass::Background,
471 cost: CostTier::Premium,
472 complexity: TaskComplexity::Reasoning,
473 ..Default::default()
474 }
475 }
476
477 /// Creates criteria for batch processing.
478 ///
479 /// Use case: Overnight jobs, bulk operations.
480 #[must_use]
481 pub fn batch() -> Self {
482 Self {
483 latency: LatencyClass::Batch,
484 cost: CostTier::Minimal,
485 complexity: TaskComplexity::Extraction,
486 ..Default::default()
487 }
488 }
489
490 /// Sets jurisdiction requirement.
491 #[must_use]
492 pub fn with_jurisdiction(mut self, jurisdiction: Jurisdiction) -> Self {
493 self.jurisdiction = jurisdiction;
494 self
495 }
496
497 /// Sets latency class.
498 #[must_use]
499 pub fn with_latency(mut self, latency: LatencyClass) -> Self {
500 self.latency = latency;
501 self
502 }
503
504 /// Sets cost tier.
505 #[must_use]
506 pub fn with_cost(mut self, cost: CostTier) -> Self {
507 self.cost = cost;
508 self
509 }
510
511 /// Sets task complexity.
512 #[must_use]
513 pub fn with_complexity(mut self, complexity: TaskComplexity) -> Self {
514 self.complexity = complexity;
515 self
516 }
517
518 /// Sets required capabilities.
519 #[must_use]
520 pub fn with_capabilities(mut self, capabilities: RequiredCapabilities) -> Self {
521 self.capabilities = capabilities;
522 self
523 }
524
525 /// Sets compliance requirement.
526 #[must_use]
527 pub fn with_compliance(mut self, compliance: ComplianceLevel) -> Self {
528 self.compliance = Some(compliance);
529 self
530 }
531
532 /// Sets user location for jurisdiction evaluation.
533 #[must_use]
534 pub fn with_user_location(
535 mut self,
536 country: impl Into<String>,
537 region: impl Into<String>,
538 ) -> Self {
539 self.user_country = Some(country.into());
540 self.user_region = Some(region.into());
541 self
542 }
543
544 /// Converts to legacy `AgentRequirements` for backward compatibility.
545 #[must_use]
546 pub fn to_legacy_requirements(&self) -> AgentRequirements {
547 let user_region = self.user_region.as_deref().unwrap_or("US");
548 AgentRequirements {
549 max_cost_class: CostClass::from_tier(self.cost),
550 max_latency_ms: self.latency.max_latency_ms(),
551 requires_reasoning: self.complexity.requires_reasoning(),
552 requires_web_search: self.capabilities.web_search,
553 min_quality: self.complexity.min_quality_hint(),
554 data_sovereignty: DataSovereignty::from_jurisdiction(self.jurisdiction, user_region),
555 compliance: self.compliance.unwrap_or(ComplianceLevel::None),
556 requires_multilingual: self.capabilities.multilingual,
557 }
558 }
559}
560
561// =============================================================================
562// LEGACY AGENT REQUIREMENTS (for backward compatibility)
563// =============================================================================
564
565/// Requirements for an agent's LLM usage.
566///
567/// **Note**: Consider using `SelectionCriteria` instead for new code.
568/// This struct is retained for backward compatibility.
569///
570/// Agents specify their requirements, and the model selector
571/// finds the best matching model.
572#[derive(Debug, Clone, PartialEq)]
573pub struct AgentRequirements {
574 /// Maximum acceptable cost class.
575 pub max_cost_class: CostClass,
576 /// Maximum acceptable latency in milliseconds.
577 pub max_latency_ms: u32,
578 /// Whether the agent requires advanced reasoning capabilities.
579 pub requires_reasoning: bool,
580 /// Whether the agent requires web search capabilities.
581 pub requires_web_search: bool,
582 /// Minimum quality threshold (0.0-1.0).
583 /// Higher values prefer more capable models.
584 pub min_quality: f64,
585 /// Data sovereignty requirements.
586 pub data_sovereignty: DataSovereignty,
587 /// Compliance and explainability requirements.
588 pub compliance: ComplianceLevel,
589 /// Whether the agent requires multi-language support.
590 pub requires_multilingual: bool,
591}
592
593impl AgentRequirements {
594 /// Creates requirements for a fast, cheap agent (many instances).
595 ///
596 /// Use case: High-volume agents that need quick, cost-effective responses.
597 #[must_use]
598 pub fn fast_cheap() -> Self {
599 Self {
600 max_cost_class: CostClass::VeryLow,
601 max_latency_ms: 2000,
602 requires_reasoning: false,
603 requires_web_search: false,
604 min_quality: 0.6,
605 data_sovereignty: DataSovereignty::Any,
606 compliance: ComplianceLevel::None,
607 requires_multilingual: false,
608 }
609 }
610
611 /// Creates requirements for a deep research agent.
612 ///
613 /// Use case: Agents that need thorough analysis and reasoning.
614 #[must_use]
615 pub fn deep_research() -> Self {
616 Self {
617 max_cost_class: CostClass::High,
618 max_latency_ms: 30000, // 30 seconds
619 requires_reasoning: true,
620 requires_web_search: true,
621 min_quality: 0.9,
622 data_sovereignty: DataSovereignty::Any,
623 compliance: ComplianceLevel::None,
624 requires_multilingual: false,
625 }
626 }
627
628 /// Creates requirements for a balanced agent.
629 ///
630 /// Use case: General-purpose agents with moderate requirements.
631 #[must_use]
632 pub fn balanced() -> Self {
633 Self {
634 max_cost_class: CostClass::Medium,
635 max_latency_ms: 5000,
636 requires_reasoning: false,
637 requires_web_search: false,
638 min_quality: 0.7,
639 data_sovereignty: DataSovereignty::Any,
640 compliance: ComplianceLevel::None,
641 requires_multilingual: false,
642 }
643 }
644
645 /// Creates custom requirements.
646 #[must_use]
647 pub fn new(max_cost_class: CostClass, max_latency_ms: u32, requires_reasoning: bool) -> Self {
648 Self {
649 max_cost_class,
650 max_latency_ms,
651 requires_reasoning,
652 requires_web_search: false,
653 min_quality: 0.7,
654 data_sovereignty: DataSovereignty::Any,
655 compliance: ComplianceLevel::None,
656 requires_multilingual: false,
657 }
658 }
659
660 /// Sets web search requirement.
661 #[must_use]
662 pub fn with_web_search(mut self, requires: bool) -> Self {
663 self.requires_web_search = requires;
664 self
665 }
666
667 /// Sets minimum quality threshold.
668 #[must_use]
669 pub fn with_min_quality(mut self, quality: f64) -> Self {
670 self.min_quality = quality.clamp(0.0, 1.0);
671 self
672 }
673
674 /// Sets data sovereignty requirement.
675 #[must_use]
676 pub fn with_data_sovereignty(mut self, sovereignty: DataSovereignty) -> Self {
677 self.data_sovereignty = sovereignty;
678 self
679 }
680
681 /// Sets compliance requirement.
682 #[must_use]
683 pub fn with_compliance(mut self, compliance: ComplianceLevel) -> Self {
684 self.compliance = compliance;
685 self
686 }
687
688 /// Sets multilingual requirement.
689 #[must_use]
690 pub fn with_multilingual(mut self, requires: bool) -> Self {
691 self.requires_multilingual = requires;
692 self
693 }
694
695 /// Creates from new `SelectionCriteria`.
696 #[must_use]
697 pub fn from_criteria(criteria: &SelectionCriteria) -> Self {
698 criteria.to_legacy_requirements()
699 }
700}
701
702// ModelMetadata and ModelSelector implementations are in converge-provider.
703// Core only provides the abstract interface (ModelSelectorTrait) and requirements.
704
705/// Trait for model selection based on agent requirements.
706///
707/// This trait allows injecting provider-specific model selection logic
708/// without coupling core to concrete providers.
709///
710/// Concrete implementations (with provider metadata) are in `converge-provider`.
711pub trait ModelSelectorTrait: Send + Sync {
712 /// Selects the best model for the given requirements.
713 ///
714 /// Returns `(provider, model)` if a suitable model is found.
715 ///
716 /// # Errors
717 ///
718 /// Returns error if no model satisfies the requirements.
719 fn select(&self, requirements: &AgentRequirements) -> Result<(String, String), LlmError>;
720}
721
722#[cfg(test)]
723mod tests {
724 use super::*;
725
726 // =========================================================================
727 // NEW DIMENSION TESTS
728 // =========================================================================
729
730 #[test]
731 fn test_jurisdiction_trusted() {
732 // GDPR adequacy countries should be trusted
733 assert!(is_trusted_jurisdiction("EU"));
734 assert!(is_trusted_jurisdiction("EEA"));
735 assert!(is_trusted_jurisdiction("CH"));
736 assert!(is_trusted_jurisdiction("UK"));
737 assert!(is_trusted_jurisdiction("JP"));
738
739 // Non-adequacy countries should not
740 assert!(!is_trusted_jurisdiction("CN"));
741 assert!(!is_trusted_jurisdiction("RU"));
742 }
743
744 #[test]
745 fn test_jurisdiction_same_region() {
746 let jurisdiction = Jurisdiction::SameRegion;
747
748 // Swedish user, EU provider = OK
749 assert!(jurisdiction.satisfied_by("DE", "EU", "SE", "EU"));
750
751 // Swedish user, US provider = NOT OK
752 assert!(!jurisdiction.satisfied_by("US", "US", "SE", "EU"));
753 }
754
755 #[test]
756 fn test_latency_class_thresholds() {
757 assert_eq!(LatencyClass::Realtime.max_latency_ms(), 100);
758 assert_eq!(LatencyClass::Interactive.max_latency_ms(), 2000);
759 assert_eq!(LatencyClass::Background.max_latency_ms(), 30000);
760 assert_eq!(LatencyClass::Batch.max_latency_ms(), 300_000);
761 }
762
763 #[test]
764 fn test_latency_satisfied_by() {
765 assert!(LatencyClass::Interactive.satisfied_by(1500));
766 assert!(!LatencyClass::Interactive.satisfied_by(3000));
767 assert!(LatencyClass::Background.satisfied_by(3000));
768 }
769
770 #[test]
771 fn test_task_complexity_hints() {
772 assert!(
773 TaskComplexity::Extraction.min_quality_hint()
774 < TaskComplexity::Reasoning.min_quality_hint()
775 );
776 assert!(TaskComplexity::Reasoning.requires_reasoning());
777 assert!(!TaskComplexity::Extraction.requires_reasoning());
778 }
779
780 #[test]
781 fn test_required_capabilities_builder() {
782 let caps = RequiredCapabilities::none()
783 .with_tool_use()
784 .with_vision()
785 .with_min_context(128_000);
786
787 assert!(caps.tool_use);
788 assert!(caps.vision);
789 assert_eq!(caps.min_context_tokens, Some(128_000));
790 assert!(!caps.code);
791 }
792
793 #[test]
794 fn test_selection_criteria_presets() {
795 let high_vol = SelectionCriteria::high_volume();
796 assert_eq!(high_vol.cost, CostTier::Minimal);
797 assert_eq!(high_vol.complexity, TaskComplexity::Extraction);
798
799 let analysis = SelectionCriteria::analysis();
800 assert_eq!(analysis.cost, CostTier::Premium);
801 assert_eq!(analysis.complexity, TaskComplexity::Reasoning);
802 }
803
804 #[test]
805 fn test_selection_criteria_to_legacy() {
806 let criteria = SelectionCriteria::default()
807 .with_latency(LatencyClass::Background)
808 .with_cost(CostTier::Premium)
809 .with_complexity(TaskComplexity::Reasoning);
810
811 let legacy = criteria.to_legacy_requirements();
812
813 assert_eq!(legacy.max_latency_ms, 30000);
814 assert!(legacy.requires_reasoning);
815 assert!(legacy.min_quality >= 0.8);
816 }
817
818 #[test]
819 fn test_cost_class_from_tier() {
820 assert_eq!(CostClass::from_tier(CostTier::Minimal), CostClass::Low);
821 assert_eq!(CostClass::from_tier(CostTier::Standard), CostClass::Medium);
822 assert_eq!(CostClass::from_tier(CostTier::Premium), CostClass::VeryHigh);
823 }
824
825 // =========================================================================
826 // LEGACY TESTS (retained for backward compatibility)
827 // =========================================================================
828
829 #[test]
830 fn test_fast_cheap_requirements() {
831 let reqs = AgentRequirements::fast_cheap();
832 assert_eq!(reqs.max_cost_class, CostClass::VeryLow);
833 assert_eq!(reqs.max_latency_ms, 2000);
834 assert!(!reqs.requires_reasoning);
835 }
836
837 #[test]
838 fn test_deep_research_requirements() {
839 let reqs = AgentRequirements::deep_research();
840 assert!(reqs.max_cost_class >= CostClass::High);
841 assert!(reqs.requires_reasoning);
842 assert!(reqs.requires_web_search);
843 }
844
845 #[test]
846 fn test_cost_class_allowed() {
847 assert_eq!(CostClass::VeryLow.allowed_classes().len(), 1);
848 assert_eq!(CostClass::Low.allowed_classes().len(), 2);
849 assert_eq!(CostClass::VeryHigh.allowed_classes().len(), 5);
850 }
851}