systemprompt_models/validators/
ai.rs1use 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}