Skip to main content

systemprompt_models/validators/
ai.rs

1//! AI configuration validator.
2
3use super::ValidationConfigProvider;
4use crate::ServicesConfig;
5use systemprompt_traits::validation_report::{
6    ValidationError, ValidationReport, ValidationWarning,
7};
8use systemprompt_traits::{ConfigProvider, DomainConfig, DomainConfigError};
9
10#[derive(Debug, Default)]
11pub struct AiConfigValidator {
12    config: Option<ServicesConfig>,
13}
14
15impl AiConfigValidator {
16    pub fn new() -> Self {
17        Self::default()
18    }
19}
20
21impl DomainConfig for AiConfigValidator {
22    fn domain_id(&self) -> &'static str {
23        "ai"
24    }
25
26    fn priority(&self) -> u32 {
27        50
28    }
29
30    fn dependencies(&self) -> &[&'static str] {
31        &["mcp"]
32    }
33
34    fn load(&mut self, config: &dyn ConfigProvider) -> Result<(), DomainConfigError> {
35        let provider = config
36            .as_any()
37            .downcast_ref::<ValidationConfigProvider>()
38            .ok_or_else(|| {
39                DomainConfigError::LoadError(
40                    "Expected ValidationConfigProvider with merged ServicesConfig".into(),
41                )
42            })?;
43
44        self.config = Some(provider.services_config().clone());
45        Ok(())
46    }
47
48    fn validate(&self) -> Result<ValidationReport, DomainConfigError> {
49        let mut report = ValidationReport::new("ai");
50        let config = self
51            .config
52            .as_ref()
53            .ok_or_else(|| DomainConfigError::ValidationError("Not loaded".into()))?;
54        let ai_config = &config.ai;
55
56        Self::validate_default_provider(&mut report, ai_config);
57        Self::validate_enabled_providers(&mut report, ai_config);
58        Self::validate_mcp_config(&mut report, ai_config);
59
60        if ai_config.history.retention_days == 0 {
61            report.add_warning(ValidationWarning::new(
62                "ai.history.retention_days",
63                "History retention set to 0 days, history will not be retained",
64            ));
65        }
66
67        Ok(report)
68    }
69}
70
71impl AiConfigValidator {
72    fn validate_default_provider(report: &mut ValidationReport, ai_config: &crate::AiConfig) {
73        if ai_config.default_provider.is_empty() {
74            report.add_error(ValidationError::new(
75                "ai.default_provider",
76                "Default AI provider not configured",
77            ));
78        } else if !ai_config
79            .providers
80            .contains_key(&ai_config.default_provider)
81        {
82            report.add_error(
83                ValidationError::new(
84                    "ai.default_provider",
85                    format!(
86                        "Default provider '{}' not found in providers",
87                        ai_config.default_provider
88                    ),
89                )
90                .with_suggestion("Add the provider to ai.providers or change default_provider"),
91            );
92        }
93    }
94
95    fn validate_enabled_providers(report: &mut ValidationReport, ai_config: &crate::AiConfig) {
96        let enabled: Vec<_> = ai_config
97            .providers
98            .iter()
99            .filter(|(_, c)| c.enabled)
100            .collect();
101
102        if enabled.is_empty() {
103            report.add_error(
104                ValidationError::new("ai.providers", "No AI providers are enabled")
105                    .with_suggestion("Enable at least one provider in ai.providers"),
106            );
107        }
108
109        for (name, cfg) in &enabled {
110            if cfg.api_key.is_empty() {
111                report.add_warning(
112                    ValidationWarning::new(
113                        format!("ai.providers.{}", name),
114                        format!("Provider '{}' is enabled but has no API key", name),
115                    )
116                    .with_suggestion(format!(
117                        "Set {}_API_KEY environment variable",
118                        name.to_uppercase()
119                    )),
120                );
121            }
122            if cfg.default_model.is_empty() {
123                report.add_error(ValidationError::new(
124                    format!("ai.providers.{}.default_model", name),
125                    format!("Provider '{}' has no default model specified", name),
126                ));
127            }
128
129            Self::validate_provider_models(report, name, cfg);
130        }
131    }
132
133    fn validate_provider_models(
134        report: &mut ValidationReport,
135        provider_name: &str,
136        cfg: &crate::AiProviderConfig,
137    ) {
138        if cfg.models.is_empty() {
139            return;
140        }
141
142        if !cfg.default_model.is_empty() && !cfg.models.contains_key(&cfg.default_model) {
143            report.add_warning(
144                ValidationWarning::new(
145                    format!("ai.providers.{}.default_model", provider_name),
146                    format!(
147                        "Default model '{}' not defined in models for provider '{}'",
148                        cfg.default_model, provider_name
149                    ),
150                )
151                .with_suggestion(
152                    "Add the model to ai.providers.{provider}.models or change default_model",
153                ),
154            );
155        }
156
157        for (model_name, model_def) in &cfg.models {
158            let path = format!("ai.providers.{}.models.{}", provider_name, model_name);
159
160            if model_def.limits.context_window == 0 {
161                report.add_warning(ValidationWarning::new(
162                    format!("{}.limits.context_window", path),
163                    format!("Model '{}' has no context_window defined", model_name),
164                ));
165            }
166
167            if model_def.limits.max_output_tokens == 0 {
168                report.add_warning(ValidationWarning::new(
169                    format!("{}.limits.max_output_tokens", path),
170                    format!("Model '{}' has no max_output_tokens defined", model_name),
171                ));
172            }
173        }
174    }
175
176    fn validate_mcp_config(report: &mut ValidationReport, ai_config: &crate::AiConfig) {
177        if ai_config.mcp.connect_timeout_ms == 0 {
178            report.add_error(ValidationError::new(
179                "ai.mcp.connect_timeout_ms",
180                "MCP connect timeout must be greater than 0",
181            ));
182        }
183        if ai_config.mcp.execution_timeout_ms == 0 {
184            report.add_error(ValidationError::new(
185                "ai.mcp.execution_timeout_ms",
186                "MCP execution timeout must be greater than 0",
187            ));
188        }
189    }
190}