Skip to main content

heartbit_core/config/
provider.rs

1use serde::Deserialize;
2use std::time::Duration;
3
4/// LLM provider configuration.
5///
6/// When running as a cloud-delegated runtime (daemon mode with no agents),
7/// the provider section can be omitted — per-request provider keys are used instead.
8#[derive(Debug, Default, Deserialize)]
9pub struct ProviderConfig {
10    /// Provider name (`anthropic`, `openrouter`, `mock`, …).
11    #[serde(default)]
12    pub name: String,
13    /// Model identifier (provider-specific).
14    #[serde(default)]
15    pub model: String,
16    /// Custom API endpoint URL (overrides the default for the provider).
17    /// Useful for self-hosted models, Azure, or proxies.
18    #[serde(default)]
19    pub base_url: Option<String>,
20    /// Direct API key (alternative to environment variable).
21    /// Prefer env vars in production; this is for testing/local dev.
22    #[serde(default)]
23    pub api_key: Option<String>,
24    /// Retry configuration for transient LLM API failures.
25    pub retry: Option<RetryProviderConfig>,
26    /// Enable Anthropic prompt caching (system prompt + tool definitions).
27    /// Only effective for the `anthropic` provider. Defaults to `false`.
28    #[serde(default)]
29    pub prompt_caching: bool,
30    /// Model cascading configuration. When enabled, tries cheaper models first
31    /// and escalates to the main model only when the confidence gate rejects.
32    pub cascade: Option<CascadeConfig>,
33    /// Circuit breaker configuration for this provider.
34    /// When absent, sensible defaults are used (5 failures → 30 s open, max 300 s).
35    #[serde(default)]
36    pub circuit: ProviderCircuitConfig,
37}
38
39/// Model cascading configuration for cost-efficient LLM selection.
40///
41/// When enabled, the provider tries cheaper model tiers first and only
42/// escalates to the main (most expensive) model when the confidence gate
43/// rejects the cheaper response or the tier errors.
44#[derive(Debug, Clone, Deserialize)]
45pub struct CascadeConfig {
46    /// Enable model cascading. Default: false.
47    #[serde(default)]
48    pub enabled: bool,
49    /// Model tiers from cheapest to most expensive.
50    /// The main `[provider].model` is always the implicit final tier.
51    #[serde(default)]
52    pub tiers: Vec<CascadeTierConfig>,
53    /// Confidence gate configuration. Default: heuristic with sensible defaults.
54    #[serde(default)]
55    pub gate: CascadeGateConfig,
56}
57
58/// A single tier in the model cascade.
59#[derive(Debug, Clone, Deserialize)]
60pub struct CascadeTierConfig {
61    /// Model identifier for this cascade tier.
62    pub model: String,
63}
64
65/// Confidence gate configuration for model cascading.
66#[derive(Debug, Clone, Deserialize)]
67#[serde(tag = "type", rename_all = "snake_case")]
68pub enum CascadeGateConfig {
69    /// Heuristic gate: zero-cost checks on response length, refusal patterns, etc.
70    Heuristic {
71        /// Minimum output tokens for acceptance (default: 5).
72        #[serde(default = "default_min_output_tokens")]
73        min_output_tokens: u32,
74        /// Accept responses that include tool calls (default: true).
75        #[serde(default = "super::default_true")]
76        accept_tool_calls: bool,
77        /// Escalate on MaxTokens stop reason (default: true).
78        #[serde(default = "super::default_true")]
79        escalate_on_max_tokens: bool,
80    },
81}
82
83impl Default for CascadeGateConfig {
84    fn default() -> Self {
85        Self::Heuristic {
86            min_output_tokens: default_min_output_tokens(),
87            accept_tool_calls: true,
88            escalate_on_max_tokens: true,
89        }
90    }
91}
92
93fn default_min_output_tokens() -> u32 {
94    5
95}
96
97/// Circuit breaker configuration for the LLM provider.
98///
99/// Controls how quickly the circuit opens on consecutive failures and how long
100/// it stays open before allowing a probe request through. All fields are optional;
101/// absent fields fall back to [`crate::llm::circuit::CircuitConfig`] defaults.
102#[derive(Debug, Clone, Default, serde::Serialize, Deserialize)]
103#[serde(deny_unknown_fields)]
104pub struct ProviderCircuitConfig {
105    /// Number of consecutive failures before the circuit opens. Must be > 0.
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub failure_threshold: Option<u32>,
108    /// Initial duration in seconds the circuit stays open after tripping. Must be > 0.
109    #[serde(default, skip_serializing_if = "Option::is_none")]
110    pub initial_open_duration_seconds: Option<u32>,
111    /// Maximum backoff duration in seconds before a half-open probe. Must be > 0.
112    #[serde(default, skip_serializing_if = "Option::is_none")]
113    pub max_open_duration_seconds: Option<u32>,
114    /// Backoff multiplier applied after each re-trip (exponential backoff).
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub backoff_multiplier: Option<f64>,
117}
118
119impl From<&ProviderCircuitConfig> for crate::llm::circuit::CircuitConfig {
120    fn from(c: &ProviderCircuitConfig) -> Self {
121        let default = crate::llm::circuit::CircuitConfig::default();
122        Self {
123            failure_threshold: c.failure_threshold.unwrap_or(default.failure_threshold),
124            initial_open_duration: c
125                .initial_open_duration_seconds
126                .map(|s| std::time::Duration::from_secs(u64::from(s)))
127                .unwrap_or(default.initial_open_duration),
128            max_open_duration: c
129                .max_open_duration_seconds
130                .map(|s| std::time::Duration::from_secs(u64::from(s)))
131                .unwrap_or(default.max_open_duration),
132            backoff_multiplier: c.backoff_multiplier.unwrap_or(default.backoff_multiplier),
133        }
134    }
135}
136
137/// Retry configuration for transient LLM API failures (429, 500, 502, 503, 529).
138#[derive(Debug, Deserialize)]
139pub struct RetryProviderConfig {
140    /// Maximum retry attempts (default: 3).
141    #[serde(default = "default_max_retries")]
142    pub max_retries: u32,
143    /// Base delay in milliseconds for exponential backoff (default: 500).
144    #[serde(default = "default_base_delay_ms")]
145    pub base_delay_ms: u64,
146    /// Maximum delay cap in milliseconds (default: 30000).
147    #[serde(default = "default_max_delay_ms")]
148    pub max_delay_ms: u64,
149}
150
151fn default_max_retries() -> u32 {
152    3
153}
154
155fn default_base_delay_ms() -> u64 {
156    500
157}
158
159fn default_max_delay_ms() -> u64 {
160    30_000
161}
162
163impl From<&RetryProviderConfig> for crate::llm::retry::RetryConfig {
164    fn from(r: &RetryProviderConfig) -> Self {
165        Self {
166            max_retries: r.max_retries,
167            base_delay: Duration::from_millis(r.base_delay_ms),
168            max_delay: Duration::from_millis(r.max_delay_ms),
169        }
170    }
171}