use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::BTreeMap;
type ConfigMap = BTreeMap<String, Configuration>;
type EnvMap = BTreeMap<String, String>;
type JsonMap = BTreeMap<String, serde_json::Value>;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
pub enum StorageMode {
#[serde(rename = "env")]
#[default]
Env,
#[serde(rename = "config")]
Config,
}
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct Configuration {
pub alias_name: String,
pub token: String,
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub small_fast_model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_thinking_tokens: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub api_timeout_ms: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub claude_code_disable_nonessential_traffic: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub anthropic_default_sonnet_model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub anthropic_default_opus_model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub anthropic_default_haiku_model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub claude_code_experimental_agent_teams: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub claude_code_disable_1m_context: Option<u32>,
}
impl Configuration {
pub fn get_env_field_names() -> Vec<&'static str> {
vec![
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_BASE_URL",
"ANTHROPIC_MODEL",
"ANTHROPIC_SMALL_FAST_MODEL",
"ANTHROPIC_MAX_THINKING_TOKENS",
"API_TIMEOUT_MS",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
"CLAUDE_CODE_DISABLE_1M_CONTEXT",
"ANTHROPIC_DEFAULT_SONNET_MODEL",
"ANTHROPIC_DEFAULT_OPUS_MODEL",
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
]
}
pub fn get_clearable_env_field_names() -> Vec<&'static str> {
vec![
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_BASE_URL",
"ANTHROPIC_MODEL",
"ANTHROPIC_SMALL_FAST_MODEL",
"ANTHROPIC_MAX_THINKING_TOKENS",
"API_TIMEOUT_MS",
"ANTHROPIC_DEFAULT_SONNET_MODEL",
"ANTHROPIC_DEFAULT_OPUS_MODEL",
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_env_field_names() {
let fields = Configuration::get_env_field_names();
let expected_fields = vec![
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_BASE_URL",
"ANTHROPIC_MODEL",
"ANTHROPIC_SMALL_FAST_MODEL",
"ANTHROPIC_MAX_THINKING_TOKENS",
"API_TIMEOUT_MS",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
"ANTHROPIC_DEFAULT_SONNET_MODEL",
"ANTHROPIC_DEFAULT_OPUS_MODEL",
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
"CLAUDE_CODE_DISABLE_1M_CONTEXT",
];
assert_eq!(
fields.len(),
expected_fields.len(),
"Should have exactly 12 fields"
);
for expected_field in expected_fields {
assert!(
fields.contains(&expected_field),
"Missing field: {}",
expected_field
);
}
for field in &fields {
assert_eq!(
field,
&field.to_uppercase(),
"Field {} should be uppercase",
field
);
}
}
#[test]
fn test_get_clearable_env_field_names() {
let fields = Configuration::get_clearable_env_field_names();
let expected_fields = vec![
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_BASE_URL",
"ANTHROPIC_MODEL",
"ANTHROPIC_SMALL_FAST_MODEL",
"ANTHROPIC_MAX_THINKING_TOKENS",
"API_TIMEOUT_MS",
"ANTHROPIC_DEFAULT_SONNET_MODEL",
"ANTHROPIC_DEFAULT_OPUS_MODEL",
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
];
let excluded_fields = vec![
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC",
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS",
"CLAUDE_CODE_DISABLE_1M_CONTEXT",
];
assert_eq!(
fields.len(),
expected_fields.len(),
"Should have exactly 9 clearable fields"
);
for expected_field in expected_fields {
assert!(
fields.contains(&expected_field),
"Missing clearable field: {}",
expected_field
);
}
for excluded_field in excluded_fields {
assert!(
!fields.contains(&excluded_field),
"User preference field {} should NOT be in clearable list",
excluded_field
);
}
}
#[test]
fn test_remove_anthropic_env_uses_dynamic_fields() {
let mut settings = ClaudeSettings::default();
let env_fields = Configuration::get_env_field_names();
for field in &env_fields {
settings
.env
.insert(field.to_string(), "test_value".to_string());
}
settings
.env
.insert("OTHER_VAR".to_string(), "other_value".to_string());
settings
.env
.insert("CLAUDE_THEME".to_string(), "dark".to_string());
settings.remove_anthropic_env();
for field in &env_fields {
assert!(
!settings.env.contains_key(*field),
"Field {} should be removed",
field
);
}
assert!(
settings.env.contains_key("OTHER_VAR"),
"Other variables should be preserved"
);
assert!(
settings.env.contains_key("CLAUDE_THEME"),
"Other variables should be preserved"
);
assert_eq!(
settings.env.get("OTHER_VAR"),
Some(&"other_value".to_string())
);
assert_eq!(settings.env.get("CLAUDE_THEME"), Some(&"dark".to_string()));
}
#[test]
fn test_switch_to_config_uses_dynamic_fields() {
let mut settings = ClaudeSettings::default();
let env_fields = Configuration::get_env_field_names();
for field in &env_fields {
settings
.env
.insert(field.to_string(), "old_value".to_string());
}
let config = Configuration {
alias_name: "test".to_string(),
token: "new_token".to_string(),
url: "https://api.new.com".to_string(),
model: Some("new_model".to_string()),
small_fast_model: Some("new_fast_model".to_string()),
max_thinking_tokens: Some(50000),
api_timeout_ms: Some(300000),
claude_code_disable_nonessential_traffic: Some(1),
anthropic_default_sonnet_model: Some("new_sonnet".to_string()),
anthropic_default_opus_model: Some("new_opus".to_string()),
anthropic_default_haiku_model: Some("new_haiku".to_string()),
claude_code_experimental_agent_teams: None,
claude_code_disable_1m_context: None,
};
settings.switch_to_config(&config);
assert_eq!(
settings.env.get("ANTHROPIC_AUTH_TOKEN"),
Some(&"new_token".to_string())
);
assert_eq!(
settings.env.get("ANTHROPIC_BASE_URL"),
Some(&"https://api.new.com".to_string())
);
assert_eq!(
settings.env.get("ANTHROPIC_MODEL"),
Some(&"new_model".to_string())
);
assert_eq!(
settings.env.get("ANTHROPIC_SMALL_FAST_MODEL"),
Some(&"new_fast_model".to_string())
);
assert_eq!(
settings.env.get("ANTHROPIC_MAX_THINKING_TOKENS"),
Some(&"50000".to_string())
);
assert_eq!(
settings.env.get("API_TIMEOUT_MS"),
Some(&"300000".to_string())
);
assert_eq!(
settings.env.get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"),
Some(&"1".to_string())
);
assert_eq!(
settings.env.get("ANTHROPIC_DEFAULT_SONNET_MODEL"),
Some(&"new_sonnet".to_string())
);
assert_eq!(
settings.env.get("ANTHROPIC_DEFAULT_OPUS_MODEL"),
Some(&"new_opus".to_string())
);
assert_eq!(
settings.env.get("ANTHROPIC_DEFAULT_HAIKU_MODEL"),
Some(&"new_haiku".to_string())
);
}
#[test]
fn test_switch_to_config_removes_optional_fields_when_not_provided() {
let mut settings = ClaudeSettings::default();
let env_fields = Configuration::get_env_field_names();
for field in &env_fields {
settings
.env
.insert(field.to_string(), "old_value".to_string());
}
let config = Configuration {
alias_name: "test".to_string(),
token: "new_token".to_string(),
url: "https://api.new.com".to_string(),
model: Some("new_model".to_string()),
small_fast_model: Some("new_fast_model".to_string()),
max_thinking_tokens: None,
api_timeout_ms: None,
claude_code_disable_nonessential_traffic: None,
anthropic_default_sonnet_model: None,
anthropic_default_opus_model: None,
anthropic_default_haiku_model: None,
claude_code_experimental_agent_teams: None,
claude_code_disable_1m_context: None,
};
settings.switch_to_config(&config);
assert_eq!(
settings.env.get("ANTHROPIC_AUTH_TOKEN"),
Some(&"new_token".to_string())
);
assert_eq!(
settings.env.get("ANTHROPIC_BASE_URL"),
Some(&"https://api.new.com".to_string())
);
assert_eq!(
settings.env.get("ANTHROPIC_MODEL"),
Some(&"new_model".to_string())
);
assert_eq!(
settings.env.get("ANTHROPIC_SMALL_FAST_MODEL"),
Some(&"new_fast_model".to_string())
);
assert!(!settings.env.contains_key("ANTHROPIC_MAX_THINKING_TOKENS"));
assert!(!settings.env.contains_key("API_TIMEOUT_MS"));
assert!(
!settings
.env
.contains_key("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")
);
assert!(!settings.env.contains_key("ANTHROPIC_DEFAULT_SONNET_MODEL"));
assert!(!settings.env.contains_key("ANTHROPIC_DEFAULT_OPUS_MODEL"));
assert!(!settings.env.contains_key("ANTHROPIC_DEFAULT_HAIKU_MODEL"));
}
}
#[derive(Serialize, Deserialize, Default)]
pub struct ConfigStorage {
pub configurations: ConfigMap,
pub claude_settings_dir: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_storage_mode: Option<StorageMode>,
}
#[derive(Default, Clone)]
#[allow(dead_code)]
pub struct ClaudeSettings {
pub env: EnvMap,
pub other: JsonMap,
}
impl Serialize for ClaudeSettings {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(
self.other.len() + if self.env.is_empty() { 0 } else { 1 },
))?;
if !self.env.is_empty() {
map.serialize_entry("env", &self.env)?;
}
for (key, value) in &self.other {
map.serialize_entry(key, value)?;
}
map.end()
}
}
impl<'de> Deserialize<'de> for ClaudeSettings {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct ClaudeSettingsHelper {
#[serde(default)]
env: EnvMap,
#[serde(flatten)]
other: JsonMap,
}
let helper = ClaudeSettingsHelper::deserialize(deserializer)?;
Ok(ClaudeSettings {
env: helper.env,
other: helper.other,
})
}
}
#[allow(dead_code)]
pub struct AddCommandParams {
pub alias_name: String,
pub token: Option<String>,
pub url: Option<String>,
pub model: Option<String>,
pub small_fast_model: Option<String>,
pub max_thinking_tokens: Option<u32>,
pub api_timeout_ms: Option<u32>,
pub claude_code_disable_nonessential_traffic: Option<u32>,
pub anthropic_default_sonnet_model: Option<String>,
pub anthropic_default_opus_model: Option<String>,
pub anthropic_default_haiku_model: Option<String>,
pub force: bool,
pub interactive: bool,
pub token_arg: Option<String>,
pub url_arg: Option<String>,
pub from_file: Option<String>,
}