Skip to main content

codei_config/
model.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6/// Full configuration after merging all sources.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Config {
9    #[serde(default)]
10    pub defaults: DefaultsConfig,
11
12    #[serde(default)]
13    pub providers: HashMap<String, ProviderConfig>,
14
15    #[serde(default)]
16    pub agent: AgentConfig,
17
18    #[serde(default)]
19    pub tools: ToolsConfig,
20
21    #[serde(default)]
22    pub ui: UiConfig,
23
24    #[serde(default)]
25    pub session: SessionConfig,
26}
27
28/// Resolved configuration with metadata about where values came from.
29#[derive(Debug, Clone)]
30pub struct ResolvedConfig {
31    pub config: Config,
32    pub cwd: PathBuf,
33    pub project_root: Option<PathBuf>,
34    pub user_config_path: PathBuf,
35    pub project_config_path: Option<PathBuf>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct DefaultsConfig {
40    pub model: String,
41    pub provider: String,
42    pub temperature: f32,
43    pub max_tokens: u32,
44    pub language: String,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ProviderConfig {
49    /// Direct API key; takes precedence over `api_key_env` when set.
50    #[serde(default)]
51    pub api_key: Option<String>,
52    #[serde(default)]
53    pub api_key_env: Option<String>,
54    #[serde(default)]
55    pub base_url: Option<String>,
56    #[serde(default)]
57    pub api_style: Option<String>,
58    /// `tools` (OpenAI tools API) or `functions` (legacy function calling). Default: `functions`.
59    #[serde(default)]
60    pub tool_format: Option<String>,
61}
62
63impl ProviderConfig {
64    /// Resolve API key: `api_key` first, then the environment variable named by `api_key_env`.
65    pub fn resolve_api_key(&self) -> Result<String, crate::ConfigError> {
66        if let Some(key) = self
67            .api_key
68            .as_deref()
69            .map(str::trim)
70            .filter(|k| !k.is_empty())
71        {
72            return Ok(key.to_string());
73        }
74        let env_name = self
75            .api_key_env
76            .as_deref()
77            .filter(|name| !name.is_empty())
78            .unwrap_or("OPENAI_API_KEY");
79        std::env::var(env_name).map_err(|_| crate::ConfigError::MissingApiKey {
80            env: env_name.to_string(),
81        })
82    }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct AgentConfig {
87    pub max_turns: u32,
88    pub max_tool_rounds_per_turn: u32,
89    pub context_window_tokens: u32,
90    pub compaction_threshold: f32,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ToolsConfig {
95    pub shell: ShellToolConfig,
96    pub write: EnabledToolConfig,
97    pub web_search: EnabledToolConfig,
98    #[serde(default)]
99    pub grep: GrepToolConfig,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct GrepToolConfig {
104    pub max_matches: usize,
105    pub max_files: usize,
106}
107
108#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
109#[serde(rename_all = "lowercase")]
110pub enum ShellSandboxMode {
111    #[default]
112    Off,
113    Restricted,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct ShellToolConfig {
118    pub enabled: bool,
119    pub timeout_secs: u64,
120    #[serde(default)]
121    pub sandbox: ShellSandboxMode,
122    #[serde(default)]
123    pub allowlist: Vec<String>,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct EnabledToolConfig {
128    pub enabled: bool,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct UiConfig {
133    pub theme: UiTheme,
134    pub show_tool_output: bool,
135    pub confirm_destructive: bool,
136}
137
138#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
139#[serde(rename_all = "lowercase")]
140pub enum UiTheme {
141    #[default]
142    Auto,
143    Dark,
144    Light,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct SessionConfig {
149    pub storage: SessionStorage,
150    pub dir: String,
151}
152
153#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
154#[serde(rename_all = "lowercase")]
155pub enum SessionStorage {
156    #[default]
157    Sqlite,
158    Json,
159}
160
161impl Default for Config {
162    fn default() -> Self {
163        Self {
164            defaults: DefaultsConfig::default(),
165            providers: default_providers(),
166            agent: AgentConfig::default(),
167            tools: ToolsConfig::default(),
168            ui: UiConfig::default(),
169            session: SessionConfig::default(),
170        }
171    }
172}
173
174impl Default for DefaultsConfig {
175    fn default() -> Self {
176        Self {
177            model: "gpt-4o".to_string(),
178            provider: "openai".to_string(),
179            temperature: 0.2,
180            max_tokens: 8192,
181            language: "zh-CN".to_string(),
182        }
183    }
184}
185
186impl Default for AgentConfig {
187    fn default() -> Self {
188        Self {
189            max_turns: 50,
190            max_tool_rounds_per_turn: 25,
191            context_window_tokens: 128_000,
192            compaction_threshold: 0.85,
193        }
194    }
195}
196
197impl Default for ToolsConfig {
198    fn default() -> Self {
199        Self {
200            shell: ShellToolConfig {
201                enabled: true,
202                timeout_secs: 120,
203                sandbox: ShellSandboxMode::Off,
204                allowlist: Vec::new(),
205            },
206            write: EnabledToolConfig { enabled: true },
207            web_search: EnabledToolConfig { enabled: false },
208            grep: GrepToolConfig::default(),
209        }
210    }
211}
212
213impl Default for GrepToolConfig {
214    fn default() -> Self {
215        Self {
216            max_matches: 200,
217            max_files: 5_000,
218        }
219    }
220}
221
222impl Default for UiConfig {
223    fn default() -> Self {
224        Self {
225            theme: UiTheme::Auto,
226            show_tool_output: true,
227            confirm_destructive: true,
228        }
229    }
230}
231
232impl Default for SessionConfig {
233    fn default() -> Self {
234        Self {
235            storage: SessionStorage::Sqlite,
236            dir: "~/.local/share/codei/sessions".to_string(),
237        }
238    }
239}
240
241fn default_providers() -> HashMap<String, ProviderConfig> {
242    HashMap::from([
243        (
244            "openai".to_string(),
245            ProviderConfig {
246                api_key: None,
247                api_key_env: Some("OPENAI_API_KEY".to_string()),
248                base_url: Some("https://api.openai.com/v1".to_string()),
249                api_style: Some("openai".to_string()),
250                tool_format: Some("tools".to_string()),
251            },
252        ),
253        (
254            "anthropic".to_string(),
255            ProviderConfig {
256                api_key: None,
257                api_key_env: Some("ANTHROPIC_API_KEY".to_string()),
258                base_url: None,
259                api_style: Some("anthropic".to_string()),
260                tool_format: None,
261            },
262        ),
263        (
264            "custom".to_string(),
265            ProviderConfig {
266                api_key: None,
267                api_key_env: Some("CUSTOM_API_KEY".to_string()),
268                base_url: Some("http://localhost:8080/v1".to_string()),
269                api_style: Some("openai".to_string()),
270                tool_format: Some("tools".to_string()),
271            },
272        ),
273    ])
274}
275
276impl ResolvedConfig {
277    pub fn validate(&self) -> Result<(), crate::ConfigError> {
278        let lang = &self.config.defaults.language;
279        if lang != "zh-CN" && lang != "en-US" {
280            return Err(crate::ConfigError::InvalidLanguage {
281                language: lang.clone(),
282            });
283        }
284        Ok(())
285    }
286}
287
288#[cfg(test)]
289mod provider_tests {
290    use super::ProviderConfig;
291
292    #[test]
293    fn resolve_api_key_prefers_direct_config() {
294        let cfg = ProviderConfig {
295            api_key: Some("sk-direct".into()),
296            api_key_env: Some("OPENAI_API_KEY".into()),
297            base_url: None,
298            api_style: None,
299            tool_format: None,
300        };
301        assert_eq!(cfg.resolve_api_key().unwrap(), "sk-direct");
302    }
303
304    #[test]
305    fn blank_api_key_falls_back_to_env() {
306        let cfg = ProviderConfig {
307            api_key: Some("   ".into()),
308            api_key_env: Some("NONEXISTENT_CODEI_API_KEY".into()),
309            base_url: None,
310            api_style: None,
311            tool_format: None,
312        };
313        let err = cfg.resolve_api_key().unwrap_err();
314        assert!(matches!(
315            err,
316            crate::ConfigError::MissingApiKey {
317                env: ref name
318            } if name == "NONEXISTENT_CODEI_API_KEY"
319        ));
320    }
321}