systemprompt_models/validators/
ai.rs1use super::ValidationConfigProvider;
7use crate::ServicesConfig;
8use systemprompt_traits::validation_report::{
9 ValidationError, ValidationReport, ValidationWarning,
10};
11use systemprompt_traits::{ConfigProvider, DomainConfig, DomainConfigError};
12
13#[derive(Debug, Default)]
14pub struct AiConfigValidator {
15 config: Option<ServicesConfig>,
16}
17
18impl AiConfigValidator {
19 pub fn new() -> Self {
20 Self::default()
21 }
22}
23
24impl DomainConfig for AiConfigValidator {
25 fn domain_id(&self) -> &'static str {
26 "ai"
27 }
28
29 fn priority(&self) -> u32 {
30 50
31 }
32
33 fn dependencies(&self) -> &[&'static str] {
34 &["mcp"]
35 }
36
37 fn load(&mut self, config: &dyn ConfigProvider) -> Result<(), DomainConfigError> {
38 let provider = config
39 .as_any()
40 .downcast_ref::<ValidationConfigProvider>()
41 .ok_or_else(|| DomainConfigError::LoadError {
42 message: "Expected ValidationConfigProvider with merged ServicesConfig".into(),
43 })?;
44
45 self.config = Some(provider.services_config().clone());
46 Ok(())
47 }
48
49 fn validate(&self) -> Result<ValidationReport, DomainConfigError> {
50 let mut report = ValidationReport::new("ai");
51 let config = self
52 .config
53 .as_ref()
54 .ok_or_else(|| DomainConfigError::ValidationError {
55 message: "Not loaded".into(),
56 })?;
57 let ai_config = &config.ai;
58
59 Self::validate_default_provider(&mut report, ai_config);
60 Self::validate_enabled_providers(&mut report, ai_config);
61 Self::validate_mcp_config(&mut report, ai_config);
62
63 if ai_config.history.retention_days == 0 {
64 report.add_warning(ValidationWarning::new(
65 "ai.history.retention_days",
66 "History retention set to 0 days, history will not be retained",
67 ));
68 }
69
70 Ok(report)
71 }
72}
73
74impl AiConfigValidator {
75 fn validate_default_provider(report: &mut ValidationReport, ai_config: &crate::AiConfig) {
76 if ai_config.default_provider.is_empty() {
77 report.add_error(ValidationError::new(
78 "ai.default_provider",
79 "Default AI provider not configured",
80 ));
81 } else if !ai_config
82 .providers
83 .contains_key(&ai_config.default_provider)
84 {
85 report.add_error(
86 ValidationError::new(
87 "ai.default_provider",
88 format!(
89 "Default provider '{}' not found in providers",
90 ai_config.default_provider
91 ),
92 )
93 .with_suggestion("Add the provider to ai.providers or change default_provider"),
94 );
95 }
96 }
97
98 fn validate_enabled_providers(report: &mut ValidationReport, ai_config: &crate::AiConfig) {
99 let enabled: Vec<_> = ai_config
100 .providers
101 .iter()
102 .filter(|(_, c)| c.enabled)
103 .collect();
104
105 if enabled.is_empty() {
106 report.add_error(
107 ValidationError::new("ai.providers", "No AI providers are enabled")
108 .with_suggestion("Enable at least one provider in ai.providers"),
109 );
110 }
111
112 for (name, cfg) in &enabled {
117 if cfg.default_model.is_empty() {
118 report.add_warning(ValidationWarning::new(
119 format!("ai.providers.{}.default_model", name),
120 format!(
121 "Provider '{}' has no default_model override; the provider client default \
122 will be used",
123 name
124 ),
125 ));
126 }
127 }
128 }
129
130 fn validate_mcp_config(report: &mut ValidationReport, ai_config: &crate::AiConfig) {
131 if ai_config.mcp.resilience.connect_timeout_ms == 0 {
132 report.add_error(ValidationError::new(
133 "ai.mcp.resilience.connect_timeout_ms",
134 "MCP connect timeout must be greater than 0",
135 ));
136 }
137 if ai_config.mcp.resilience.request_timeout_ms == 0 {
138 report.add_error(ValidationError::new(
139 "ai.mcp.resilience.request_timeout_ms",
140 "MCP execution timeout must be greater than 0",
141 ));
142 }
143 }
144}