Skip to main content

g3_config/
lib.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::Path;
5
6/// Main configuration structure
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Config {
9    pub providers: ProvidersConfig,
10    pub agent: AgentConfig,
11    pub computer_control: ComputerControlConfig,
12    pub webdriver: WebDriverConfig,
13}
14
15/// Provider configuration with named configs per provider type
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct ProvidersConfig {
18    /// Default provider in format "<provider_type>.<config_name>"
19    pub default_provider: String,
20    
21    /// Provider for planner mode (optional, falls back to default_provider)
22    pub planner: Option<String>,
23    
24    /// Provider for coach in autonomous mode (optional, falls back to default_provider)
25    pub coach: Option<String>,
26    
27    /// Provider for player in autonomous mode (optional, falls back to default_provider)
28    pub player: Option<String>,
29    
30    /// Named Anthropic provider configs
31    #[serde(default)]
32    pub anthropic: HashMap<String, AnthropicConfig>,
33    
34    /// Named OpenAI provider configs
35    #[serde(default)]
36    pub openai: HashMap<String, OpenAIConfig>,
37    
38    /// Named Databricks provider configs
39    #[serde(default)]
40    pub databricks: HashMap<String, DatabricksConfig>,
41    
42    /// Named embedded provider configs
43    #[serde(default)]
44    pub embedded: HashMap<String, EmbeddedConfig>,
45    
46    /// Multiple named OpenAI-compatible providers (e.g., openrouter, groq, etc.)
47    #[serde(default)]
48    pub openai_compatible: HashMap<String, OpenAIConfig>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct OpenAIConfig {
53    pub api_key: String,
54    pub model: String,
55    pub base_url: Option<String>,
56    pub max_tokens: Option<u32>,
57    pub temperature: Option<f32>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct AnthropicConfig {
62    pub api_key: String,
63    pub model: String,
64    pub max_tokens: Option<u32>,
65    pub temperature: Option<f32>,
66    pub cache_config: Option<String>,
67    pub enable_1m_context: Option<bool>,
68    pub thinking_budget_tokens: Option<u32>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct DatabricksConfig {
73    pub host: String,
74    pub token: Option<String>,
75    pub model: String,
76    pub max_tokens: Option<u32>,
77    pub temperature: Option<f32>,
78    pub use_oauth: Option<bool>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct EmbeddedConfig {
83    pub model_path: String,
84    pub model_type: String,
85    pub context_length: Option<u32>,
86    pub max_tokens: Option<u32>,
87    pub temperature: Option<f32>,
88    pub gpu_layers: Option<u32>,
89    pub threads: Option<u32>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct AgentConfig {
94    pub max_context_length: Option<u32>,
95    pub fallback_default_max_tokens: usize,
96    pub enable_streaming: bool,
97    pub timeout_seconds: u64,
98    pub auto_compact: bool,
99    pub max_retry_attempts: u32,
100    pub autonomous_max_retry_attempts: u32,
101    #[serde(default = "default_check_todo_staleness")]
102    pub check_todo_staleness: bool,
103}
104
105fn default_check_todo_staleness() -> bool {
106    true
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct ComputerControlConfig {
111    pub enabled: bool,
112    pub require_confirmation: bool,
113    pub max_actions_per_second: u32,
114}
115
116/// Browser type for WebDriver
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
118#[serde(rename_all = "lowercase")]
119pub enum WebDriverBrowser {
120    #[default]
121    Safari,
122    #[serde(rename = "chrome-headless")]
123    ChromeHeadless,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct WebDriverConfig {
128    pub enabled: bool,
129    pub safari_port: u16,
130    #[serde(default)]
131    pub chrome_port: u16,
132    #[serde(default)]
133    /// Optional path to Chrome binary (e.g., Chrome for Testing)
134    /// If not set, ChromeDriver will use the default Chrome installation
135    pub chrome_binary: Option<String>,
136    #[serde(default)]
137    pub browser: WebDriverBrowser,
138}
139
140impl Default for WebDriverConfig {
141    fn default() -> Self {
142        Self {
143            enabled: true,
144            safari_port: 4444,
145            chrome_port: 9515,
146            chrome_binary: None,
147            browser: WebDriverBrowser::Safari,
148        }
149    }
150}
151
152impl Default for ComputerControlConfig {
153    fn default() -> Self {
154        Self {
155            enabled: false,
156            require_confirmation: true,
157            max_actions_per_second: 5,
158        }
159    }
160}
161
162impl Default for Config {
163    fn default() -> Self {
164        let mut databricks_configs = HashMap::new();
165        databricks_configs.insert(
166            "default".to_string(),
167            DatabricksConfig {
168                host: "https://your-workspace.cloud.databricks.com".to_string(),
169                token: None,
170                model: "databricks-claude-sonnet-4".to_string(),
171                max_tokens: Some(4096),
172                temperature: Some(0.1),
173                use_oauth: Some(true),
174            },
175        );
176
177        Self {
178            providers: ProvidersConfig {
179                default_provider: "databricks.default".to_string(),
180                planner: None,
181                coach: None,
182                player: None,
183                anthropic: HashMap::new(),
184                openai: HashMap::new(),
185                databricks: databricks_configs,
186                embedded: HashMap::new(),
187                openai_compatible: HashMap::new(),
188            },
189            agent: AgentConfig {
190                max_context_length: None,
191                fallback_default_max_tokens: 8192,
192                enable_streaming: true,
193                timeout_seconds: 60,
194                auto_compact: true,
195                max_retry_attempts: 3,
196                autonomous_max_retry_attempts: 6,
197                check_todo_staleness: true,
198            },
199            computer_control: ComputerControlConfig::default(),
200            webdriver: WebDriverConfig::default(),
201        }
202    }
203}
204
205/// Error message for old config format
206const OLD_CONFIG_FORMAT_ERROR: &str = r#"Your configuration file uses an old format that is no longer supported.
207
208Please update your configuration to use the new provider format:
209
210```toml
211[providers]
212default_provider = "anthropic.default"  # Format: "<provider_type>.<config_name>"
213planner = "anthropic.planner"           # Optional: specific provider for planner
214coach = "anthropic.default"             # Optional: specific provider for coach  
215player = "openai.player"                # Optional: specific provider for player
216
217# Named configs per provider type
218[providers.anthropic.default]
219api_key = "your-api-key"
220model = "claude-sonnet-4-5"
221max_tokens = 64000
222
223[providers.anthropic.planner]
224api_key = "your-api-key"
225model = "claude-opus-4-5"
226thinking_budget_tokens = 16000
227
228[providers.openai.player]
229api_key = "your-api-key"
230model = "gpt-5"
231```
232
233Each mode (planner, coach, player) can specify a full path like "<provider_type>.<config_name>".
234If not specified, they fall back to `default_provider`."#;
235
236impl Config {
237    pub fn load(config_path: Option<&str>) -> Result<Self> {
238        // Check if any config file exists
239        let config_exists = if let Some(path) = config_path {
240            Path::new(path).exists()
241        } else {
242            let default_paths = ["./g3.toml", "~/.config/g3/config.toml", "~/.g3.toml"];
243            default_paths.iter().any(|path| {
244                let expanded_path = shellexpand::tilde(path);
245                Path::new(expanded_path.as_ref()).exists()
246            })
247        };
248
249        // If no config exists, create and save a default config
250        if !config_exists {
251            let default_config = Self::default();
252
253            let config_dir = dirs::home_dir()
254                .map(|mut path| {
255                    path.push(".config");
256                    path.push("g3");
257                    path
258                })
259                .unwrap_or_else(|| std::path::PathBuf::from("."));
260
261            std::fs::create_dir_all(&config_dir).ok();
262
263            let config_file = config_dir.join("config.toml");
264            if let Err(e) = default_config.save(config_file.to_str().unwrap()) {
265                eprintln!("Warning: Could not save default config: {}", e);
266            } else {
267                println!(
268                    "Created default configuration at: {}",
269                    config_file.display()
270                );
271            }
272
273            return Ok(default_config);
274        }
275
276        // Load config from file
277        let config_path_to_load = if let Some(path) = config_path {
278            Some(path.to_string())
279        } else {
280            let default_paths = ["./g3.toml", "~/.config/g3/config.toml", "~/.g3.toml"];
281            default_paths.iter().find_map(|path| {
282                let expanded_path = shellexpand::tilde(path);
283                if Path::new(expanded_path.as_ref()).exists() {
284                    Some(expanded_path.to_string())
285                } else {
286                    None
287                }
288            })
289        };
290
291        if let Some(path) = config_path_to_load {
292            // Read and parse the config file
293            let config_content = std::fs::read_to_string(&path)?;
294            
295            // Check for old format (direct provider config without named configs)
296            if Self::is_old_format(&config_content) {
297                anyhow::bail!("{}", OLD_CONFIG_FORMAT_ERROR);
298            }
299            
300            let config: Config = toml::from_str(&config_content)?;
301            
302            // Validate the default_provider format
303            config.validate_provider_reference(&config.providers.default_provider)?;
304            
305            return Ok(config);
306        }
307
308        Ok(Self::default())
309    }
310
311    /// Check if the config content uses the old format
312    fn is_old_format(content: &str) -> bool {
313        // Old format has [providers.anthropic] with api_key directly
314        // New format has [providers.anthropic.<name>] with api_key
315        
316        // Parse as TOML value to inspect structure
317        if let Ok(value) = content.parse::<toml::Value>() {
318            if let Some(providers) = value.get("providers") {
319                if let Some(providers_table) = providers.as_table() {
320                    // Check anthropic section
321                    if let Some(anthropic) = providers_table.get("anthropic") {
322                        if let Some(anthropic_table) = anthropic.as_table() {
323                            // If anthropic has api_key directly, it's old format
324                            if anthropic_table.contains_key("api_key") {
325                                return true;
326                            }
327                        }
328                    }
329                    // Check databricks section
330                    if let Some(databricks) = providers_table.get("databricks") {
331                        if let Some(databricks_table) = databricks.as_table() {
332                            // If databricks has host directly, it's old format
333                            if databricks_table.contains_key("host") {
334                                return true;
335                            }
336                        }
337                    }
338                    // Check openai section
339                    if let Some(openai) = providers_table.get("openai") {
340                        if let Some(openai_table) = openai.as_table() {
341                            // If openai has api_key directly, it's old format
342                            if openai_table.contains_key("api_key") {
343                                return true;
344                            }
345                        }
346                    }
347                }
348            }
349        }
350        false
351    }
352
353    /// Validate a provider reference (format: "<provider_type>.<config_name>")
354    fn validate_provider_reference(&self, reference: &str) -> Result<()> {
355        let parts: Vec<&str> = reference.split('.').collect();
356        if parts.len() != 2 {
357            anyhow::bail!(
358                "Invalid provider reference '{}'. Expected format: '<provider_type>.<config_name>'",
359                reference
360            );
361        }
362
363        let (provider_type, config_name) = (parts[0], parts[1]);
364
365        match provider_type {
366            "anthropic" => {
367                if !self.providers.anthropic.contains_key(config_name) {
368                    anyhow::bail!(
369                        "Provider config 'anthropic.{}' not found. Available: {:?}",
370                        config_name,
371                        self.providers.anthropic.keys().collect::<Vec<_>>()
372                    );
373                }
374            }
375            "openai" => {
376                if !self.providers.openai.contains_key(config_name) {
377                    anyhow::bail!(
378                        "Provider config 'openai.{}' not found. Available: {:?}",
379                        config_name,
380                        self.providers.openai.keys().collect::<Vec<_>>()
381                    );
382                }
383            }
384            "databricks" => {
385                if !self.providers.databricks.contains_key(config_name) {
386                    anyhow::bail!(
387                        "Provider config 'databricks.{}' not found. Available: {:?}",
388                        config_name,
389                        self.providers.databricks.keys().collect::<Vec<_>>()
390                    );
391                }
392            }
393            "embedded" => {
394                if !self.providers.embedded.contains_key(config_name) {
395                    anyhow::bail!(
396                        "Provider config 'embedded.{}' not found. Available: {:?}",
397                        config_name,
398                        self.providers.embedded.keys().collect::<Vec<_>>()
399                    );
400                }
401            }
402            _ => {
403                // Check openai_compatible providers
404                if !self.providers.openai_compatible.contains_key(provider_type) {
405                    anyhow::bail!(
406                        "Unknown provider type '{}'. Valid types: anthropic, openai, databricks, embedded, or openai_compatible names",
407                        provider_type
408                    );
409                }
410            }
411        }
412
413        Ok(())
414    }
415
416    /// Parse a provider reference into (provider_type, config_name)
417    pub fn parse_provider_reference(reference: &str) -> Result<(String, String)> {
418        let parts: Vec<&str> = reference.split('.').collect();
419        if parts.len() != 2 {
420            anyhow::bail!(
421                "Invalid provider reference '{}'. Expected format: '<provider_type>.<config_name>'",
422                reference
423            );
424        }
425        Ok((parts[0].to_string(), parts[1].to_string()))
426    }
427
428    pub fn save(&self, path: &str) -> Result<()> {
429        let toml_string = toml::to_string_pretty(self)?;
430        std::fs::write(path, toml_string)?;
431        Ok(())
432    }
433
434    pub fn load_with_overrides(
435        config_path: Option<&str>,
436        provider_override: Option<String>,
437        model_override: Option<String>,
438    ) -> Result<Self> {
439        let mut config = Self::load(config_path)?;
440
441        // Apply provider override
442        if let Some(provider) = provider_override {
443            // Validate the override
444            config.validate_provider_reference(&provider)?;
445            config.providers.default_provider = provider;
446        }
447
448        // Apply model override to the active provider
449        if let Some(model) = model_override {
450            let (provider_type, config_name) = Self::parse_provider_reference(
451                &config.providers.default_provider
452            )?;
453
454            match provider_type.as_str() {
455                "anthropic" => {
456                    if let Some(ref mut anthropic_config) = config.providers.anthropic.get_mut(&config_name) {
457                        anthropic_config.model = model;
458                    } else {
459                        return Err(anyhow::anyhow!(
460                            "Provider config 'anthropic.{}' not found.",
461                            config_name
462                        ));
463                    }
464                }
465                "databricks" => {
466                    if let Some(ref mut databricks_config) = config.providers.databricks.get_mut(&config_name) {
467                        databricks_config.model = model;
468                    } else {
469                        return Err(anyhow::anyhow!(
470                            "Provider config 'databricks.{}' not found.",
471                            config_name
472                        ));
473                    }
474                }
475                "embedded" => {
476                    if let Some(ref mut embedded_config) = config.providers.embedded.get_mut(&config_name) {
477                        embedded_config.model_path = model;
478                    } else {
479                        return Err(anyhow::anyhow!(
480                            "Provider config 'embedded.{}' not found.",
481                            config_name
482                        ));
483                    }
484                }
485                "openai" => {
486                    if let Some(ref mut openai_config) = config.providers.openai.get_mut(&config_name) {
487                        openai_config.model = model;
488                    } else {
489                        return Err(anyhow::anyhow!(
490                            "Provider config 'openai.{}' not found.",
491                            config_name
492                        ));
493                    }
494                }
495                _ => {
496                    // Check openai_compatible
497                    if let Some(ref mut compat_config) = config.providers.openai_compatible.get_mut(&provider_type) {
498                        compat_config.model = model;
499                    } else {
500                        return Err(anyhow::anyhow!(
501                            "Unknown provider type: {}",
502                            provider_type
503                        ));
504                    }
505                }
506            }
507        }
508
509        Ok(config)
510    }
511
512    /// Get the provider reference for planner mode
513    pub fn get_planner_provider(&self) -> &str {
514        self.providers
515            .planner
516            .as_deref()
517            .unwrap_or(&self.providers.default_provider)
518    }
519
520    /// Get the provider reference for coach mode in autonomous execution
521    pub fn get_coach_provider(&self) -> &str {
522        self.providers
523            .coach
524            .as_deref()
525            .unwrap_or(&self.providers.default_provider)
526    }
527
528    /// Get the provider reference for player mode in autonomous execution
529    pub fn get_player_provider(&self) -> &str {
530        self.providers
531            .player
532            .as_deref()
533            .unwrap_or(&self.providers.default_provider)
534    }
535
536    /// Create a copy of the config with a different default provider
537    pub fn with_provider_override(&self, provider_ref: &str) -> Result<Self> {
538        // Validate that the provider is configured
539        self.validate_provider_reference(provider_ref)?;
540
541        let mut config = self.clone();
542        config.providers.default_provider = provider_ref.to_string();
543        Ok(config)
544    }
545
546    /// Create a copy of the config for planner mode
547    pub fn for_planner(&self) -> Result<Self> {
548        self.with_provider_override(self.get_planner_provider())
549    }
550
551    /// Create a copy of the config for coach mode in autonomous execution
552    pub fn for_coach(&self) -> Result<Self> {
553        self.with_provider_override(self.get_coach_provider())
554    }
555
556    /// Create a copy of the config for player mode in autonomous execution
557    pub fn for_player(&self) -> Result<Self> {
558        self.with_provider_override(self.get_player_provider())
559    }
560
561    /// Get Anthropic config by name
562    pub fn get_anthropic_config(&self, name: &str) -> Option<&AnthropicConfig> {
563        self.providers.anthropic.get(name)
564    }
565
566    /// Get OpenAI config by name
567    pub fn get_openai_config(&self, name: &str) -> Option<&OpenAIConfig> {
568        self.providers.openai.get(name)
569    }
570
571    /// Get Databricks config by name
572    pub fn get_databricks_config(&self, name: &str) -> Option<&DatabricksConfig> {
573        self.providers.databricks.get(name)
574    }
575
576    /// Get Embedded config by name
577    pub fn get_embedded_config(&self, name: &str) -> Option<&EmbeddedConfig> {
578        self.providers.embedded.get(name)
579    }
580
581    /// Get the current default provider's config
582    pub fn get_default_provider_config(&self) -> Result<ProviderConfigRef<'_>> {
583        let (provider_type, config_name) = Self::parse_provider_reference(
584            &self.providers.default_provider
585        )?;
586
587        match provider_type.as_str() {
588            "anthropic" => {
589                self.providers.anthropic.get(&config_name)
590                    .map(ProviderConfigRef::Anthropic)
591                    .ok_or_else(|| anyhow::anyhow!("Anthropic config '{}' not found", config_name))
592            }
593            "openai" => {
594                self.providers.openai.get(&config_name)
595                    .map(ProviderConfigRef::OpenAI)
596                    .ok_or_else(|| anyhow::anyhow!("OpenAI config '{}' not found", config_name))
597            }
598            "databricks" => {
599                self.providers.databricks.get(&config_name)
600                    .map(ProviderConfigRef::Databricks)
601                    .ok_or_else(|| anyhow::anyhow!("Databricks config '{}' not found", config_name))
602            }
603            "embedded" => {
604                self.providers.embedded.get(&config_name)
605                    .map(ProviderConfigRef::Embedded)
606                    .ok_or_else(|| anyhow::anyhow!("Embedded config '{}' not found", config_name))
607            }
608            _ => {
609                self.providers.openai_compatible.get(&provider_type)
610                    .map(ProviderConfigRef::OpenAICompatible)
611                    .ok_or_else(|| anyhow::anyhow!("OpenAI compatible config '{}' not found", provider_type))
612            }
613        }
614    }
615}
616
617/// Reference to a provider configuration
618#[derive(Debug)]
619pub enum ProviderConfigRef<'a> {
620    Anthropic(&'a AnthropicConfig),
621    OpenAI(&'a OpenAIConfig),
622    Databricks(&'a DatabricksConfig),
623    Embedded(&'a EmbeddedConfig),
624    OpenAICompatible(&'a OpenAIConfig),
625}
626
627#[cfg(test)]
628mod tests;