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}