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    /// Disable prompt caching flag
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub disable_prompt_caching: Option<u32>,
83    /// Disable experimental betas flag
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub claude_code_disable_experimental_betas: Option<u32>,
86    /// Disable auto-updater flag
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub disable_autoupdater: Option<u32>,
89}
90
91impl Configuration {
92    /// Get all environment variable names that this configuration can set
93    ///
94    /// Returns a vector of all UPPERCASE environment variable names
95    /// that can be set by this configuration, used for conflict detection
96    /// in config mode.
97    pub fn get_env_field_names() -> Vec<&'static str> {
98        vec![
99            "ANTHROPIC_AUTH_TOKEN",
100            "ANTHROPIC_BASE_URL",
101            "ANTHROPIC_MODEL",
102            "ANTHROPIC_SMALL_FAST_MODEL",
103            "ANTHROPIC_MAX_THINKING_TOKENS",
104            "API_TIMEOUT_MS",
105            "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
106            "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
107            "CLAUDE_CODE_DISABLE_1M_CONTEXT",
108            "CLAUDE_CODE_SUBAGENT_MODEL",
109            "CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK",
110            "CLAUDE_CODE_EFFORT_LEVEL",
111            "DISABLE_PROMPT_CACHING",
112            "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS",
113            "DISABLE_AUTOUPDATER",
114            "ANTHROPIC_DEFAULT_SONNET_MODEL",
115            "ANTHROPIC_DEFAULT_OPUS_MODEL",
116            "ANTHROPIC_DEFAULT_HAIKU_MODEL",
117        ]
118    }
119
120    /// Get environment variable names that should be cleared in env mode
121    ///
122    /// Returns a vector of UPPERCASE environment variable names that are
123    /// configuration-specific and should be cleared from settings.json when
124    /// switching in env mode. User preference fields are excluded.
125    pub fn get_clearable_env_field_names() -> Vec<&'static str> {
126        vec![
127            "ANTHROPIC_AUTH_TOKEN",
128            "ANTHROPIC_BASE_URL",
129            "ANTHROPIC_MODEL",
130            "ANTHROPIC_SMALL_FAST_MODEL",
131            "ANTHROPIC_MAX_THINKING_TOKENS",
132            "API_TIMEOUT_MS",
133            "ANTHROPIC_DEFAULT_SONNET_MODEL",
134            "ANTHROPIC_DEFAULT_OPUS_MODEL",
135            "ANTHROPIC_DEFAULT_HAIKU_MODEL",
136            "CLAUDE_CODE_SUBAGENT_MODEL",
137            "CLAUDE_CODE_EFFORT_LEVEL",
138            // User preference fields are NOT included:
139            // - CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC
140            // - CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS
141            // - CLAUDE_CODE_DISABLE_1M_CONTEXT
142            // - CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK
143            // - DISABLE_PROMPT_CACHING
144            // - CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS
145            // - DISABLE_AUTOUPDATER
146        ]
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_get_env_field_names() {
156        let fields = Configuration::get_env_field_names();
157
158        // Verify all expected fields are present
159        let expected_fields = vec![
160            "ANTHROPIC_AUTH_TOKEN",
161            "ANTHROPIC_BASE_URL",
162            "ANTHROPIC_MODEL",
163            "ANTHROPIC_SMALL_FAST_MODEL",
164            "ANTHROPIC_MAX_THINKING_TOKENS",
165            "API_TIMEOUT_MS",
166            "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
167            "ANTHROPIC_DEFAULT_SONNET_MODEL",
168            "ANTHROPIC_DEFAULT_OPUS_MODEL",
169            "ANTHROPIC_DEFAULT_HAIKU_MODEL",
170            "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
171            "CLAUDE_CODE_DISABLE_1M_CONTEXT",
172            "CLAUDE_CODE_SUBAGENT_MODEL",
173            "CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK",
174            "CLAUDE_CODE_EFFORT_LEVEL",
175            "DISABLE_PROMPT_CACHING",
176            "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS",
177            "DISABLE_AUTOUPDATER",
178        ];
179
180        assert_eq!(
181            fields.len(),
182            expected_fields.len(),
183            "Should have exactly 18 fields"
184        );
185
186        for expected_field in expected_fields {
187            assert!(
188                fields.contains(&expected_field),
189                "Missing field: {}",
190                expected_field
191            );
192        }
193
194        // Verify all fields are uppercase
195        for field in &fields {
196            assert_eq!(
197                field,
198                &field.to_uppercase(),
199                "Field {} should be uppercase",
200                field
201            );
202        }
203    }
204
205    #[test]
206    fn test_get_clearable_env_field_names() {
207        let fields = Configuration::get_clearable_env_field_names();
208
209        // Verify clearable fields (excludes user preference fields)
210        let expected_fields = vec![
211            "ANTHROPIC_AUTH_TOKEN",
212            "ANTHROPIC_BASE_URL",
213            "ANTHROPIC_MODEL",
214            "ANTHROPIC_SMALL_FAST_MODEL",
215            "ANTHROPIC_MAX_THINKING_TOKENS",
216            "API_TIMEOUT_MS",
217            "ANTHROPIC_DEFAULT_SONNET_MODEL",
218            "ANTHROPIC_DEFAULT_OPUS_MODEL",
219            "ANTHROPIC_DEFAULT_HAIKU_MODEL",
220            "CLAUDE_CODE_SUBAGENT_MODEL",
221            "CLAUDE_CODE_EFFORT_LEVEL",
222        ];
223
224        // User preference fields should NOT be in clearable list
225        let excluded_fields = vec![
226            "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
227            "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
228            "CLAUDE_CODE_DISABLE_1M_CONTEXT",
229            "CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK",
230            "DISABLE_PROMPT_CACHING",
231            "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS",
232            "DISABLE_AUTOUPDATER",
233        ];
234
235        assert_eq!(
236            fields.len(),
237            expected_fields.len(),
238            "Should have exactly 11 clearable fields"
239        );
240
241        for expected_field in expected_fields {
242            assert!(
243                fields.contains(&expected_field),
244                "Missing clearable field: {}",
245                expected_field
246            );
247        }
248
249        // Verify excluded fields are NOT present
250        for excluded_field in excluded_fields {
251            assert!(
252                !fields.contains(&excluded_field),
253                "User preference field {} should NOT be in clearable list",
254                excluded_field
255            );
256        }
257    }
258
259    #[test]
260    fn test_remove_anthropic_env_uses_dynamic_fields() {
261        let mut settings = ClaudeSettings::default();
262
263        // Add all possible environment variables
264        let env_fields = Configuration::get_env_field_names();
265        for field in &env_fields {
266            settings
267                .env
268                .insert(field.to_string(), "test_value".to_string());
269        }
270
271        // Add some other env variables that shouldn't be removed
272        settings
273            .env
274            .insert("OTHER_VAR".to_string(), "other_value".to_string());
275        settings
276            .env
277            .insert("CLAUDE_THEME".to_string(), "dark".to_string());
278
279        // Remove Anthropic environment variables
280        settings.remove_anthropic_env();
281
282        // Verify all Anthropic fields are removed
283        for field in &env_fields {
284            assert!(
285                !settings.env.contains_key(*field),
286                "Field {} should be removed",
287                field
288            );
289        }
290
291        // Verify other fields are preserved
292        assert!(
293            settings.env.contains_key("OTHER_VAR"),
294            "Other variables should be preserved"
295        );
296        assert!(
297            settings.env.contains_key("CLAUDE_THEME"),
298            "Other variables should be preserved"
299        );
300        assert_eq!(
301            settings.env.get("OTHER_VAR"),
302            Some(&"other_value".to_string())
303        );
304        assert_eq!(settings.env.get("CLAUDE_THEME"), Some(&"dark".to_string()));
305    }
306
307    #[test]
308    fn test_switch_to_config_uses_dynamic_fields() {
309        let mut settings = ClaudeSettings::default();
310
311        // Add all possible environment variables
312        let env_fields = Configuration::get_env_field_names();
313        for field in &env_fields {
314            settings
315                .env
316                .insert(field.to_string(), "old_value".to_string());
317        }
318
319        // Create a test configuration
320        let config = Configuration {
321            alias_name: "test".to_string(),
322            token: "new_token".to_string(),
323            url: "https://api.new.com".to_string(),
324            model: Some("new_model".to_string()),
325            small_fast_model: Some("new_fast_model".to_string()),
326            max_thinking_tokens: Some(50000),
327            api_timeout_ms: Some(300000),
328            claude_code_disable_nonessential_traffic: Some(1),
329            anthropic_default_sonnet_model: Some("new_sonnet".to_string()),
330            anthropic_default_opus_model: Some("new_opus".to_string()),
331            anthropic_default_haiku_model: Some("new_haiku".to_string()),
332            claude_code_experimental_agent_teams: None,
333            claude_code_disable_1m_context: None,
334            claude_code_subagent_model: None,
335            claude_code_disable_nonstreaming_fallback: None,
336            claude_code_effort_level: None,
337            disable_prompt_caching: None,
338            claude_code_disable_experimental_betas: None,
339            disable_autoupdater: None,
340        };
341
342        // Switch to new configuration
343        settings.switch_to_config(&config);
344
345        // Verify the required fields are set correctly
346        assert_eq!(
347            settings.env.get("ANTHROPIC_AUTH_TOKEN"),
348            Some(&"new_token".to_string())
349        );
350        assert_eq!(
351            settings.env.get("ANTHROPIC_BASE_URL"),
352            Some(&"https://api.new.com".to_string())
353        );
354        assert_eq!(
355            settings.env.get("ANTHROPIC_MODEL"),
356            Some(&"new_model".to_string())
357        );
358        assert_eq!(
359            settings.env.get("ANTHROPIC_SMALL_FAST_MODEL"),
360            Some(&"new_fast_model".to_string())
361        );
362
363        // Verify additional fields are set correctly when provided in config
364        assert_eq!(
365            settings.env.get("ANTHROPIC_MAX_THINKING_TOKENS"),
366            Some(&"50000".to_string())
367        );
368        assert_eq!(
369            settings.env.get("API_TIMEOUT_MS"),
370            Some(&"300000".to_string())
371        );
372        assert_eq!(
373            settings.env.get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"),
374            Some(&"1".to_string())
375        );
376        assert_eq!(
377            settings.env.get("ANTHROPIC_DEFAULT_SONNET_MODEL"),
378            Some(&"new_sonnet".to_string())
379        );
380        assert_eq!(
381            settings.env.get("ANTHROPIC_DEFAULT_OPUS_MODEL"),
382            Some(&"new_opus".to_string())
383        );
384        assert_eq!(
385            settings.env.get("ANTHROPIC_DEFAULT_HAIKU_MODEL"),
386            Some(&"new_haiku".to_string())
387        );
388    }
389
390    #[test]
391    fn test_switch_to_config_removes_optional_fields_when_not_provided() {
392        let mut settings = ClaudeSettings::default();
393
394        // Add all possible environment variables with old values
395        let env_fields = Configuration::get_env_field_names();
396        for field in &env_fields {
397            settings
398                .env
399                .insert(field.to_string(), "old_value".to_string());
400        }
401
402        // Create a test configuration without optional fields
403        let config = Configuration {
404            alias_name: "test".to_string(),
405            token: "new_token".to_string(),
406            url: "https://api.new.com".to_string(),
407            model: Some("new_model".to_string()),
408            small_fast_model: Some("new_fast_model".to_string()),
409            max_thinking_tokens: None,
410            api_timeout_ms: None,
411            claude_code_disable_nonessential_traffic: None,
412            anthropic_default_sonnet_model: None,
413            anthropic_default_opus_model: None,
414            anthropic_default_haiku_model: None,
415            claude_code_experimental_agent_teams: None,
416            claude_code_disable_1m_context: None,
417            claude_code_subagent_model: None,
418            claude_code_disable_nonstreaming_fallback: None,
419            claude_code_effort_level: None,
420            disable_prompt_caching: None,
421            claude_code_disable_experimental_betas: None,
422            disable_autoupdater: None,
423        };
424
425        // Switch to new configuration
426        settings.switch_to_config(&config);
427
428        // Verify the required fields are set correctly
429        assert_eq!(
430            settings.env.get("ANTHROPIC_AUTH_TOKEN"),
431            Some(&"new_token".to_string())
432        );
433        assert_eq!(
434            settings.env.get("ANTHROPIC_BASE_URL"),
435            Some(&"https://api.new.com".to_string())
436        );
437        assert_eq!(
438            settings.env.get("ANTHROPIC_MODEL"),
439            Some(&"new_model".to_string())
440        );
441        assert_eq!(
442            settings.env.get("ANTHROPIC_SMALL_FAST_MODEL"),
443            Some(&"new_fast_model".to_string())
444        );
445
446        // Verify optional fields are removed when not provided in config
447        assert!(!settings.env.contains_key("ANTHROPIC_MAX_THINKING_TOKENS"));
448        assert!(!settings.env.contains_key("API_TIMEOUT_MS"));
449        assert!(
450            !settings
451                .env
452                .contains_key("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")
453        );
454        assert!(!settings.env.contains_key("ANTHROPIC_DEFAULT_SONNET_MODEL"));
455        assert!(!settings.env.contains_key("ANTHROPIC_DEFAULT_OPUS_MODEL"));
456        assert!(!settings.env.contains_key("ANTHROPIC_DEFAULT_HAIKU_MODEL"));
457        assert!(!settings.env.contains_key("CLAUDE_CODE_SUBAGENT_MODEL"));
458        assert!(
459            !settings
460                .env
461                .contains_key("CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK")
462        );
463        assert!(!settings.env.contains_key("CLAUDE_CODE_EFFORT_LEVEL"));
464        assert!(!settings.env.contains_key("DISABLE_PROMPT_CACHING"));
465        assert!(
466            !settings
467                .env
468                .contains_key("CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS")
469        );
470        assert!(!settings.env.contains_key("DISABLE_AUTOUPDATER"));
471    }
472}
473
474/// Storage manager for Claude API configurations
475///
476/// Handles persistence and retrieval of multiple API configurations
477/// stored in `~/.cc_auto_switch/configurations.json`
478#[derive(Serialize, Deserialize, Default)]
479pub struct ConfigStorage {
480    /// Map of alias names to configuration objects
481    pub configurations: ConfigMap,
482    /// Custom directory for Claude settings (optional)
483    pub claude_settings_dir: Option<String>,
484    /// Default storage mode for writing configurations (None = use env mode)
485    #[serde(skip_serializing_if = "Option::is_none")]
486    pub default_storage_mode: Option<StorageMode>,
487    /// Codex (OpenAI) configurations, stored separately from Claude configurations
488    #[serde(skip_serializing_if = "Option::is_none")]
489    pub codex_configurations: Option<CodexConfigMap>,
490}
491
492/// Claude settings manager for API configuration
493///
494/// Manages the Claude settings.json file to control Claude's API configuration
495/// Handles environment variables and preserves other settings
496#[derive(Default, Clone)]
497#[allow(dead_code)]
498pub struct ClaudeSettings {
499    /// Environment variables map (ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, ANTHROPIC_MODEL, ANTHROPIC_SMALL_FAST_MODEL)
500    pub env: EnvMap,
501    /// Other settings to preserve when modifying API configuration
502    pub other: JsonMap,
503}
504
505impl Serialize for ClaudeSettings {
506    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
507    where
508        S: Serializer,
509    {
510        use serde::ser::SerializeMap;
511
512        let mut map = serializer.serialize_map(Some(
513            self.other.len() + if self.env.is_empty() { 0 } else { 1 },
514        ))?;
515
516        // Serialize env field only if it has content
517        if !self.env.is_empty() {
518            map.serialize_entry("env", &self.env)?;
519        }
520
521        // Serialize other fields
522        for (key, value) in &self.other {
523            map.serialize_entry(key, value)?;
524        }
525
526        map.end()
527    }
528}
529
530impl<'de> Deserialize<'de> for ClaudeSettings {
531    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
532    where
533        D: Deserializer<'de>,
534    {
535        #[derive(Deserialize)]
536        struct ClaudeSettingsHelper {
537            #[serde(default)]
538            env: EnvMap,
539            #[serde(flatten)]
540            other: JsonMap,
541        }
542
543        let helper = ClaudeSettingsHelper::deserialize(deserializer)?;
544        Ok(ClaudeSettings {
545            env: helper.env,
546            other: helper.other,
547        })
548    }
549}
550
551/// Parameters for adding a new configuration
552#[allow(dead_code)]
553pub struct AddCommandParams {
554    pub alias_name: String,
555    pub token: Option<String>,
556    pub url: Option<String>,
557    pub model: Option<String>,
558    pub small_fast_model: Option<String>,
559    pub max_thinking_tokens: Option<u32>,
560    pub api_timeout_ms: Option<u32>,
561    pub claude_code_disable_nonessential_traffic: Option<u32>,
562    pub anthropic_default_sonnet_model: Option<String>,
563    pub anthropic_default_opus_model: Option<String>,
564    pub anthropic_default_haiku_model: Option<String>,
565    pub claude_code_subagent_model: Option<String>,
566    pub claude_code_disable_nonstreaming_fallback: Option<u32>,
567    pub claude_code_effort_level: Option<String>,
568    pub disable_prompt_caching: Option<u32>,
569    pub claude_code_disable_experimental_betas: Option<u32>,
570    pub disable_autoupdater: Option<u32>,
571    pub force: bool,
572    pub interactive: bool,
573    pub token_arg: Option<String>,
574    pub url_arg: Option<String>,
575    pub from_file: Option<String>,
576}