claude_code_acp/types/
config.rs

1//! Agent configuration from environment variables
2
3use std::collections::HashMap;
4
5/// Agent configuration loaded from environment variables and settings files
6///
7/// Configuration priority (highest to lowest):
8/// 1. Environment variables (e.g., `ANTHROPIC_MODEL`)
9/// 2. Settings files - Top-level fields (e.g., `model`)
10/// 3. Settings files - `env` object (e.g., `env.ANTHROPIC_MODEL`)
11/// 4. Defaults
12///
13/// Settings files are loaded from:
14/// - `~/.claude/settings.json` (user settings)
15/// - `<project_dir>/.claude/settings.json` (project settings)
16/// - `<project_dir>/.claude/settings.local.json` (local settings)
17///
18/// Supports configuring alternative AI model providers (e.g., domestic providers in China)
19/// through environment variables or settings files.
20#[derive(Debug, Clone, Default)]
21pub struct AgentConfig {
22    /// Anthropic API base URL
23    /// Environment variable: `ANTHROPIC_BASE_URL`
24    /// Settings field: `apiBaseUrl`
25    pub base_url: Option<String>,
26
27    /// API key for authentication
28    /// Environment variable: `ANTHROPIC_API_KEY` (preferred) or `ANTHROPIC_AUTH_TOKEN` (legacy)
29    /// Note: Not supported in settings files for security reasons
30    pub api_key: Option<String>,
31
32    /// Primary model name
33    /// Environment variable: `ANTHROPIC_MODEL`
34    /// Settings field: `model`
35    pub model: Option<String>,
36
37    /// Small/fast model name (fallback)
38    /// Environment variable: `ANTHROPIC_SMALL_FAST_MODEL`
39    /// Settings field: `smallFastModel`
40    pub small_fast_model: Option<String>,
41
42    /// Maximum tokens for thinking blocks (extended thinking mode)
43    ///
44    /// Can be set via:
45    /// - Environment variable: `MAX_THINKING_TOKENS`
46    /// - Settings field: `alwaysThinkingEnabled` (sets to default 20000)
47    /// - Settings `env` object: `env.MAX_THINKING_TOKENS`
48    ///
49    /// When `alwaysThinkingEnabled` is true in settings, this defaults to 20000.
50    /// Typical values: 4096, 8000, 16000, 20000
51    pub max_thinking_tokens: Option<u32>,
52}
53
54impl AgentConfig {
55    /// Create a new empty configuration
56    pub fn new() -> Self {
57        Self::default()
58    }
59
60    /// Load configuration from environment variables
61    ///
62    /// Reads the following environment variables:
63    /// - `ANTHROPIC_BASE_URL`: API base URL
64    /// - `ANTHROPIC_API_KEY`: API key (preferred)
65    /// - `ANTHROPIC_AUTH_TOKEN`: Auth token (legacy, fallback if API_KEY not set)
66    /// - `ANTHROPIC_MODEL`: Primary model name
67    /// - `ANTHROPIC_SMALL_FAST_MODEL`: Small/fast model name
68    /// - `MAX_THINKING_TOKENS`: Maximum tokens for thinking blocks
69    pub fn from_env() -> Self {
70        // Prefer ANTHROPIC_API_KEY, fallback to ANTHROPIC_AUTH_TOKEN for compatibility
71        let api_key = std::env::var("ANTHROPIC_API_KEY")
72            .ok()
73            .or_else(|| std::env::var("ANTHROPIC_AUTH_TOKEN").ok());
74
75        // Parse MAX_THINKING_TOKENS if present
76        let max_thinking_tokens = std::env::var("MAX_THINKING_TOKENS")
77            .ok()
78            .and_then(|s| s.parse::<u32>().ok());
79
80        Self {
81            base_url: std::env::var("ANTHROPIC_BASE_URL").ok(),
82            api_key,
83            model: std::env::var("ANTHROPIC_MODEL").ok(),
84            small_fast_model: std::env::var("ANTHROPIC_SMALL_FAST_MODEL").ok(),
85            max_thinking_tokens,
86        }
87    }
88
89    /// Load configuration from settings files and environment variables
90    ///
91    /// Configuration priority (highest to lowest):
92    /// 1. Environment variables (e.g., `ANTHROPIC_MODEL`)
93    /// 2. Settings files - Top-level fields (e.g., `model`)
94    /// 3. Settings files - `env` object (e.g., `env.ANTHROPIC_MODEL`)
95    /// 4. Defaults (including `alwaysThinkingEnabled` → default MAX_THINKING_TOKENS)
96    ///
97    /// Settings files are loaded in this order (later ones override earlier):
98    /// - `~/.claude/settings.json` (user settings)
99    /// - `<project_dir>/.claude/settings.json` (project settings)
100    /// - `<project_dir>/.claude/settings.local.json` (local settings)
101    ///
102    /// # Arguments
103    ///
104    /// * `project_dir` - The project working directory
105    ///
106    /// # Example settings.json
107    ///
108    /// Using top-level fields:
109    /// ```json
110    /// {
111    ///   "model": "claude-opus-4-20250514",
112    ///   "smallFastModel": "claude-haiku-4-20250514",
113    ///   "apiBaseUrl": "https://api.anthropic.com"
114    /// }
115    /// ```
116    ///
117    /// Using `env` object (compatible with Claude Code CLI):
118    /// ```json
119    /// {
120    ///   "env": {
121    ///     "ANTHROPIC_MODEL": "claude-opus-4-20250514",
122    ///     "ANTHROPIC_SMALL_FAST_MODEL": "claude-haiku-4-20250514",
123    ///     "ANTHROPIC_BASE_URL": "https://api.anthropic.com"
124    ///   }
125    /// }
126    /// ```
127    ///
128    /// Enabling extended thinking mode with `alwaysThinkingEnabled`:
129    /// ```json
130    /// {
131    ///   "model": "claude-sonnet-4-20250514",
132    ///   "alwaysThinkingEnabled": true
133    /// }
134    /// ```
135    /// This will set `MAX_THINKING_TOKENS` to 20000 by default.
136    /// You can still override it with the `MAX_THINKING_TOKENS` environment variable.
137    pub fn from_settings_or_env(project_dir: &std::path::Path) -> Self {
138        use crate::settings::SettingsManager;
139
140        // Load settings from files (may fail if files don't exist)
141        let settings = SettingsManager::new(project_dir)
142            .map(|m| m.settings().clone())
143            .unwrap_or_default();
144
145        // Trace settings file discovery (debug level)
146        // Check if settings.env has any configuration entries
147        let has_env_settings = settings.env.as_ref().map_or(false, |env| !env.is_empty());
148        tracing::trace!(
149            has_user_settings =
150                settings.model.is_some() || settings.api_base_url.is_some() || has_env_settings,
151            "Settings files discovered"
152        );
153
154        // Check if env vars are set before moving settings
155        let has_model_env = std::env::var("ANTHROPIC_MODEL").is_ok();
156        let has_base_url_env = std::env::var("ANTHROPIC_BASE_URL").is_ok();
157        let has_small_fast_model_env = std::env::var("ANTHROPIC_SMALL_FAST_MODEL").is_ok();
158        let has_max_thinking_tokens_env = std::env::var("MAX_THINKING_TOKENS").is_ok();
159
160        // Store settings flags before moving (both top-level and env)
161        let has_model_settings = settings.model.is_some();
162        let has_base_url_settings = settings.api_base_url.is_some();
163        let has_small_fast_model_settings = settings.small_fast_model.is_some();
164        let has_model_env_settings = settings
165            .env
166            .as_ref()
167            .and_then(|env| env.get("ANTHROPIC_MODEL"))
168            .is_some();
169        let has_base_url_env_settings = settings
170            .env
171            .as_ref()
172            .and_then(|env| env.get("ANTHROPIC_BASE_URL"))
173            .is_some();
174        let has_small_fast_model_env_settings = settings
175            .env
176            .as_ref()
177            .and_then(|env| env.get("ANTHROPIC_SMALL_FAST_MODEL"))
178            .is_some();
179        let has_max_thinking_tokens_env_settings = settings
180            .env
181            .as_ref()
182            .and_then(|env| env.get("MAX_THINKING_TOKENS"))
183            .is_some();
184
185        // Resolve configuration with priority: Env > Settings (top-level) > Settings (env) > Default
186        let base_url = std::env::var("ANTHROPIC_BASE_URL")
187            .ok()
188            .or_else(|| settings.api_base_url.map(|u| u.to_string()))
189            .or_else(|| {
190                settings
191                    .env
192                    .as_ref()
193                    .and_then(|env| env.get("ANTHROPIC_BASE_URL").cloned())
194            });
195
196        let model = std::env::var("ANTHROPIC_MODEL")
197            .ok()
198            .or_else(|| settings.model.map(|m| m.to_string()))
199            .or_else(|| {
200                settings
201                    .env
202                    .as_ref()
203                    .and_then(|env| env.get("ANTHROPIC_MODEL").cloned())
204            });
205
206        let small_fast_model = std::env::var("ANTHROPIC_SMALL_FAST_MODEL")
207            .ok()
208            .or_else(|| settings.small_fast_model.map(|m| m.to_string()))
209            .or_else(|| {
210                settings
211                    .env
212                    .as_ref()
213                    .and_then(|env| env.get("ANTHROPIC_SMALL_FAST_MODEL").cloned())
214            });
215
216        // API key is not loaded from settings for security reasons
217        // Note: ANTHROPIC_AUTH_TOKEN in settings.env is also not loaded for security
218        let api_key = std::env::var("ANTHROPIC_API_KEY")
219            .ok()
220            .or_else(|| std::env::var("ANTHROPIC_AUTH_TOKEN").ok());
221
222        // Default max thinking tokens when always_thinking_enabled is true
223        const DEFAULT_MAX_THINKING_TOKENS: u32 = 20000;
224
225        // Check if always_thinking_enabled is set in settings
226        let always_thinking_enabled = settings.always_thinking_enabled.unwrap_or(false);
227
228        let max_thinking_tokens = std::env::var("MAX_THINKING_TOKENS")
229            .ok()
230            .and_then(|s| s.parse::<u32>().ok())
231            .or_else(|| {
232                settings.env.as_ref().and_then(|env| {
233                    env.get("MAX_THINKING_TOKENS")
234                        .and_then(|s| s.parse::<u32>().ok())
235                })
236            })
237            .or_else(|| {
238                // If always_thinking_enabled is true and no MAX_THINKING_TOKENS is set,
239                // use the default value to enable extended thinking mode
240                if always_thinking_enabled {
241                    Some(DEFAULT_MAX_THINKING_TOKENS)
242                } else {
243                    None
244                }
245            });
246
247        let config = Self {
248            base_url,
249            api_key,
250            model,
251            small_fast_model,
252            max_thinking_tokens,
253        };
254
255        // Log configuration sources
256        tracing::info!(
257            model = ?config.model,
258            model_source = if has_model_env { "env" } else if has_model_settings { "settings" } else if has_model_env_settings { "settings.env" } else { "default" },
259            base_url = ?config.base_url,
260            base_url_source = if has_base_url_env { "env" } else if has_base_url_settings { "settings" } else if has_base_url_env_settings { "settings.env" } else { "default" },
261            small_fast_model = ?config.small_fast_model,
262            small_fast_model_source = if has_small_fast_model_env { "env" } else if has_small_fast_model_settings { "settings" } else if has_small_fast_model_env_settings { "settings.env" } else { "default" },
263            max_thinking_tokens = ?config.max_thinking_tokens,
264            max_thinking_tokens_source = if has_max_thinking_tokens_env { "env" } else if has_max_thinking_tokens_env_settings { "settings.env" } else if always_thinking_enabled { "alwaysThinkingEnabled" } else { "default" },
265            always_thinking_enabled = always_thinking_enabled,
266            api_key = ?config.masked_api_key(),
267            "Configuration loaded (priority: env > settings.{{top-level, env}} > default)"
268        );
269
270        config
271    }
272
273    /// Check if any configuration is set
274    pub fn is_configured(&self) -> bool {
275        self.base_url.is_some()
276            || self.api_key.is_some()
277            || self.model.is_some()
278            || self.small_fast_model.is_some()
279            || self.max_thinking_tokens.is_some()
280    }
281
282    /// Get environment variables to pass to Claude Code CLI
283    ///
284    /// Returns a HashMap of environment variable names and values
285    /// that should be passed to the subprocess.
286    pub fn to_env_vars(&self) -> HashMap<String, String> {
287        let mut env = HashMap::new();
288
289        if let Some(ref url) = self.base_url {
290            env.insert("ANTHROPIC_BASE_URL".to_string(), url.clone());
291        }
292        // Pass as ANTHROPIC_API_KEY (standard name for Claude CLI)
293        if let Some(ref key) = self.api_key {
294            env.insert("ANTHROPIC_API_KEY".to_string(), key.clone());
295        }
296        if let Some(ref model) = self.model {
297            env.insert("ANTHROPIC_MODEL".to_string(), model.clone());
298        }
299        if let Some(ref model) = self.small_fast_model {
300            env.insert("ANTHROPIC_SMALL_FAST_MODEL".to_string(), model.clone());
301        }
302
303        env
304    }
305
306    /// Get a masked version of the API key for logging
307    ///
308    /// Shows first 4 and last 4 characters with the middle masked by asterisks.
309    /// For example: `sk-ant-api03-xxx...` becomes `sk-a***xxx`
310    ///
311    /// Returns None if no API key is set.
312    pub fn masked_api_key(&self) -> Option<String> {
313        self.api_key.as_ref().map(|key| {
314            let key = key.as_str();
315            if key.is_empty() {
316                "***".to_string()
317            } else if key.len() <= 2 {
318                // Very short keys: show first character only
319                format!("{}***", &key[..1])
320            } else if key.len() <= 8 {
321                // Short keys: show first and last character
322                format!("{}***{}", &key[..1], &key[key.len() - 1..])
323            } else {
324                // Longer keys: show first 4 and last 4 characters
325                format!("{}***{}", &key[..4], &key[key.len() - 4..])
326            }
327        })
328    }
329
330    /// Apply configuration to ClaudeAgentOptions
331    ///
332    /// Sets the model and environment variables on the options.
333    pub fn apply_to_options(&self, options: &mut claude_code_agent_sdk::ClaudeAgentOptions) {
334        // Set model if configured
335        if let Some(ref model) = self.model {
336            options.model = Some(model.clone());
337        }
338
339        // Set fallback model if configured
340        if let Some(ref fallback) = self.small_fast_model {
341            options.fallback_model = Some(fallback.clone());
342        }
343
344        // Set max_thinking_tokens if configured (enables extended thinking mode)
345        if let Some(tokens) = self.max_thinking_tokens {
346            options.max_thinking_tokens = Some(tokens);
347        }
348
349        // Pass base_url and api_key as environment variables
350        let env_vars = self.to_env_vars();
351        if !env_vars.is_empty() {
352            options.env = env_vars;
353        }
354
355        // Log the applied configuration
356        tracing::debug!(
357            model = ?self.model,
358            fallback_model = ?self.small_fast_model,
359            base_url = ?self.base_url,
360            max_thinking_tokens = ?self.max_thinking_tokens,
361            api_key = ?self.masked_api_key(),
362            env_vars_count = options.env.len(),
363            "Agent configuration applied to options"
364        );
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371
372    #[test]
373    fn test_default_config() {
374        let config = AgentConfig::default();
375        assert!(config.base_url.is_none());
376        assert!(config.api_key.is_none());
377        assert!(config.model.is_none());
378        assert!(config.small_fast_model.is_none());
379        assert!(config.max_thinking_tokens.is_none());
380        assert!(!config.is_configured());
381    }
382
383    #[test]
384    fn test_to_env_vars() {
385        let config = AgentConfig {
386            base_url: Some("https://api.example.com".to_string()),
387            api_key: Some("secret-key".to_string()),
388            model: Some("claude-3".to_string()),
389            small_fast_model: None,
390            max_thinking_tokens: None,
391        };
392
393        let env = config.to_env_vars();
394        assert_eq!(
395            env.get("ANTHROPIC_BASE_URL").unwrap(),
396            "https://api.example.com"
397        );
398        assert_eq!(env.get("ANTHROPIC_API_KEY").unwrap(), "secret-key");
399        assert_eq!(env.get("ANTHROPIC_MODEL").unwrap(), "claude-3");
400        assert!(!env.contains_key("ANTHROPIC_SMALL_FAST_MODEL"));
401    }
402
403    #[test]
404    fn test_masked_api_key() {
405        // Test with no API key
406        let config = AgentConfig::default();
407        assert!(config.masked_api_key().is_none());
408
409        // Test with empty string (edge case)
410        let config = AgentConfig {
411            api_key: Some("".to_string()),
412            ..Default::default()
413        };
414        assert_eq!(config.masked_api_key().unwrap(), "***");
415
416        // Test with single character (edge case)
417        let config = AgentConfig {
418            api_key: Some("a".to_string()),
419            ..Default::default()
420        };
421        assert_eq!(config.masked_api_key().unwrap(), "a***");
422
423        // Test with two characters (edge case)
424        let config = AgentConfig {
425            api_key: Some("ab".to_string()),
426            ..Default::default()
427        };
428        assert_eq!(config.masked_api_key().unwrap(), "a***");
429
430        // Test with short API key (<= 8 characters)
431        let config = AgentConfig {
432            api_key: Some("abc123".to_string()),
433            ..Default::default()
434        };
435        assert_eq!(config.masked_api_key().unwrap(), "a***3");
436
437        // Test with long API key
438        let config = AgentConfig {
439            api_key: Some("sk-ant-api03-12345-abcd".to_string()),
440            ..Default::default()
441        };
442        assert_eq!(config.masked_api_key().unwrap(), "sk-a***abcd");
443
444        // Test with Anthropic-style key
445        let config = AgentConfig {
446            api_key: Some("sk-ant-api03-xxxx-xxxx-xxxx-xxxxxxxxxxx".to_string()),
447            ..Default::default()
448        };
449        let masked = config.masked_api_key().unwrap();
450        assert!(masked.starts_with("sk-a"));
451        assert!(masked.ends_with("xxxx"));
452        assert!(masked.contains("***"));
453    }
454
455    #[test]
456    fn test_is_configured() {
457        let mut config = AgentConfig::default();
458        assert!(!config.is_configured());
459
460        config.model = Some("test".to_string());
461        assert!(config.is_configured());
462    }
463
464    #[test]
465    fn test_max_thinking_tokens_config() {
466        let config = AgentConfig {
467            base_url: None,
468            api_key: None,
469            model: None,
470            small_fast_model: None,
471            max_thinking_tokens: Some(4096),
472        };
473
474        assert!(config.is_configured());
475        assert_eq!(config.max_thinking_tokens, Some(4096));
476    }
477
478    #[test]
479    #[serial_test::serial]
480    fn test_from_settings_or_env() {
481        // Use temp dir for test files
482        let temp_base = std::env::temp_dir();
483        let temp_dir = temp_base.join("test_config_combined");
484        let settings_dir = temp_dir.join(".claude");
485
486        // Cleanup env vars FIRST (before any test operations)
487        // This is important because tests run in parallel and env vars are process-global
488        unsafe {
489            std::env::remove_var("ANTHROPIC_MODEL");
490        }
491        unsafe {
492            std::env::remove_var("ANTHROPIC_SMALL_FAST_MODEL");
493        }
494        unsafe {
495            std::env::remove_var("ANTHROPIC_BASE_URL");
496        }
497
498        // Cleanup any existing test directory
499        drop(std::fs::remove_dir_all(&temp_dir));
500        std::fs::create_dir_all(&settings_dir).ok();
501
502        let settings_file = settings_dir.join("settings.json");
503        std::fs::write(
504            &settings_file,
505            r#"{
506            "model": "settings-model",
507            "smallFastModel": "settings-small-model",
508            "apiBaseUrl": "https://settings.api.com"
509        }"#,
510        )
511        .ok();
512
513        // Test 1: Env overrides settings
514        // Set env var for model (should override)
515        unsafe {
516            std::env::set_var("ANTHROPIC_MODEL", "env-model");
517        }
518
519        let config = AgentConfig::from_settings_or_env(&temp_dir);
520        assert_eq!(config.model, Some("env-model".to_string()));
521        assert_eq!(
522            config.small_fast_model,
523            Some("settings-small-model".to_string())
524        );
525        assert_eq!(
526            config.base_url,
527            Some("https://settings.api.com".to_string())
528        );
529
530        // Test 2: Settings only (no env)
531        // Verify env var is actually removed before asserting
532        unsafe {
533            std::env::remove_var("ANTHROPIC_MODEL");
534        }
535        assert!(
536            std::env::var("ANTHROPIC_MODEL").is_err(),
537            "ANTHROPIC_MODEL should be removed"
538        );
539
540        let config2 = AgentConfig::from_settings_or_env(&temp_dir);
541        assert_eq!(config2.model, Some("settings-model".to_string()));
542        assert_eq!(
543            config2.small_fast_model,
544            Some("settings-small-model".to_string())
545        );
546
547        // Test 3: Env only (no settings)
548        std::fs::remove_file(&settings_file).ok();
549        unsafe {
550            std::env::set_var("ANTHROPIC_MODEL", "env-only-model");
551        }
552
553        let config3 = AgentConfig::from_settings_or_env(&temp_dir);
554        assert_eq!(config3.model, Some("env-only-model".to_string()));
555        assert!(config3.small_fast_model.is_none());
556
557        // Cleanup
558        unsafe {
559            std::env::remove_var("ANTHROPIC_MODEL");
560        }
561        drop(std::fs::remove_dir_all(&temp_dir));
562    }
563
564    #[test]
565    #[serial_test::serial]
566    fn test_from_settings_env_fallback() {
567        // Test that settings.env is used as fallback when top-level fields are not set
568        let temp_base = std::env::temp_dir();
569        let temp_dir = temp_base.join("test_config_env_fallback");
570        let settings_dir = temp_dir.join(".claude");
571
572        // Cleanup env vars FIRST (before any test operations)
573        // This is important because tests run in parallel and env vars are process-global
574        unsafe {
575            std::env::remove_var("ANTHROPIC_MODEL");
576        }
577        unsafe {
578            std::env::remove_var("ANTHROPIC_SMALL_FAST_MODEL");
579        }
580        unsafe {
581            std::env::remove_var("ANTHROPIC_BASE_URL");
582        }
583
584        // Cleanup any existing test directory
585        drop(std::fs::remove_dir_all(&temp_dir));
586        std::fs::create_dir_all(&settings_dir).ok();
587
588        // Create settings with env object (compatible with Claude Code CLI format)
589        let settings_file = settings_dir.join("settings.json");
590        std::fs::write(
591            &settings_file,
592            r#"{
593            "env": {
594                "ANTHROPIC_MODEL": "env-settings-model",
595                "ANTHROPIC_SMALL_FAST_MODEL": "env-settings-small-model",
596                "ANTHROPIC_BASE_URL": "https://env-settings.api.com"
597            }
598        }"#,
599        )
600        .ok();
601
602        let config = AgentConfig::from_settings_or_env(&temp_dir);
603        assert_eq!(config.model, Some("env-settings-model".to_string()));
604        assert_eq!(
605            config.small_fast_model,
606            Some("env-settings-small-model".to_string())
607        );
608        assert_eq!(
609            config.base_url,
610            Some("https://env-settings.api.com".to_string())
611        );
612
613        // Cleanup
614        drop(std::fs::remove_dir_all(&temp_dir));
615    }
616
617    #[test]
618    #[serial_test::serial]
619    fn test_from_settings_priority_order() {
620        // Test priority: env > settings.top-level > settings.env > default
621        let temp_base = std::env::temp_dir();
622        let temp_dir = temp_base.join("test_config_priority");
623        let settings_dir = temp_dir.join(".claude");
624
625        // Cleanup env vars FIRST (before any test operations)
626        // This is important because tests run in parallel and env vars are process-global
627        unsafe {
628            std::env::remove_var("ANTHROPIC_MODEL");
629        }
630        unsafe {
631            std::env::remove_var("ANTHROPIC_SMALL_FAST_MODEL");
632        }
633        unsafe {
634            std::env::remove_var("ANTHROPIC_BASE_URL");
635        }
636
637        drop(std::fs::remove_dir_all(&temp_dir));
638        std::fs::create_dir_all(&settings_dir).ok();
639
640        // Create settings with both top-level and env fields
641        let settings_file = settings_dir.join("settings.json");
642        std::fs::write(
643            &settings_file,
644            r#"{
645            "model": "top-level-model",
646            "env": {
647                "ANTHROPIC_MODEL": "env-object-model"
648            }
649        }"#,
650        )
651        .ok();
652
653        // Test 1: Top-level should override env object
654        let config1 = AgentConfig::from_settings_or_env(&temp_dir);
655        assert_eq!(config1.model, Some("top-level-model".to_string()));
656
657        // Test 2: Env var should override both
658        unsafe {
659            std::env::set_var("ANTHROPIC_MODEL", "env-var-model");
660        }
661        let config2 = AgentConfig::from_settings_or_env(&temp_dir);
662        assert_eq!(config2.model, Some("env-var-model".to_string()));
663
664        // Cleanup
665        unsafe {
666            std::env::remove_var("ANTHROPIC_MODEL");
667        }
668        drop(std::fs::remove_dir_all(&temp_dir));
669    }
670
671    #[test]
672    #[serial_test::serial]
673    fn test_always_thinking_enabled() {
674        // Test that alwaysThinkingEnabled sets default MAX_THINKING_TOKENS
675        // Use settings.local.json to override any user settings
676        let temp_base = std::env::temp_dir();
677        let temp_dir = temp_base.join("test_config_thinking");
678        let settings_dir = temp_dir.join(".claude");
679
680        // Cleanup any existing test directory and env vars
681        drop(std::fs::remove_dir_all(&temp_dir));
682        unsafe {
683            std::env::remove_var("MAX_THINKING_TOKENS");
684        }
685
686        std::fs::create_dir_all(&settings_dir).ok();
687
688        // Use settings.local.json (higher priority than user settings)
689        let local_settings_file = settings_dir.join("settings.local.json");
690
691        // Test 1: alwaysThinkingEnabled = true should set default MAX_THINKING_TOKENS
692        std::fs::write(
693            &local_settings_file,
694            r#"{
695            "alwaysThinkingEnabled": true
696        }"#,
697        )
698        .ok();
699
700        let config1 = AgentConfig::from_settings_or_env(&temp_dir);
701        assert_eq!(config1.max_thinking_tokens, Some(20000));
702
703        // Test 2: alwaysThinkingEnabled = false should not set MAX_THINKING_TOKENS
704        std::fs::write(
705            &local_settings_file,
706            r#"{
707            "alwaysThinkingEnabled": false
708        }"#,
709        )
710        .ok();
711
712        let config2 = AgentConfig::from_settings_or_env(&temp_dir);
713        assert_eq!(config2.max_thinking_tokens, None);
714
715        // Test 3: Env var should override alwaysThinkingEnabled
716        std::fs::write(
717            &local_settings_file,
718            r#"{
719            "alwaysThinkingEnabled": true
720        }"#,
721        )
722        .ok();
723        unsafe {
724            std::env::set_var("MAX_THINKING_TOKENS", "8000");
725        }
726
727        let config3 = AgentConfig::from_settings_or_env(&temp_dir);
728        assert_eq!(config3.max_thinking_tokens, Some(8000));
729
730        // Test 4: No alwaysThinkingEnabled should not set MAX_THINKING_TOKENS
731        unsafe {
732            std::env::remove_var("MAX_THINKING_TOKENS");
733        }
734        // Explicitly set to false to override any user settings
735        std::fs::write(
736            &local_settings_file,
737            r#"{"model": "test-model", "alwaysThinkingEnabled": false}"#,
738        )
739        .ok();
740
741        let config4 = AgentConfig::from_settings_or_env(&temp_dir);
742        assert_eq!(config4.max_thinking_tokens, None);
743
744        // Cleanup
745        unsafe {
746            std::env::remove_var("MAX_THINKING_TOKENS");
747        }
748        drop(std::fs::remove_dir_all(&temp_dir));
749    }
750}