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>;
10
11#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
13pub enum StorageMode {
14 #[serde(rename = "env")]
16 #[default]
17 Env,
18 #[serde(rename = "config")]
20 Config,
21}
22
23#[derive(Serialize, Deserialize, Default, Clone)]
32pub struct Configuration {
33 pub alias_name: String,
35 pub token: String,
37 pub url: String,
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub model: Option<String>,
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub small_fast_model: Option<String>,
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub max_thinking_tokens: Option<u32>,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub api_timeout_ms: Option<u32>,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub claude_code_disable_nonessential_traffic: Option<u32>,
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub anthropic_default_sonnet_model: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub anthropic_default_opus_model: Option<String>,
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub anthropic_default_haiku_model: Option<String>,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub claude_code_experimental_agent_teams: Option<u32>,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub claude_code_disable_1m_context: Option<u32>,
69}
70
71impl Configuration {
72 pub fn get_env_field_names() -> Vec<&'static str> {
78 vec![
79 "ANTHROPIC_AUTH_TOKEN",
80 "ANTHROPIC_BASE_URL",
81 "ANTHROPIC_MODEL",
82 "ANTHROPIC_SMALL_FAST_MODEL",
83 "ANTHROPIC_MAX_THINKING_TOKENS",
84 "API_TIMEOUT_MS",
85 "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
86 "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
87 "CLAUDE_CODE_DISABLE_1M_CONTEXT",
88 "ANTHROPIC_DEFAULT_SONNET_MODEL",
89 "ANTHROPIC_DEFAULT_OPUS_MODEL",
90 "ANTHROPIC_DEFAULT_HAIKU_MODEL",
91 ]
92 }
93
94 pub fn get_clearable_env_field_names() -> Vec<&'static str> {
100 vec![
101 "ANTHROPIC_AUTH_TOKEN",
102 "ANTHROPIC_BASE_URL",
103 "ANTHROPIC_MODEL",
104 "ANTHROPIC_SMALL_FAST_MODEL",
105 "ANTHROPIC_MAX_THINKING_TOKENS",
106 "API_TIMEOUT_MS",
107 "ANTHROPIC_DEFAULT_SONNET_MODEL",
108 "ANTHROPIC_DEFAULT_OPUS_MODEL",
109 "ANTHROPIC_DEFAULT_HAIKU_MODEL",
110 ]
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn test_get_env_field_names() {
124 let fields = Configuration::get_env_field_names();
125
126 let expected_fields = vec![
128 "ANTHROPIC_AUTH_TOKEN",
129 "ANTHROPIC_BASE_URL",
130 "ANTHROPIC_MODEL",
131 "ANTHROPIC_SMALL_FAST_MODEL",
132 "ANTHROPIC_MAX_THINKING_TOKENS",
133 "API_TIMEOUT_MS",
134 "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
135 "ANTHROPIC_DEFAULT_SONNET_MODEL",
136 "ANTHROPIC_DEFAULT_OPUS_MODEL",
137 "ANTHROPIC_DEFAULT_HAIKU_MODEL",
138 "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
139 "CLAUDE_CODE_DISABLE_1M_CONTEXT",
140 ];
141
142 assert_eq!(
143 fields.len(),
144 expected_fields.len(),
145 "Should have exactly 12 fields"
146 );
147
148 for expected_field in expected_fields {
149 assert!(
150 fields.contains(&expected_field),
151 "Missing field: {}",
152 expected_field
153 );
154 }
155
156 for field in &fields {
158 assert_eq!(
159 field,
160 &field.to_uppercase(),
161 "Field {} should be uppercase",
162 field
163 );
164 }
165 }
166
167 #[test]
168 fn test_get_clearable_env_field_names() {
169 let fields = Configuration::get_clearable_env_field_names();
170
171 let expected_fields = vec![
173 "ANTHROPIC_AUTH_TOKEN",
174 "ANTHROPIC_BASE_URL",
175 "ANTHROPIC_MODEL",
176 "ANTHROPIC_SMALL_FAST_MODEL",
177 "ANTHROPIC_MAX_THINKING_TOKENS",
178 "API_TIMEOUT_MS",
179 "ANTHROPIC_DEFAULT_SONNET_MODEL",
180 "ANTHROPIC_DEFAULT_OPUS_MODEL",
181 "ANTHROPIC_DEFAULT_HAIKU_MODEL",
182 ];
183
184 let excluded_fields = vec![
186 "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
187 "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
188 "CLAUDE_CODE_DISABLE_1M_CONTEXT",
189 ];
190
191 assert_eq!(
192 fields.len(),
193 expected_fields.len(),
194 "Should have exactly 9 clearable fields"
195 );
196
197 for expected_field in expected_fields {
198 assert!(
199 fields.contains(&expected_field),
200 "Missing clearable field: {}",
201 expected_field
202 );
203 }
204
205 for excluded_field in excluded_fields {
207 assert!(
208 !fields.contains(&excluded_field),
209 "User preference field {} should NOT be in clearable list",
210 excluded_field
211 );
212 }
213 }
214
215 #[test]
216 fn test_remove_anthropic_env_uses_dynamic_fields() {
217 let mut settings = ClaudeSettings::default();
218
219 let env_fields = Configuration::get_env_field_names();
221 for field in &env_fields {
222 settings
223 .env
224 .insert(field.to_string(), "test_value".to_string());
225 }
226
227 settings
229 .env
230 .insert("OTHER_VAR".to_string(), "other_value".to_string());
231 settings
232 .env
233 .insert("CLAUDE_THEME".to_string(), "dark".to_string());
234
235 settings.remove_anthropic_env();
237
238 for field in &env_fields {
240 assert!(
241 !settings.env.contains_key(*field),
242 "Field {} should be removed",
243 field
244 );
245 }
246
247 assert!(
249 settings.env.contains_key("OTHER_VAR"),
250 "Other variables should be preserved"
251 );
252 assert!(
253 settings.env.contains_key("CLAUDE_THEME"),
254 "Other variables should be preserved"
255 );
256 assert_eq!(
257 settings.env.get("OTHER_VAR"),
258 Some(&"other_value".to_string())
259 );
260 assert_eq!(settings.env.get("CLAUDE_THEME"), Some(&"dark".to_string()));
261 }
262
263 #[test]
264 fn test_switch_to_config_uses_dynamic_fields() {
265 let mut settings = ClaudeSettings::default();
266
267 let env_fields = Configuration::get_env_field_names();
269 for field in &env_fields {
270 settings
271 .env
272 .insert(field.to_string(), "old_value".to_string());
273 }
274
275 let config = Configuration {
277 alias_name: "test".to_string(),
278 token: "new_token".to_string(),
279 url: "https://api.new.com".to_string(),
280 model: Some("new_model".to_string()),
281 small_fast_model: Some("new_fast_model".to_string()),
282 max_thinking_tokens: Some(50000),
283 api_timeout_ms: Some(300000),
284 claude_code_disable_nonessential_traffic: Some(1),
285 anthropic_default_sonnet_model: Some("new_sonnet".to_string()),
286 anthropic_default_opus_model: Some("new_opus".to_string()),
287 anthropic_default_haiku_model: Some("new_haiku".to_string()),
288 claude_code_experimental_agent_teams: None,
289 claude_code_disable_1m_context: None,
290 };
291
292 settings.switch_to_config(&config);
294
295 assert_eq!(
297 settings.env.get("ANTHROPIC_AUTH_TOKEN"),
298 Some(&"new_token".to_string())
299 );
300 assert_eq!(
301 settings.env.get("ANTHROPIC_BASE_URL"),
302 Some(&"https://api.new.com".to_string())
303 );
304 assert_eq!(
305 settings.env.get("ANTHROPIC_MODEL"),
306 Some(&"new_model".to_string())
307 );
308 assert_eq!(
309 settings.env.get("ANTHROPIC_SMALL_FAST_MODEL"),
310 Some(&"new_fast_model".to_string())
311 );
312
313 assert_eq!(
315 settings.env.get("ANTHROPIC_MAX_THINKING_TOKENS"),
316 Some(&"50000".to_string())
317 );
318 assert_eq!(
319 settings.env.get("API_TIMEOUT_MS"),
320 Some(&"300000".to_string())
321 );
322 assert_eq!(
323 settings.env.get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"),
324 Some(&"1".to_string())
325 );
326 assert_eq!(
327 settings.env.get("ANTHROPIC_DEFAULT_SONNET_MODEL"),
328 Some(&"new_sonnet".to_string())
329 );
330 assert_eq!(
331 settings.env.get("ANTHROPIC_DEFAULT_OPUS_MODEL"),
332 Some(&"new_opus".to_string())
333 );
334 assert_eq!(
335 settings.env.get("ANTHROPIC_DEFAULT_HAIKU_MODEL"),
336 Some(&"new_haiku".to_string())
337 );
338 }
339
340 #[test]
341 fn test_switch_to_config_removes_optional_fields_when_not_provided() {
342 let mut settings = ClaudeSettings::default();
343
344 let env_fields = Configuration::get_env_field_names();
346 for field in &env_fields {
347 settings
348 .env
349 .insert(field.to_string(), "old_value".to_string());
350 }
351
352 let config = Configuration {
354 alias_name: "test".to_string(),
355 token: "new_token".to_string(),
356 url: "https://api.new.com".to_string(),
357 model: Some("new_model".to_string()),
358 small_fast_model: Some("new_fast_model".to_string()),
359 max_thinking_tokens: None,
360 api_timeout_ms: None,
361 claude_code_disable_nonessential_traffic: None,
362 anthropic_default_sonnet_model: None,
363 anthropic_default_opus_model: None,
364 anthropic_default_haiku_model: None,
365 claude_code_experimental_agent_teams: None,
366 claude_code_disable_1m_context: None,
367 };
368
369 settings.switch_to_config(&config);
371
372 assert_eq!(
374 settings.env.get("ANTHROPIC_AUTH_TOKEN"),
375 Some(&"new_token".to_string())
376 );
377 assert_eq!(
378 settings.env.get("ANTHROPIC_BASE_URL"),
379 Some(&"https://api.new.com".to_string())
380 );
381 assert_eq!(
382 settings.env.get("ANTHROPIC_MODEL"),
383 Some(&"new_model".to_string())
384 );
385 assert_eq!(
386 settings.env.get("ANTHROPIC_SMALL_FAST_MODEL"),
387 Some(&"new_fast_model".to_string())
388 );
389
390 assert!(!settings.env.contains_key("ANTHROPIC_MAX_THINKING_TOKENS"));
392 assert!(!settings.env.contains_key("API_TIMEOUT_MS"));
393 assert!(
394 !settings
395 .env
396 .contains_key("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")
397 );
398 assert!(!settings.env.contains_key("ANTHROPIC_DEFAULT_SONNET_MODEL"));
399 assert!(!settings.env.contains_key("ANTHROPIC_DEFAULT_OPUS_MODEL"));
400 assert!(!settings.env.contains_key("ANTHROPIC_DEFAULT_HAIKU_MODEL"));
401 }
402}
403
404#[derive(Serialize, Deserialize, Default)]
409pub struct ConfigStorage {
410 pub configurations: ConfigMap,
412 pub claude_settings_dir: Option<String>,
414 #[serde(skip_serializing_if = "Option::is_none")]
416 pub default_storage_mode: Option<StorageMode>,
417}
418
419#[derive(Default, Clone)]
424#[allow(dead_code)]
425pub struct ClaudeSettings {
426 pub env: EnvMap,
428 pub other: JsonMap,
430}
431
432impl Serialize for ClaudeSettings {
433 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
434 where
435 S: Serializer,
436 {
437 use serde::ser::SerializeMap;
438
439 let mut map = serializer.serialize_map(Some(
440 self.other.len() + if self.env.is_empty() { 0 } else { 1 },
441 ))?;
442
443 if !self.env.is_empty() {
445 map.serialize_entry("env", &self.env)?;
446 }
447
448 for (key, value) in &self.other {
450 map.serialize_entry(key, value)?;
451 }
452
453 map.end()
454 }
455}
456
457impl<'de> Deserialize<'de> for ClaudeSettings {
458 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
459 where
460 D: Deserializer<'de>,
461 {
462 #[derive(Deserialize)]
463 struct ClaudeSettingsHelper {
464 #[serde(default)]
465 env: EnvMap,
466 #[serde(flatten)]
467 other: JsonMap,
468 }
469
470 let helper = ClaudeSettingsHelper::deserialize(deserializer)?;
471 Ok(ClaudeSettings {
472 env: helper.env,
473 other: helper.other,
474 })
475 }
476}
477
478#[allow(dead_code)]
480pub struct AddCommandParams {
481 pub alias_name: String,
482 pub token: Option<String>,
483 pub url: Option<String>,
484 pub model: Option<String>,
485 pub small_fast_model: Option<String>,
486 pub max_thinking_tokens: Option<u32>,
487 pub api_timeout_ms: Option<u32>,
488 pub claude_code_disable_nonessential_traffic: Option<u32>,
489 pub anthropic_default_sonnet_model: Option<String>,
490 pub anthropic_default_opus_model: Option<String>,
491 pub anthropic_default_haiku_model: Option<String>,
492 pub force: bool,
493 pub interactive: bool,
494 pub token_arg: Option<String>,
495 pub url_arg: Option<String>,
496 pub from_file: Option<String>,
497}