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    /// Guard that saves and restores environment variables
373    /// Automatically restores on drop, ensuring cleanup even on test failure
374    struct EnvGuard {
375        vars: Vec<(String, Option<String>)>,
376    }
377
378    impl EnvGuard {
379        fn new(var_names: &[&str]) -> Self {
380            let vars = var_names
381                .iter()
382                .map(|&name| {
383                    let original = std::env::var(name).ok();
384                    // Remove the env var for clean test state
385                    // Safety: We're in a serial test context
386                    unsafe {
387                        std::env::remove_var(name);
388                    }
389                    (name.to_string(), original)
390                })
391                .collect();
392            Self { vars }
393        }
394    }
395
396    impl Drop for EnvGuard {
397        fn drop(&mut self) {
398            // Restore original env vars
399            for (name, original) in &self.vars {
400                // Safety: We're restoring to the original state
401                unsafe {
402                    match original {
403                        Some(val) => std::env::set_var(name, val),
404                        None => std::env::remove_var(name),
405                    }
406                }
407            }
408        }
409    }
410
411    #[test]
412    fn test_default_config() {
413        let config = AgentConfig::default();
414        assert!(config.base_url.is_none());
415        assert!(config.api_key.is_none());
416        assert!(config.model.is_none());
417        assert!(config.small_fast_model.is_none());
418        assert!(config.max_thinking_tokens.is_none());
419        assert!(!config.is_configured());
420    }
421
422    #[test]
423    fn test_to_env_vars() {
424        let config = AgentConfig {
425            base_url: Some("https://api.example.com".to_string()),
426            api_key: Some("secret-key".to_string()),
427            model: Some("claude-3".to_string()),
428            small_fast_model: None,
429            max_thinking_tokens: None,
430        };
431
432        let env = config.to_env_vars();
433        assert_eq!(
434            env.get("ANTHROPIC_BASE_URL").unwrap(),
435            "https://api.example.com"
436        );
437        assert_eq!(env.get("ANTHROPIC_API_KEY").unwrap(), "secret-key");
438        assert_eq!(env.get("ANTHROPIC_MODEL").unwrap(), "claude-3");
439        assert!(!env.contains_key("ANTHROPIC_SMALL_FAST_MODEL"));
440    }
441
442    #[test]
443    fn test_masked_api_key() {
444        // Test with no API key
445        let config = AgentConfig::default();
446        assert!(config.masked_api_key().is_none());
447
448        // Test with empty string (edge case)
449        let config = AgentConfig {
450            api_key: Some("".to_string()),
451            ..Default::default()
452        };
453        assert_eq!(config.masked_api_key().unwrap(), "***");
454
455        // Test with single character (edge case)
456        let config = AgentConfig {
457            api_key: Some("a".to_string()),
458            ..Default::default()
459        };
460        assert_eq!(config.masked_api_key().unwrap(), "a***");
461
462        // Test with two characters (edge case)
463        let config = AgentConfig {
464            api_key: Some("ab".to_string()),
465            ..Default::default()
466        };
467        assert_eq!(config.masked_api_key().unwrap(), "a***");
468
469        // Test with short API key (<= 8 characters)
470        let config = AgentConfig {
471            api_key: Some("abc123".to_string()),
472            ..Default::default()
473        };
474        assert_eq!(config.masked_api_key().unwrap(), "a***3");
475
476        // Test with long API key
477        let config = AgentConfig {
478            api_key: Some("sk-ant-api03-12345-abcd".to_string()),
479            ..Default::default()
480        };
481        assert_eq!(config.masked_api_key().unwrap(), "sk-a***abcd");
482
483        // Test with Anthropic-style key
484        let config = AgentConfig {
485            api_key: Some("sk-ant-api03-xxxx-xxxx-xxxx-xxxxxxxxxxx".to_string()),
486            ..Default::default()
487        };
488        let masked = config.masked_api_key().unwrap();
489        assert!(masked.starts_with("sk-a"));
490        assert!(masked.ends_with("xxxx"));
491        assert!(masked.contains("***"));
492    }
493
494    #[test]
495    fn test_is_configured() {
496        let mut config = AgentConfig::default();
497        assert!(!config.is_configured());
498
499        config.model = Some("test".to_string());
500        assert!(config.is_configured());
501    }
502
503    #[test]
504    fn test_max_thinking_tokens_config() {
505        let config = AgentConfig {
506            base_url: None,
507            api_key: None,
508            model: None,
509            small_fast_model: None,
510            max_thinking_tokens: Some(4096),
511        };
512
513        assert!(config.is_configured());
514        assert_eq!(config.max_thinking_tokens, Some(4096));
515    }
516
517    #[test]
518    #[serial_test::serial]
519    fn test_from_settings_or_env() {
520        // Use EnvGuard to save and restore env vars, ensuring clean test state
521        let _guard = EnvGuard::new(&[
522            "ANTHROPIC_MODEL",
523            "ANTHROPIC_SMALL_FAST_MODEL",
524            "ANTHROPIC_BASE_URL",
525        ]);
526
527        // Use temp dir for test files
528        let temp_base = std::env::temp_dir();
529        let temp_dir = temp_base.join("test_config_combined");
530        let settings_dir = temp_dir.join(".claude");
531
532        // Cleanup any existing test directory
533        drop(std::fs::remove_dir_all(&temp_dir));
534        std::fs::create_dir_all(&settings_dir).ok();
535
536        let settings_file = settings_dir.join("settings.json");
537        std::fs::write(
538            &settings_file,
539            r#"{
540            "model": "settings-model",
541            "smallFastModel": "settings-small-model",
542            "apiBaseUrl": "https://settings.api.com"
543        }"#,
544        )
545        .ok();
546
547        // Test 1: Env overrides settings
548        // Set env var for model (should override)
549        unsafe {
550            std::env::set_var("ANTHROPIC_MODEL", "env-model");
551        }
552
553        let config = AgentConfig::from_settings_or_env(&temp_dir);
554        assert_eq!(config.model, Some("env-model".to_string()));
555        assert_eq!(
556            config.small_fast_model,
557            Some("settings-small-model".to_string())
558        );
559        assert_eq!(
560            config.base_url,
561            Some("https://settings.api.com".to_string())
562        );
563
564        // Test 2: Settings only (no env)
565        // Verify env var is actually removed before asserting
566        unsafe {
567            std::env::remove_var("ANTHROPIC_MODEL");
568        }
569        assert!(
570            std::env::var("ANTHROPIC_MODEL").is_err(),
571            "ANTHROPIC_MODEL should be removed"
572        );
573
574        let config2 = AgentConfig::from_settings_or_env(&temp_dir);
575        assert_eq!(config2.model, Some("settings-model".to_string()));
576        assert_eq!(
577            config2.small_fast_model,
578            Some("settings-small-model".to_string())
579        );
580
581        // Test 3: Env only (no settings)
582        std::fs::remove_file(&settings_file).ok();
583        unsafe {
584            std::env::set_var("ANTHROPIC_MODEL", "env-only-model");
585        }
586
587        let config3 = AgentConfig::from_settings_or_env(&temp_dir);
588        assert_eq!(config3.model, Some("env-only-model".to_string()));
589        assert!(config3.small_fast_model.is_none());
590
591        // Cleanup (EnvGuard handles env var restoration)
592        drop(std::fs::remove_dir_all(&temp_dir));
593    }
594
595    #[test]
596    #[serial_test::serial]
597    fn test_from_settings_env_fallback() {
598        // Test that settings.env is used as fallback when top-level fields are not set
599        //
600        // Note: This test uses settings.local.json which has highest priority.
601        // It explicitly sets model to null to override any user's global ~/.claude/settings.json
602        // However, due to the merge logic (only overwrites if Some), we can't truly "unset"
603        // a field. So this test verifies the code path works by explicitly NOT setting
604        // top-level model in local settings and checking that env values are available.
605        //
606        // If this test fails with "opus" or another model, it means the user has global
607        // settings at ~/.claude/settings.json which takes precedence. In that case, the
608        // test_from_settings_priority_order test should be used to verify the fallback logic.
609
610        // Use EnvGuard to save and restore env vars, ensuring clean test state
611        let _guard = EnvGuard::new(&[
612            "ANTHROPIC_MODEL",
613            "ANTHROPIC_SMALL_FAST_MODEL",
614            "ANTHROPIC_BASE_URL",
615        ]);
616
617        let temp_base = std::env::temp_dir();
618        let temp_dir = temp_base.join("test_config_env_fallback");
619        let settings_dir = temp_dir.join(".claude");
620
621        // Cleanup any existing test directory
622        drop(std::fs::remove_dir_all(&temp_dir));
623        std::fs::create_dir_all(&settings_dir).ok();
624
625        // Use settings.local.json to set top-level model to a known value
626        // Then verify settings.env values are still accessible (even if not used for model
627        // when top-level is set)
628        //
629        // This tests the settings loading path, even though the fallback to env
630        // won't be exercised if top-level model is set.
631        let settings_file = settings_dir.join("settings.local.json");
632        std::fs::write(
633            &settings_file,
634            r#"{
635            "model": "local-model",
636            "smallFastModel": "local-small-model",
637            "apiBaseUrl": "https://local.api.com"
638        }"#,
639        )
640        .ok();
641
642        let config = AgentConfig::from_settings_or_env(&temp_dir);
643        // Local settings should override any user global settings
644        assert_eq!(config.model, Some("local-model".to_string()));
645        assert_eq!(
646            config.small_fast_model,
647            Some("local-small-model".to_string())
648        );
649        assert_eq!(config.base_url, Some("https://local.api.com".to_string()));
650
651        // Cleanup
652        drop(std::fs::remove_dir_all(&temp_dir));
653    }
654
655    #[test]
656    #[serial_test::serial]
657    fn test_from_settings_priority_order() {
658        // Test priority: env > settings.top-level > settings.env > default
659        // Use EnvGuard to save and restore env vars, ensuring clean test state
660        let _guard = EnvGuard::new(&[
661            "ANTHROPIC_MODEL",
662            "ANTHROPIC_SMALL_FAST_MODEL",
663            "ANTHROPIC_BASE_URL",
664        ]);
665
666        let temp_base = std::env::temp_dir();
667        let temp_dir = temp_base.join("test_config_priority");
668        let settings_dir = temp_dir.join(".claude");
669
670        drop(std::fs::remove_dir_all(&temp_dir));
671        std::fs::create_dir_all(&settings_dir).ok();
672
673        // Create settings with both top-level and env fields
674        let settings_file = settings_dir.join("settings.json");
675        std::fs::write(
676            &settings_file,
677            r#"{
678            "model": "top-level-model",
679            "env": {
680                "ANTHROPIC_MODEL": "env-object-model"
681            }
682        }"#,
683        )
684        .ok();
685
686        // Test 1: Top-level should override env object
687        let config1 = AgentConfig::from_settings_or_env(&temp_dir);
688        assert_eq!(config1.model, Some("top-level-model".to_string()));
689
690        // Test 2: Env var should override both
691        unsafe {
692            std::env::set_var("ANTHROPIC_MODEL", "env-var-model");
693        }
694        let config2 = AgentConfig::from_settings_or_env(&temp_dir);
695        assert_eq!(config2.model, Some("env-var-model".to_string()));
696
697        // Cleanup (EnvGuard handles env var restoration)
698        drop(std::fs::remove_dir_all(&temp_dir));
699    }
700
701    #[test]
702    #[serial_test::serial]
703    fn test_always_thinking_enabled() {
704        // Test that alwaysThinkingEnabled sets default MAX_THINKING_TOKENS
705        // Use EnvGuard to save and restore env vars, ensuring clean test state
706        let _guard = EnvGuard::new(&["MAX_THINKING_TOKENS"]);
707
708        // Use settings.local.json to override any user settings
709        let temp_base = std::env::temp_dir();
710        let temp_dir = temp_base.join("test_config_thinking");
711        let settings_dir = temp_dir.join(".claude");
712
713        // Cleanup any existing test directory
714        drop(std::fs::remove_dir_all(&temp_dir));
715
716        std::fs::create_dir_all(&settings_dir).ok();
717
718        // Use settings.local.json (higher priority than user settings)
719        let local_settings_file = settings_dir.join("settings.local.json");
720
721        // Test 1: alwaysThinkingEnabled = true should set default MAX_THINKING_TOKENS
722        std::fs::write(
723            &local_settings_file,
724            r#"{
725            "alwaysThinkingEnabled": true
726        }"#,
727        )
728        .ok();
729
730        let config1 = AgentConfig::from_settings_or_env(&temp_dir);
731        assert_eq!(config1.max_thinking_tokens, Some(20000));
732
733        // Test 2: alwaysThinkingEnabled = false should not set MAX_THINKING_TOKENS
734        std::fs::write(
735            &local_settings_file,
736            r#"{
737            "alwaysThinkingEnabled": false
738        }"#,
739        )
740        .ok();
741
742        let config2 = AgentConfig::from_settings_or_env(&temp_dir);
743        assert_eq!(config2.max_thinking_tokens, None);
744
745        // Test 3: Env var should override alwaysThinkingEnabled
746        std::fs::write(
747            &local_settings_file,
748            r#"{
749            "alwaysThinkingEnabled": true
750        }"#,
751        )
752        .ok();
753        unsafe {
754            std::env::set_var("MAX_THINKING_TOKENS", "8000");
755        }
756
757        let config3 = AgentConfig::from_settings_or_env(&temp_dir);
758        assert_eq!(config3.max_thinking_tokens, Some(8000));
759
760        // Test 4: No alwaysThinkingEnabled should not set MAX_THINKING_TOKENS
761        unsafe {
762            std::env::remove_var("MAX_THINKING_TOKENS");
763        }
764        // Explicitly set to false to override any user settings
765        std::fs::write(
766            &local_settings_file,
767            r#"{"model": "test-model", "alwaysThinkingEnabled": false}"#,
768        )
769        .ok();
770
771        let config4 = AgentConfig::from_settings_or_env(&temp_dir);
772        assert_eq!(config4.max_thinking_tokens, None);
773
774        // Cleanup (EnvGuard handles env var restoration)
775        drop(std::fs::remove_dir_all(&temp_dir));
776    }
777}