1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::collections::BTreeMap;
3
4type ConfigMap = BTreeMap<String, Configuration>;
6type EnvMap = BTreeMap<String, String>;
8type JsonMap = BTreeMap<String, serde_json::Value>;
10type CodexConfigMap = BTreeMap<String, crate::codex::CodexConfiguration>;
12
13#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
15pub enum StorageMode {
16 #[serde(rename = "env")]
18 #[default]
19 Env,
20 #[serde(rename = "config")]
22 Config,
23}
24
25#[derive(Serialize, Deserialize, Default, Clone)]
34pub struct Configuration {
35 pub alias_name: String,
37 pub token: String,
39 pub url: String,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub model: Option<String>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub small_fast_model: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub max_thinking_tokens: Option<u32>,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub api_timeout_ms: Option<u32>,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub claude_code_disable_nonessential_traffic: Option<u32>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub anthropic_default_sonnet_model: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub anthropic_default_opus_model: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub anthropic_default_haiku_model: Option<String>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub claude_code_experimental_agent_teams: Option<u32>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub claude_code_disable_1m_context: Option<u32>,
71 #[serde(skip_serializing_if = "Option::is_none")]
73 pub claude_code_subagent_model: Option<String>,
74 #[serde(skip_serializing_if = "Option::is_none")]
76 pub claude_code_disable_nonstreaming_fallback: Option<u32>,
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub claude_code_effort_level: Option<String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub disable_prompt_caching: Option<u32>,
83 #[serde(skip_serializing_if = "Option::is_none")]
85 pub claude_code_disable_experimental_betas: Option<u32>,
86 #[serde(skip_serializing_if = "Option::is_none")]
88 pub disable_autoupdater: Option<u32>,
89}
90
91impl Configuration {
92 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 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 ]
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 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 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 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 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 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 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 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 settings.remove_anthropic_env();
281
282 for field in &env_fields {
284 assert!(
285 !settings.env.contains_key(*field),
286 "Field {} should be removed",
287 field
288 );
289 }
290
291 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 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 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 settings.switch_to_config(&config);
344
345 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 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 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 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 settings.switch_to_config(&config);
427
428 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 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#[derive(Serialize, Deserialize, Default)]
479pub struct ConfigStorage {
480 pub configurations: ConfigMap,
482 pub claude_settings_dir: Option<String>,
484 #[serde(skip_serializing_if = "Option::is_none")]
486 pub default_storage_mode: Option<StorageMode>,
487 #[serde(skip_serializing_if = "Option::is_none")]
489 pub codex_configurations: Option<CodexConfigMap>,
490}
491
492#[derive(Default, Clone)]
497#[allow(dead_code)]
498pub struct ClaudeSettings {
499 pub env: EnvMap,
501 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 if !self.env.is_empty() {
518 map.serialize_entry("env", &self.env)?;
519 }
520
521 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#[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}