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