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}
81
82impl Configuration {
83 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 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 ]
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 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 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 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 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 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 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 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 settings.remove_anthropic_env();
260
261 for field in &env_fields {
263 assert!(
264 !settings.env.contains_key(*field),
265 "Field {} should be removed",
266 field
267 );
268 }
269
270 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 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 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 settings.switch_to_config(&config);
320
321 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 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 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 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 settings.switch_to_config(&config);
400
401 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 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#[derive(Serialize, Deserialize, Default)]
445pub struct ConfigStorage {
446 pub configurations: ConfigMap,
448 pub claude_settings_dir: Option<String>,
450 #[serde(skip_serializing_if = "Option::is_none")]
452 pub default_storage_mode: Option<StorageMode>,
453 #[serde(skip_serializing_if = "Option::is_none")]
455 pub codex_configurations: Option<CodexConfigMap>,
456}
457
458#[derive(Default, Clone)]
463#[allow(dead_code)]
464pub struct ClaudeSettings {
465 pub env: EnvMap,
467 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 if !self.env.is_empty() {
484 map.serialize_entry("env", &self.env)?;
485 }
486
487 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#[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}