Skip to main content

systemprompt_models/validators/
rate_limits.rs

1//! Domain validator for rate-limit configuration.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use super::ValidationConfigProvider;
7use crate::config::RateLimitConfig;
8use systemprompt_traits::validation_report::{
9    ValidationError, ValidationReport, ValidationWarning,
10};
11use systemprompt_traits::{ConfigProvider, DomainConfig, DomainConfigError};
12
13#[derive(Debug, Default, Clone, Copy)]
14pub struct RateLimitsConfigValidator {
15    config: Option<RateLimitConfig>,
16}
17
18impl RateLimitsConfigValidator {
19    pub fn new() -> Self {
20        Self::default()
21    }
22}
23
24impl DomainConfig for RateLimitsConfigValidator {
25    fn domain_id(&self) -> &'static str {
26        "rate_limits"
27    }
28
29    fn priority(&self) -> u32 {
30        10
31    }
32
33    fn load(&mut self, config: &dyn ConfigProvider) -> Result<(), DomainConfigError> {
34        let provider = config
35            .as_any()
36            .downcast_ref::<ValidationConfigProvider>()
37            .ok_or_else(|| DomainConfigError::LoadError {
38                message: "Expected ValidationConfigProvider".into(),
39            })?;
40
41        self.config = Some(provider.config().rate_limits);
42        Ok(())
43    }
44
45    fn validate(&self) -> Result<ValidationReport, DomainConfigError> {
46        let mut report = ValidationReport::new("rate_limits");
47        let config = self
48            .config
49            .as_ref()
50            .ok_or_else(|| DomainConfigError::ValidationError {
51                message: "Not loaded".into(),
52            })?;
53
54        if config.disabled {
55            return Ok(report);
56        }
57
58        Self::validate_quota_limits(&mut report, config);
59        Self::validate_stream_limits(&mut report, config);
60        Self::validate_agent_limits(&mut report, config);
61
62        Ok(report)
63    }
64}
65
66impl RateLimitsConfigValidator {
67    fn validate_quota_limits(report: &mut ValidationReport, config: &RateLimitConfig) {
68        if config.burst_multiplier == 0 {
69            report.add_error(ValidationError::new(
70                "rate_limits.burst_multiplier",
71                "burst_multiplier must be at least 1; a zero burst leaves every route unlimited",
72            ));
73        }
74
75        let limits: [(&str, u64); 11] = [
76            ("oauth_public_per_second", config.oauth_public_per_second),
77            ("oauth_auth_per_second", config.oauth_auth_per_second),
78            ("contexts_per_second", config.contexts_per_second),
79            ("tasks_per_second", config.tasks_per_second),
80            ("artifacts_per_second", config.artifacts_per_second),
81            (
82                "agent_registry_per_second",
83                config.agent_registry_per_second,
84            ),
85            ("agents_per_second", config.agents_per_second),
86            ("mcp_registry_per_second", config.mcp_registry_per_second),
87            ("mcp_per_second", config.mcp_per_second),
88            ("stream_per_second", config.stream_per_second),
89            ("content_per_second", config.content_per_second),
90        ];
91
92        for (field, per_second) in limits {
93            if per_second == 0 {
94                report.add_error(ValidationError::new(
95                    format!("rate_limits.{field}"),
96                    "must be at least 1; a zero rate leaves the route unlimited",
97                ));
98                continue;
99            }
100
101            if per_second.saturating_mul(config.burst_multiplier) > u64::from(u32::MAX) {
102                report.add_error(ValidationError::new(
103                    format!("rate_limits.{field}"),
104                    format!(
105                        "{per_second} x burst_multiplier {} exceeds the maximum representable \
106                         burst of {}",
107                        config.burst_multiplier,
108                        u32::MAX
109                    ),
110                ));
111            }
112        }
113    }
114
115    fn validate_stream_limits(report: &mut ValidationReport, config: &RateLimitConfig) {
116        if config.stream_per_second < 10 {
117            report.add_warning(
118                ValidationWarning::new(
119                    "rate_limits.stream_per_second",
120                    format!(
121                        "stream_per_second={} is restrictive. Users may experience connection \
122                         issues.",
123                        config.stream_per_second
124                    ),
125                )
126                .with_suggestion("Consider increasing to at least 10 for production use"),
127            );
128        }
129    }
130    fn validate_agent_limits(report: &mut ValidationReport, config: &RateLimitConfig) {
131        if config.agents_per_second < 5 {
132            report.add_warning(
133                ValidationWarning::new(
134                    "rate_limits.agents_per_second",
135                    format!(
136                        "agents_per_second={} may cause agent timeouts under load.",
137                        config.agents_per_second
138                    ),
139                )
140                .with_suggestion("Consider at least 10 for stable agent operations"),
141            );
142        }
143
144        if config.contexts_per_second < 20 {
145            report.add_warning(
146                ValidationWarning::new(
147                    "rate_limits.contexts_per_second",
148                    format!(
149                        "contexts_per_second={} may slow down conversation operations.",
150                        config.contexts_per_second
151                    ),
152                )
153                .with_suggestion("Consider at least 50 for responsive context management"),
154            );
155        }
156    }
157}