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 #[serde(skip_serializing_if = "Option::is_none")]
71 pub claude_code_subagent_model: Option<String>,
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub claude_code_disable_nonstreaming_fallback: Option<u32>,
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub claude_code_effort_level: Option<String>,
78}
79
80impl Configuration {
81 pub fn get_env_field_names() -> Vec<&'static str> {
87 vec![
88 "ANTHROPIC_AUTH_TOKEN",
89 "ANTHROPIC_BASE_URL",
90 "ANTHROPIC_MODEL",
91 "ANTHROPIC_SMALL_FAST_MODEL",
92 "ANTHROPIC_MAX_THINKING_TOKENS",
93 "API_TIMEOUT_MS",
94 "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
95 "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
96 "CLAUDE_CODE_DISABLE_1M_CONTEXT",
97 "CLAUDE_CODE_SUBAGENT_MODEL",
98 "CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK",
99 "CLAUDE_CODE_EFFORT_LEVEL",
100 "ANTHROPIC_DEFAULT_SONNET_MODEL",
101 "ANTHROPIC_DEFAULT_OPUS_MODEL",
102 "ANTHROPIC_DEFAULT_HAIKU_MODEL",
103 ]
104 }
105
106 pub fn get_clearable_env_field_names() -> Vec<&'static str> {
112 vec![
113 "ANTHROPIC_AUTH_TOKEN",
114 "ANTHROPIC_BASE_URL",
115 "ANTHROPIC_MODEL",
116 "ANTHROPIC_SMALL_FAST_MODEL",
117 "ANTHROPIC_MAX_THINKING_TOKENS",
118 "API_TIMEOUT_MS",
119 "ANTHROPIC_DEFAULT_SONNET_MODEL",
120 "ANTHROPIC_DEFAULT_OPUS_MODEL",
121 "ANTHROPIC_DEFAULT_HAIKU_MODEL",
122 "CLAUDE_CODE_SUBAGENT_MODEL",
123 "CLAUDE_CODE_EFFORT_LEVEL",
124 ]
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[test]
138 fn test_get_env_field_names() {
139 let fields = Configuration::get_env_field_names();
140
141 let expected_fields = vec![
143 "ANTHROPIC_AUTH_TOKEN",
144 "ANTHROPIC_BASE_URL",
145 "ANTHROPIC_MODEL",
146 "ANTHROPIC_SMALL_FAST_MODEL",
147 "ANTHROPIC_MAX_THINKING_TOKENS",
148 "API_TIMEOUT_MS",
149 "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
150 "ANTHROPIC_DEFAULT_SONNET_MODEL",
151 "ANTHROPIC_DEFAULT_OPUS_MODEL",
152 "ANTHROPIC_DEFAULT_HAIKU_MODEL",
153 "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
154 "CLAUDE_CODE_DISABLE_1M_CONTEXT",
155 "CLAUDE_CODE_SUBAGENT_MODEL",
156 "CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK",
157 "CLAUDE_CODE_EFFORT_LEVEL",
158 ];
159
160 assert_eq!(
161 fields.len(),
162 expected_fields.len(),
163 "Should have exactly 15 fields"
164 );
165
166 for expected_field in expected_fields {
167 assert!(
168 fields.contains(&expected_field),
169 "Missing field: {}",
170 expected_field
171 );
172 }
173
174 for field in &fields {
176 assert_eq!(
177 field,
178 &field.to_uppercase(),
179 "Field {} should be uppercase",
180 field
181 );
182 }
183 }
184
185 #[test]
186 fn test_get_clearable_env_field_names() {
187 let fields = Configuration::get_clearable_env_field_names();
188
189 let expected_fields = vec![
191 "ANTHROPIC_AUTH_TOKEN",
192 "ANTHROPIC_BASE_URL",
193 "ANTHROPIC_MODEL",
194 "ANTHROPIC_SMALL_FAST_MODEL",
195 "ANTHROPIC_MAX_THINKING_TOKENS",
196 "API_TIMEOUT_MS",
197 "ANTHROPIC_DEFAULT_SONNET_MODEL",
198 "ANTHROPIC_DEFAULT_OPUS_MODEL",
199 "ANTHROPIC_DEFAULT_HAIKU_MODEL",
200 "CLAUDE_CODE_SUBAGENT_MODEL",
201 "CLAUDE_CODE_EFFORT_LEVEL",
202 ];
203
204 let excluded_fields = vec![
206 "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
207 "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
208 "CLAUDE_CODE_DISABLE_1M_CONTEXT",
209 "CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK",
210 ];
211
212 assert_eq!(
213 fields.len(),
214 expected_fields.len(),
215 "Should have exactly 11 clearable fields"
216 );
217
218 for expected_field in expected_fields {
219 assert!(
220 fields.contains(&expected_field),
221 "Missing clearable field: {}",
222 expected_field
223 );
224 }
225
226 for excluded_field in excluded_fields {
228 assert!(
229 !fields.contains(&excluded_field),
230 "User preference field {} should NOT be in clearable list",
231 excluded_field
232 );
233 }
234 }
235
236 #[test]
237 fn test_remove_anthropic_env_uses_dynamic_fields() {
238 let mut settings = ClaudeSettings::default();
239
240 let env_fields = Configuration::get_env_field_names();
242 for field in &env_fields {
243 settings
244 .env
245 .insert(field.to_string(), "test_value".to_string());
246 }
247
248 settings
250 .env
251 .insert("OTHER_VAR".to_string(), "other_value".to_string());
252 settings
253 .env
254 .insert("CLAUDE_THEME".to_string(), "dark".to_string());
255
256 settings.remove_anthropic_env();
258
259 for field in &env_fields {
261 assert!(
262 !settings.env.contains_key(*field),
263 "Field {} should be removed",
264 field
265 );
266 }
267
268 assert!(
270 settings.env.contains_key("OTHER_VAR"),
271 "Other variables should be preserved"
272 );
273 assert!(
274 settings.env.contains_key("CLAUDE_THEME"),
275 "Other variables should be preserved"
276 );
277 assert_eq!(
278 settings.env.get("OTHER_VAR"),
279 Some(&"other_value".to_string())
280 );
281 assert_eq!(settings.env.get("CLAUDE_THEME"), Some(&"dark".to_string()));
282 }
283
284 #[test]
285 fn test_switch_to_config_uses_dynamic_fields() {
286 let mut settings = ClaudeSettings::default();
287
288 let env_fields = Configuration::get_env_field_names();
290 for field in &env_fields {
291 settings
292 .env
293 .insert(field.to_string(), "old_value".to_string());
294 }
295
296 let config = Configuration {
298 alias_name: "test".to_string(),
299 token: "new_token".to_string(),
300 url: "https://api.new.com".to_string(),
301 model: Some("new_model".to_string()),
302 small_fast_model: Some("new_fast_model".to_string()),
303 max_thinking_tokens: Some(50000),
304 api_timeout_ms: Some(300000),
305 claude_code_disable_nonessential_traffic: Some(1),
306 anthropic_default_sonnet_model: Some("new_sonnet".to_string()),
307 anthropic_default_opus_model: Some("new_opus".to_string()),
308 anthropic_default_haiku_model: Some("new_haiku".to_string()),
309 claude_code_experimental_agent_teams: None,
310 claude_code_disable_1m_context: None,
311 claude_code_subagent_model: None,
312 claude_code_disable_nonstreaming_fallback: None,
313 claude_code_effort_level: None,
314 };
315
316 settings.switch_to_config(&config);
318
319 assert_eq!(
321 settings.env.get("ANTHROPIC_AUTH_TOKEN"),
322 Some(&"new_token".to_string())
323 );
324 assert_eq!(
325 settings.env.get("ANTHROPIC_BASE_URL"),
326 Some(&"https://api.new.com".to_string())
327 );
328 assert_eq!(
329 settings.env.get("ANTHROPIC_MODEL"),
330 Some(&"new_model".to_string())
331 );
332 assert_eq!(
333 settings.env.get("ANTHROPIC_SMALL_FAST_MODEL"),
334 Some(&"new_fast_model".to_string())
335 );
336
337 assert_eq!(
339 settings.env.get("ANTHROPIC_MAX_THINKING_TOKENS"),
340 Some(&"50000".to_string())
341 );
342 assert_eq!(
343 settings.env.get("API_TIMEOUT_MS"),
344 Some(&"300000".to_string())
345 );
346 assert_eq!(
347 settings.env.get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"),
348 Some(&"1".to_string())
349 );
350 assert_eq!(
351 settings.env.get("ANTHROPIC_DEFAULT_SONNET_MODEL"),
352 Some(&"new_sonnet".to_string())
353 );
354 assert_eq!(
355 settings.env.get("ANTHROPIC_DEFAULT_OPUS_MODEL"),
356 Some(&"new_opus".to_string())
357 );
358 assert_eq!(
359 settings.env.get("ANTHROPIC_DEFAULT_HAIKU_MODEL"),
360 Some(&"new_haiku".to_string())
361 );
362 }
363
364 #[test]
365 fn test_switch_to_config_removes_optional_fields_when_not_provided() {
366 let mut settings = ClaudeSettings::default();
367
368 let env_fields = Configuration::get_env_field_names();
370 for field in &env_fields {
371 settings
372 .env
373 .insert(field.to_string(), "old_value".to_string());
374 }
375
376 let config = Configuration {
378 alias_name: "test".to_string(),
379 token: "new_token".to_string(),
380 url: "https://api.new.com".to_string(),
381 model: Some("new_model".to_string()),
382 small_fast_model: Some("new_fast_model".to_string()),
383 max_thinking_tokens: None,
384 api_timeout_ms: None,
385 claude_code_disable_nonessential_traffic: None,
386 anthropic_default_sonnet_model: None,
387 anthropic_default_opus_model: None,
388 anthropic_default_haiku_model: None,
389 claude_code_experimental_agent_teams: None,
390 claude_code_disable_1m_context: None,
391 claude_code_subagent_model: None,
392 claude_code_disable_nonstreaming_fallback: None,
393 claude_code_effort_level: None,
394 };
395
396 settings.switch_to_config(&config);
398
399 assert_eq!(
401 settings.env.get("ANTHROPIC_AUTH_TOKEN"),
402 Some(&"new_token".to_string())
403 );
404 assert_eq!(
405 settings.env.get("ANTHROPIC_BASE_URL"),
406 Some(&"https://api.new.com".to_string())
407 );
408 assert_eq!(
409 settings.env.get("ANTHROPIC_MODEL"),
410 Some(&"new_model".to_string())
411 );
412 assert_eq!(
413 settings.env.get("ANTHROPIC_SMALL_FAST_MODEL"),
414 Some(&"new_fast_model".to_string())
415 );
416
417 assert!(!settings.env.contains_key("ANTHROPIC_MAX_THINKING_TOKENS"));
419 assert!(!settings.env.contains_key("API_TIMEOUT_MS"));
420 assert!(
421 !settings
422 .env
423 .contains_key("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")
424 );
425 assert!(!settings.env.contains_key("ANTHROPIC_DEFAULT_SONNET_MODEL"));
426 assert!(!settings.env.contains_key("ANTHROPIC_DEFAULT_OPUS_MODEL"));
427 assert!(!settings.env.contains_key("ANTHROPIC_DEFAULT_HAIKU_MODEL"));
428 assert!(!settings.env.contains_key("CLAUDE_CODE_SUBAGENT_MODEL"));
429 assert!(
430 !settings
431 .env
432 .contains_key("CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK")
433 );
434 assert!(!settings.env.contains_key("CLAUDE_CODE_EFFORT_LEVEL"));
435 }
436}
437
438#[derive(Serialize, Deserialize, Default)]
443pub struct ConfigStorage {
444 pub configurations: ConfigMap,
446 pub claude_settings_dir: Option<String>,
448 #[serde(skip_serializing_if = "Option::is_none")]
450 pub default_storage_mode: Option<StorageMode>,
451}
452
453#[derive(Default, Clone)]
458#[allow(dead_code)]
459pub struct ClaudeSettings {
460 pub env: EnvMap,
462 pub other: JsonMap,
464}
465
466impl Serialize for ClaudeSettings {
467 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
468 where
469 S: Serializer,
470 {
471 use serde::ser::SerializeMap;
472
473 let mut map = serializer.serialize_map(Some(
474 self.other.len() + if self.env.is_empty() { 0 } else { 1 },
475 ))?;
476
477 if !self.env.is_empty() {
479 map.serialize_entry("env", &self.env)?;
480 }
481
482 for (key, value) in &self.other {
484 map.serialize_entry(key, value)?;
485 }
486
487 map.end()
488 }
489}
490
491impl<'de> Deserialize<'de> for ClaudeSettings {
492 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
493 where
494 D: Deserializer<'de>,
495 {
496 #[derive(Deserialize)]
497 struct ClaudeSettingsHelper {
498 #[serde(default)]
499 env: EnvMap,
500 #[serde(flatten)]
501 other: JsonMap,
502 }
503
504 let helper = ClaudeSettingsHelper::deserialize(deserializer)?;
505 Ok(ClaudeSettings {
506 env: helper.env,
507 other: helper.other,
508 })
509 }
510}
511
512#[allow(dead_code)]
514pub struct AddCommandParams {
515 pub alias_name: String,
516 pub token: Option<String>,
517 pub url: Option<String>,
518 pub model: Option<String>,
519 pub small_fast_model: Option<String>,
520 pub max_thinking_tokens: Option<u32>,
521 pub api_timeout_ms: Option<u32>,
522 pub claude_code_disable_nonessential_traffic: Option<u32>,
523 pub anthropic_default_sonnet_model: Option<String>,
524 pub anthropic_default_opus_model: Option<String>,
525 pub anthropic_default_haiku_model: Option<String>,
526 pub claude_code_subagent_model: Option<String>,
527 pub claude_code_disable_nonstreaming_fallback: Option<u32>,
528 pub claude_code_effort_level: Option<String>,
529 pub force: bool,
530 pub interactive: bool,
531 pub token_arg: Option<String>,
532 pub url_arg: Option<String>,
533 pub from_file: Option<String>,
534}