Skip to main content

systemprompt_models/services/ai/
config.rs

1//! AI service configuration loaded from profile YAML.
2//!
3//! [`AiConfig`] is the top-level AI *policy* block: the default provider, the
4//! per-provider policy map (keyed by registry provider name), sampling, MCP
5//! discovery, and history retention. Upstream connectivity lives in the profile
6//! `providers` registry, not here. The nested [`ResilienceSettings`] is the
7//! per-dependency timeout/retry/circuit-breaker policy applied to outbound
8//! provider and MCP calls.
9
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13use super::model::AiProviderConfig;
14
15#[derive(Debug, Clone, Default, Serialize, Deserialize)]
16pub struct AiConfig {
17    #[serde(default)]
18    pub default_provider: String,
19
20    #[serde(default)]
21    pub default_max_output_tokens: Option<u32>,
22
23    #[serde(default)]
24    pub sampling: SamplingConfig,
25
26    #[serde(default)]
27    pub providers: HashMap<String, AiProviderConfig>,
28
29    #[serde(default)]
30    pub mcp: McpConfig,
31
32    #[serde(default)]
33    pub history: HistoryConfig,
34}
35
36#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
37pub struct SamplingConfig {
38    #[serde(default)]
39    pub enable_smart_routing: bool,
40
41    #[serde(default)]
42    pub fallback_enabled: bool,
43}
44
45#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
46pub struct McpConfig {
47    #[serde(default)]
48    pub auto_discover: bool,
49
50    #[serde(default = "default_mcp_resilience")]
51    pub resilience: ResilienceSettings,
52}
53
54impl Default for McpConfig {
55    fn default() -> Self {
56        Self {
57            auto_discover: false,
58            resilience: default_mcp_resilience(),
59        }
60    }
61}
62
63/// MCP defaults: tool RPCs are bounded at 30s rather than the 60s AI default.
64fn default_mcp_resilience() -> ResilienceSettings {
65    ResilienceSettings {
66        request_timeout_ms: 30_000,
67        connect_timeout_ms: 5_000,
68        ..ResilienceSettings::default()
69    }
70}
71
72/// Per-dependency resilience policy: timeouts, retry, circuit breaker,
73/// bulkhead.
74///
75/// Plain serde data loaded from profile config (all values in milliseconds or
76/// counts). Translated into the runtime form consumed by the resilience
77/// primitives in `systemprompt-database`.
78#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
79pub struct ResilienceSettings {
80    /// Per-attempt (not whole-call) timeout; non-streaming only.
81    #[serde(default = "default_request_timeout")]
82    pub request_timeout_ms: u64,
83
84    #[serde(default = "default_resilience_connect_timeout")]
85    pub connect_timeout_ms: u64,
86
87    /// Max gap between two chunks before a stream is aborted.
88    #[serde(default = "default_stream_idle_timeout")]
89    pub stream_idle_timeout_ms: u64,
90
91    /// Counts the first try, so `1` disables retries.
92    #[serde(default = "default_retry_attempts")]
93    pub retry_attempts: u32,
94
95    /// Doubles each subsequent attempt.
96    #[serde(default = "default_retry_base_delay")]
97    pub retry_base_delay_ms: u64,
98
99    #[serde(default = "default_retry_max_delay")]
100    pub retry_max_delay_ms: u64,
101
102    /// Consecutive (not cumulative) failures that trip the breaker open.
103    #[serde(default = "default_breaker_threshold")]
104    pub breaker_failure_threshold: u32,
105
106    #[serde(default = "default_breaker_cooldown")]
107    pub breaker_open_cooldown_ms: u64,
108
109    #[serde(default = "default_half_open_probes")]
110    pub breaker_half_open_probes: u32,
111
112    #[serde(default = "default_max_concurrent")]
113    pub max_concurrent: usize,
114}
115
116impl Default for ResilienceSettings {
117    fn default() -> Self {
118        Self {
119            request_timeout_ms: default_request_timeout(),
120            connect_timeout_ms: default_resilience_connect_timeout(),
121            stream_idle_timeout_ms: default_stream_idle_timeout(),
122            retry_attempts: default_retry_attempts(),
123            retry_base_delay_ms: default_retry_base_delay(),
124            retry_max_delay_ms: default_retry_max_delay(),
125            breaker_failure_threshold: default_breaker_threshold(),
126            breaker_open_cooldown_ms: default_breaker_cooldown(),
127            breaker_half_open_probes: default_half_open_probes(),
128            max_concurrent: default_max_concurrent(),
129        }
130    }
131}
132
133const fn default_request_timeout() -> u64 {
134    60_000
135}
136
137const fn default_resilience_connect_timeout() -> u64 {
138    10_000
139}
140
141const fn default_stream_idle_timeout() -> u64 {
142    60_000
143}
144
145const fn default_retry_attempts() -> u32 {
146    3
147}
148
149const fn default_retry_base_delay() -> u64 {
150    200
151}
152
153const fn default_retry_max_delay() -> u64 {
154    10_000
155}
156
157const fn default_breaker_threshold() -> u32 {
158    5
159}
160
161const fn default_breaker_cooldown() -> u64 {
162    30_000
163}
164
165const fn default_half_open_probes() -> u32 {
166    1
167}
168
169const fn default_max_concurrent() -> usize {
170    16
171}
172
173#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
174pub struct HistoryConfig {
175    #[serde(default = "default_retention_days")]
176    pub retention_days: u32,
177
178    #[serde(default)]
179    pub log_tool_executions: bool,
180}
181
182impl Default for HistoryConfig {
183    fn default() -> Self {
184        Self {
185            retention_days: default_retention_days(),
186            log_tool_executions: false,
187        }
188    }
189}
190
191const fn default_retention_days() -> u32 {
192    30
193}