Skip to main content

cc_switch/config/
types.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::collections::BTreeMap;
3
4/// Type alias for configuration map
5type ConfigMap = BTreeMap<String, Configuration>;
6/// Type alias for environment variable map
7type EnvMap = BTreeMap<String, String>;
8/// Type alias for JSON value map
9type JsonMap = BTreeMap<String, serde_json::Value>;
10/// Type alias for Codex configuration map
11type CodexConfigMap = BTreeMap<String, crate::codex::CodexConfiguration>;
12
13/// Storage mode for how configuration should be written to settings.json
14#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
15pub enum StorageMode {
16    /// Write to env field with uppercase environment variable names (default)
17    #[serde(rename = "env")]
18    #[default]
19    Env,
20    /// Write to root level with camelCase field names
21    #[serde(rename = "config")]
22    Config,
23}
24
25/// Represents a Claude API configuration
26///
27/// Contains the components needed to configure Claude API access:
28/// - alias_name: User-friendly identifier for the configuration
29/// - token: API authentication token
30/// - url: Base URL for the API endpoint
31/// - model: Optional custom model name
32/// - small_fast_model: Optional Haiku-class model for background tasks
33#[derive(Serialize, Deserialize, Default, Clone)]
34pub struct Configuration {
35    /// User-friendly alias name for this configuration
36    pub alias_name: String,
37    /// ANTHROPIC_AUTH_TOKEN value (API authentication token)
38    pub token: String,
39    /// ANTHROPIC_BASE_URL value (API endpoint URL)
40    pub url: String,
41    /// ANTHROPIC_MODEL value (custom model name)
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub model: Option<String>,
44    /// ANTHROPIC_SMALL_FAST_MODEL value (Haiku-class model for background tasks)
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub small_fast_model: Option<String>,
47    /// ANTHROPIC_MAX_THINKING_TOKENS value (Maximum thinking tokens limit)
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub max_thinking_tokens: Option<u32>,
50    /// API timeout in milliseconds
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub api_timeout_ms: Option<u32>,
53    /// Disable non-essential traffic flag
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub claude_code_disable_nonessential_traffic: Option<u32>,
56    /// Default Sonnet model name
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub anthropic_default_sonnet_model: Option<String>,
59    /// Default Opus model name
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub anthropic_default_opus_model: Option<String>,
62    /// Default Haiku model name
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub anthropic_default_haiku_model: Option<String>,
65    /// Enable experimental agent teams flag
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub claude_code_experimental_agent_teams: Option<u32>,
68    /// Disable 1M context limit flag
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub claude_code_disable_1m_context: Option<u32>,
71    /// Subagent model name
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub claude_code_subagent_model: Option<String>,
74    /// Disable non-streaming fallback flag
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub claude_code_disable_nonstreaming_fallback: Option<u32>,
77    /// Effort level for Claude Code
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub claude_code_effort_level: Option<String>,
80}
81
82impl Configuration {
83    /// Get all environment variable names that this configuration can set
84    ///
85    /// Returns a vector of all UPPERCASE environment variable names
86    /// that can be set by this configuration, used for conflict detection
87    /// in config mode.
88    pub fn get_env_field_names() -> Vec<&'static str> {
89        vec![
90            "ANTHROPIC_AUTH_TOKEN",
91            "ANTHROPIC_BASE_URL",
92            "ANTHROPIC_MODEL",
93            "ANTHROPIC_SMALL_FAST_MODEL",
94            "ANTHROPIC_MAX_THINKING_TOKENS",
95            "API_TIMEOUT_MS",
96            "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
97            "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
98            "CLAUDE_CODE_DISABLE_1M_CONTEXT",
99            "CLAUDE_CODE_SUBAGENT_MODEL",
100            "CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK",
101            "CLAUDE_CODE_EFFORT_LEVEL",
102            "ANTHROPIC_DEFAULT_SONNET_MODEL",
103            "ANTHROPIC_DEFAULT_OPUS_MODEL",
104            "ANTHROPIC_DEFAULT_HAIKU_MODEL",
105        ]
106    }
107
108    /// Get environment variable names that should be cleared in env mode
109    ///
110    /// Returns a vector of UPPERCASE environment variable names that are
111    /// configuration-specific and should be cleared from settings.json when
112    /// switching in env mode. User preference fields are excluded.
113    pub fn get_clearable_env_field_names() -> Vec<&'static str> {
114        vec![
115            "ANTHROPIC_AUTH_TOKEN",
116            "ANTHROPIC_BASE_URL",
117            "ANTHROPIC_MODEL",
118            "ANTHROPIC_SMALL_FAST_MODEL",
119            "ANTHROPIC_MAX_THINKING_TOKENS",
120            "API_TIMEOUT_MS",
121            "ANTHROPIC_DEFAULT_SONNET_MODEL",
122            "ANTHROPIC_DEFAULT_OPUS_MODEL",
123            "ANTHROPIC_DEFAULT_HAIKU_MODEL",
124            "CLAUDE_CODE_SUBAGENT_MODEL",
125            "CLAUDE_CODE_EFFORT_LEVEL",
126            // User preference fields are NOT included:
127            // - CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
128            // - CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS
129            // - CLAUDE_CODE_DISABLE_1M_CONTEXT
130            // - CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK
131        ]
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn test_get_env_field_names() {
141        let fields = Configuration::get_env_field_names();
142
143        // Verify all expected fields are present
144        let expected_fields = vec![
145            "ANTHROPIC_AUTH_TOKEN",
146            "ANTHROPIC_BASE_URL",
147            "ANTHROPIC_MODEL",
148            "ANTHROPIC_SMALL_FAST_MODEL",
149            "ANTHROPIC_MAX_THINKING_TOKENS",
150            "API_TIMEOUT_MS",
151            "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
152            "ANTHROPIC_DEFAULT_SONNET_MODEL",
153            "ANTHROPIC_DEFAULT_OPUS_MODEL",
154            "ANTHROPIC_DEFAULT_HAIKU_MODEL",
155            "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
156            "CLAUDE_CODE_DISABLE_1M_CONTEXT",
157            "CLAUDE_CODE_SUBAGENT_MODEL",
158            "CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK",
159            "CLAUDE_CODE_EFFORT_LEVEL",
160        ];
161
162        assert_eq!(
163            fields.len(),
164            expected_fields.len(),
165            "Should have exactly 15 fields"
166        );
167
168        for expected_field in expected_fields {
169            assert!(
170                fields.contains(&expected_field),
171                "Missing field: {}",
172                expected_field
173            );
174        }
175
176        // Verify all fields are uppercase
177        for field in &fields {
178            assert_eq!(
179                field,
180                &field.to_uppercase(),
181                "Field {} should be uppercase",
182                field
183            );
184        }
185    }
186
187    #[test]
188    fn test_get_clearable_env_field_names() {
189        let fields = Configuration::get_clearable_env_field_names();
190
191        // Verify clearable fields (excludes user preference fields)
192        let expected_fields = vec![
193            "ANTHROPIC_AUTH_TOKEN",
194            "ANTHROPIC_BASE_URL",
195            "ANTHROPIC_MODEL",
196            "ANTHROPIC_SMALL_FAST_MODEL",
197            "ANTHROPIC_MAX_THINKING_TOKENS",
198            "API_TIMEOUT_MS",
199            "ANTHROPIC_DEFAULT_SONNET_MODEL",
200            "ANTHROPIC_DEFAULT_OPUS_MODEL",
201            "ANTHROPIC_DEFAULT_HAIKU_MODEL",
202            "CLAUDE_CODE_SUBAGENT_MODEL",
203            "CLAUDE_CODE_EFFORT_LEVEL",
204        ];
205
206        // User preference fields should NOT be in clearable list
207        let excluded_fields = vec![
208            "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
209            "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
210            "CLAUDE_CODE_DISABLE_1M_CONTEXT",
211            "CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK",
212        ];
213
214        assert_eq!(
215            fields.len(),
216            expected_fields.len(),
217            "Should have exactly 11 clearable fields"
218        );
219
220        for expected_field in expected_fields {
221            assert!(
222                fields.contains(&expected_field),
223                "Missing clearable field: {}",
224                expected_field
225            );
226        }
227
228        // Verify excluded fields are NOT present
229        for excluded_field in excluded_fields {
230            assert!(
231                !fields.contains(&excluded_field),
232                "User preference field {} should NOT be in clearable list",
233                excluded_field
234            );
235        }
236    }
237
238    #[test]
239    fn test_remove_anthropic_env_uses_dynamic_fields() {
240        let mut settings = ClaudeSettings::default();
241
242        // Add all possible environment variables
243        let env_fields = Configuration::get_env_field_names();
244        for field in &env_fields {
245            settings
246                .env
247                .insert(field.to_string(), "test_value".to_string());
248        }
249
250        // Add some other env variables that shouldn't be removed
251        settings
252            .env
253            .insert("OTHER_VAR".to_string(), "other_value".to_string());
254        settings
255            .env
256            .insert("CLAUDE_THEME".to_string(), "dark".to_string());
257
258        // Remove Anthropic environment variables
259        settings.remove_anthropic_env();
260
261        // Verify all Anthropic fields are removed
262        for field in &env_fields {
263            assert!(
264                !settings.env.contains_key(*field),
265                "Field {} should be removed",
266                field
267            );
268        }
269
270        // Verify other fields are preserved
271        assert!(
272            settings.env.contains_key("OTHER_VAR"),
273            "Other variables should be preserved"
274        );
275        assert!(
276            settings.env.contains_key("CLAUDE_THEME"),
277            "Other variables should be preserved"
278        );
279        assert_eq!(
280            settings.env.get("OTHER_VAR"),
281            Some(&"other_value".to_string())
282        );
283        assert_eq!(settings.env.get("CLAUDE_THEME"), Some(&"dark".to_string()));
284    }
285
286    #[test]
287    fn test_switch_to_config_uses_dynamic_fields() {
288        let mut settings = ClaudeSettings::default();
289
290        // Add all possible environment variables
291        let env_fields = Configuration::get_env_field_names();
292        for field in &env_fields {
293            settings
294                .env
295                .insert(field.to_string(), "old_value".to_string());
296        }
297
298        // Create a test configuration
299        let config = Configuration {
300            alias_name: "test".to_string(),
301            token: "new_token".to_string(),
302            url: "https://api.new.com".to_string(),
303            model: Some("new_model".to_string()),
304            small_fast_model: Some("new_fast_model".to_string()),
305            max_thinking_tokens: Some(50000),
306            api_timeout_ms: Some(300000),
307            claude_code_disable_nonessential_traffic: Some(1),
308            anthropic_default_sonnet_model: Some("new_sonnet".to_string()),
309            anthropic_default_opus_model: Some("new_opus".to_string()),
310            anthropic_default_haiku_model: Some("new_haiku".to_string()),
311            claude_code_experimental_agent_teams: None,
312            claude_code_disable_1m_context: None,
313            claude_code_subagent_model: None,
314            claude_code_disable_nonstreaming_fallback: None,
315            claude_code_effort_level: None,
316        };
317
318        // Switch to new configuration
319        settings.switch_to_config(&config);
320
321        // Verify the required fields are set correctly
322        assert_eq!(
323            settings.env.get("ANTHROPIC_AUTH_TOKEN"),
324            Some(&"new_token".to_string())
325        );
326        assert_eq!(
327            settings.env.get("ANTHROPIC_BASE_URL"),
328            Some(&"https://api.new.com".to_string())
329        );
330        assert_eq!(
331            settings.env.get("ANTHROPIC_MODEL"),
332            Some(&"new_model".to_string())
333        );
334        assert_eq!(
335            settings.env.get("ANTHROPIC_SMALL_FAST_MODEL"),
336            Some(&"new_fast_model".to_string())
337        );
338
339        // Verify additional fields are set correctly when provided in config
340        assert_eq!(
341            settings.env.get("ANTHROPIC_MAX_THINKING_TOKENS"),
342            Some(&"50000".to_string())
343        );
344        assert_eq!(
345            settings.env.get("API_TIMEOUT_MS"),
346            Some(&"300000".to_string())
347        );
348        assert_eq!(
349            settings.env.get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"),
350            Some(&"1".to_string())
351        );
352        assert_eq!(
353            settings.env.get("ANTHROPIC_DEFAULT_SONNET_MODEL"),
354            Some(&"new_sonnet".to_string())
355        );
356        assert_eq!(
357            settings.env.get("ANTHROPIC_DEFAULT_OPUS_MODEL"),
358            Some(&"new_opus".to_string())
359        );
360        assert_eq!(
361            settings.env.get("ANTHROPIC_DEFAULT_HAIKU_MODEL"),
362            Some(&"new_haiku".to_string())
363        );
364    }
365
366    #[test]
367    fn test_switch_to_config_removes_optional_fields_when_not_provided() {
368        let mut settings = ClaudeSettings::default();
369
370        // Add all possible environment variables with old values
371        let env_fields = Configuration::get_env_field_names();
372        for field in &env_fields {
373            settings
374                .env
375                .insert(field.to_string(), "old_value".to_string());
376        }
377
378        // Create a test configuration without optional fields
379        let config = Configuration {
380            alias_name: "test".to_string(),
381            token: "new_token".to_string(),
382            url: "https://api.new.com".to_string(),
383            model: Some("new_model".to_string()),
384            small_fast_model: Some("new_fast_model".to_string()),
385            max_thinking_tokens: None,
386            api_timeout_ms: None,
387            claude_code_disable_nonessential_traffic: None,
388            anthropic_default_sonnet_model: None,
389            anthropic_default_opus_model: None,
390            anthropic_default_haiku_model: None,
391            claude_code_experimental_agent_teams: None,
392            claude_code_disable_1m_context: None,
393            claude_code_subagent_model: None,
394            claude_code_disable_nonstreaming_fallback: None,
395            claude_code_effort_level: None,
396        };
397
398        // Switch to new configuration
399        settings.switch_to_config(&config);
400
401        // Verify the required fields are set correctly
402        assert_eq!(
403            settings.env.get("ANTHROPIC_AUTH_TOKEN"),
404            Some(&"new_token".to_string())
405        );
406        assert_eq!(
407            settings.env.get("ANTHROPIC_BASE_URL"),
408            Some(&"https://api.new.com".to_string())
409        );
410        assert_eq!(
411            settings.env.get("ANTHROPIC_MODEL"),
412            Some(&"new_model".to_string())
413        );
414        assert_eq!(
415            settings.env.get("ANTHROPIC_SMALL_FAST_MODEL"),
416            Some(&"new_fast_model".to_string())
417        );
418
419        // Verify optional fields are removed when not provided in config
420        assert!(!settings.env.contains_key("ANTHROPIC_MAX_THINKING_TOKENS"));
421        assert!(!settings.env.contains_key("API_TIMEOUT_MS"));
422        assert!(
423            !settings
424                .env
425                .contains_key("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")
426        );
427        assert!(!settings.env.contains_key("ANTHROPIC_DEFAULT_SONNET_MODEL"));
428        assert!(!settings.env.contains_key("ANTHROPIC_DEFAULT_OPUS_MODEL"));
429        assert!(!settings.env.contains_key("ANTHROPIC_DEFAULT_HAIKU_MODEL"));
430        assert!(!settings.env.contains_key("CLAUDE_CODE_SUBAGENT_MODEL"));
431        assert!(
432            !settings
433                .env
434                .contains_key("CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK")
435        );
436        assert!(!settings.env.contains_key("CLAUDE_CODE_EFFORT_LEVEL"));
437    }
438}
439
440/// Storage manager for Claude API configurations
441///
442/// Handles persistence and retrieval of multiple API configurations
443/// stored in `~/.cc_auto_switch/configurations.json`
444#[derive(Serialize, Deserialize, Default)]
445pub struct ConfigStorage {
446    /// Map of alias names to configuration objects
447    pub configurations: ConfigMap,
448    /// Custom directory for Claude settings (optional)
449    pub claude_settings_dir: Option<String>,
450    /// Default storage mode for writing configurations (None = use env mode)
451    #[serde(skip_serializing_if = "Option::is_none")]
452    pub default_storage_mode: Option<StorageMode>,
453    /// Codex (OpenAI) configurations, stored separately from Claude configurations
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub codex_configurations: Option<CodexConfigMap>,
456}
457
458/// Claude settings manager for API configuration
459///
460/// Manages the Claude settings.json file to control Claude's API configuration
461/// Handles environment variables and preserves other settings
462#[derive(Default, Clone)]
463#[allow(dead_code)]
464pub struct ClaudeSettings {
465    /// Environment variables map (ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, ANTHROPIC_MODEL, ANTHROPIC_SMALL_FAST_MODEL)
466    pub env: EnvMap,
467    /// Other settings to preserve when modifying API configuration
468    pub other: JsonMap,
469}
470
471impl Serialize for ClaudeSettings {
472    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
473    where
474        S: Serializer,
475    {
476        use serde::ser::SerializeMap;
477
478        let mut map = serializer.serialize_map(Some(
479            self.other.len() + if self.env.is_empty() { 0 } else { 1 },
480        ))?;
481
482        // Serialize env field only if it has content
483        if !self.env.is_empty() {
484            map.serialize_entry("env", &self.env)?;
485        }
486
487        // Serialize other fields
488        for (key, value) in &self.other {
489            map.serialize_entry(key, value)?;
490        }
491
492        map.end()
493    }
494}
495
496impl<'de> Deserialize<'de> for ClaudeSettings {
497    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
498    where
499        D: Deserializer<'de>,
500    {
501        #[derive(Deserialize)]
502        struct ClaudeSettingsHelper {
503            #[serde(default)]
504            env: EnvMap,
505            #[serde(flatten)]
506            other: JsonMap,
507        }
508
509        let helper = ClaudeSettingsHelper::deserialize(deserializer)?;
510        Ok(ClaudeSettings {
511            env: helper.env,
512            other: helper.other,
513        })
514    }
515}
516
517/// Parameters for adding a new configuration
518#[allow(dead_code)]
519pub struct AddCommandParams {
520    pub alias_name: String,
521    pub token: Option<String>,
522    pub url: Option<String>,
523    pub model: Option<String>,
524    pub small_fast_model: Option<String>,
525    pub max_thinking_tokens: Option<u32>,
526    pub api_timeout_ms: Option<u32>,
527    pub claude_code_disable_nonessential_traffic: Option<u32>,
528    pub anthropic_default_sonnet_model: Option<String>,
529    pub anthropic_default_opus_model: Option<String>,
530    pub anthropic_default_haiku_model: Option<String>,
531    pub claude_code_subagent_model: Option<String>,
532    pub claude_code_disable_nonstreaming_fallback: Option<u32>,
533    pub claude_code_effort_level: Option<String>,
534    pub force: bool,
535    pub interactive: bool,
536    pub token_arg: Option<String>,
537    pub url_arg: Option<String>,
538    pub from_file: Option<String>,
539}