claude_agent/config/
cloud.rs

1//! Cloud provider environment configuration.
2
3use std::collections::HashMap;
4use std::env;
5
6use crate::client::messages::{DEFAULT_MAX_TOKENS, MIN_THINKING_BUDGET};
7
8#[derive(Clone, Debug, Default)]
9pub struct CloudConfig {
10    pub provider: ProviderSelection,
11    pub tokens: TokenLimits,
12    pub caching: CacheConfig,
13    pub gateway: GatewayOptions,
14}
15
16#[derive(Clone, Debug, Default)]
17pub struct ProviderSelection {
18    pub use_bedrock: bool,
19    pub use_vertex: bool,
20    pub use_foundry: bool,
21}
22
23#[derive(Clone, Debug)]
24pub struct TokenLimits {
25    pub max_output: u32,
26    pub max_thinking: u32,
27}
28
29impl Default for TokenLimits {
30    fn default() -> Self {
31        Self {
32            max_output: DEFAULT_MAX_TOKENS,
33            max_thinking: MIN_THINKING_BUDGET,
34        }
35    }
36}
37
38#[derive(Clone, Debug, Default)]
39pub struct CacheConfig {
40    pub disable_prompt_caching: bool,
41}
42
43#[derive(Clone, Debug, Default)]
44pub struct GatewayOptions {
45    pub disable_experimental_betas: bool,
46}
47
48#[derive(Clone, Debug)]
49pub struct BedrockConfig {
50    pub region: Option<String>,
51    pub small_model_region: Option<String>,
52    pub bearer_token: Option<String>,
53    pub auth_refresh_cmd: Option<String>,
54    pub credential_export_cmd: Option<String>,
55    /// Use global endpoint (global.anthropic.*) for maximum availability.
56    /// Recommended for most use cases. Set to false for regional (CRIS) endpoints.
57    pub use_global_endpoint: bool,
58    /// Enable 1M context window beta feature (context-1m-2025-08-07).
59    pub enable_1m_context: bool,
60}
61
62impl Default for BedrockConfig {
63    fn default() -> Self {
64        Self {
65            region: None,
66            small_model_region: None,
67            bearer_token: None,
68            auth_refresh_cmd: None,
69            credential_export_cmd: None,
70            use_global_endpoint: true, // Global endpoint is recommended
71            enable_1m_context: false,
72        }
73    }
74}
75
76#[derive(Clone, Debug, Default)]
77pub struct VertexConfig {
78    pub project_id: Option<String>,
79    pub region: Option<String>,
80    pub model_regions: HashMap<String, String>,
81    pub enable_1m_context: bool,
82}
83
84#[derive(Clone, Debug, Default)]
85pub struct FoundryConfig {
86    pub resource: Option<String>,
87    /// Alternative to resource: full base URL (e.g., `https://example-resource.services.ai.azure.com/anthropic/`)
88    pub base_url: Option<String>,
89    pub api_key: Option<String>,
90}
91
92impl CloudConfig {
93    pub fn from_env() -> Self {
94        Self {
95            provider: ProviderSelection::from_env(),
96            tokens: TokenLimits::from_env(),
97            caching: CacheConfig::from_env(),
98            gateway: GatewayOptions::from_env(),
99        }
100    }
101
102    pub fn active_provider(&self) -> Option<&'static str> {
103        if self.provider.use_bedrock {
104            Some("bedrock")
105        } else if self.provider.use_vertex {
106            Some("vertex")
107        } else if self.provider.use_foundry {
108            Some("foundry")
109        } else {
110            None
111        }
112    }
113}
114
115impl ProviderSelection {
116    pub fn from_env() -> Self {
117        Self {
118            use_bedrock: is_flag_set("CLAUDE_CODE_USE_BEDROCK"),
119            use_vertex: is_flag_set("CLAUDE_CODE_USE_VERTEX"),
120            use_foundry: is_flag_set("CLAUDE_CODE_USE_FOUNDRY"),
121        }
122    }
123}
124
125impl TokenLimits {
126    pub fn from_env() -> Self {
127        Self {
128            max_output: parse_env("CLAUDE_CODE_MAX_OUTPUT_TOKENS").unwrap_or(DEFAULT_MAX_TOKENS),
129            max_thinking: parse_env("MAX_THINKING_TOKENS")
130                .unwrap_or(MIN_THINKING_BUDGET)
131                .max(MIN_THINKING_BUDGET),
132        }
133    }
134}
135
136impl CacheConfig {
137    pub fn from_env() -> Self {
138        Self {
139            disable_prompt_caching: is_flag_set("DISABLE_PROMPT_CACHING"),
140        }
141    }
142}
143
144impl GatewayOptions {
145    pub fn from_env() -> Self {
146        Self {
147            disable_experimental_betas: is_flag_set("CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS"),
148        }
149    }
150}
151
152impl BedrockConfig {
153    pub fn from_env() -> Self {
154        Self {
155            region: env::var("AWS_REGION").ok(),
156            small_model_region: env::var("ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION").ok(),
157            bearer_token: env::var("AWS_BEARER_TOKEN_BEDROCK").ok(),
158            auth_refresh_cmd: None, // Loaded from settings.json
159            credential_export_cmd: None,
160            use_global_endpoint: !is_flag_set("BEDROCK_USE_REGIONAL_ENDPOINT"),
161            enable_1m_context: is_flag_set("BEDROCK_ENABLE_1M_CONTEXT"),
162        }
163    }
164
165    /// Builder method to set use_global_endpoint.
166    pub fn with_global_endpoint(mut self, enable: bool) -> Self {
167        self.use_global_endpoint = enable;
168        self
169    }
170
171    /// Builder method to enable 1M context.
172    pub fn with_1m_context(mut self, enable: bool) -> Self {
173        self.enable_1m_context = enable;
174        self
175    }
176}
177
178impl VertexConfig {
179    pub fn from_env() -> Self {
180        let mut model_regions = HashMap::new();
181
182        let region_vars = [
183            ("VERTEX_REGION_CLAUDE_3_5_HAIKU", "claude-3-5-haiku"),
184            ("VERTEX_REGION_CLAUDE_3_5_SONNET", "claude-3-5-sonnet"),
185            ("VERTEX_REGION_CLAUDE_3_7_SONNET", "claude-3-7-sonnet"),
186            ("VERTEX_REGION_CLAUDE_4_0_OPUS", "claude-4-0-opus"),
187            ("VERTEX_REGION_CLAUDE_4_0_SONNET", "claude-4-0-sonnet"),
188            ("VERTEX_REGION_CLAUDE_4_1_OPUS", "claude-4-1-opus"),
189            ("VERTEX_REGION_CLAUDE_4_5_SONNET", "claude-4-5-sonnet"),
190            ("VERTEX_REGION_CLAUDE_4_5_HAIKU", "claude-4-5-haiku"),
191        ];
192
193        for (env_var, model_key) in region_vars {
194            if let Ok(region) = env::var(env_var) {
195                model_regions.insert(model_key.to_string(), region);
196            }
197        }
198
199        Self {
200            project_id: env::var("ANTHROPIC_VERTEX_PROJECT_ID")
201                .or_else(|_| env::var("GOOGLE_CLOUD_PROJECT"))
202                .or_else(|_| env::var("GCLOUD_PROJECT"))
203                .ok(),
204            region: env::var("CLOUD_ML_REGION")
205                .or_else(|_| env::var("GOOGLE_CLOUD_REGION"))
206                .ok(),
207            model_regions,
208            enable_1m_context: is_flag_set("VERTEX_ENABLE_1M_CONTEXT"),
209        }
210    }
211
212    pub fn region_for_model(&self, model: &str) -> Option<&str> {
213        for (key, region) in &self.model_regions {
214            if model.contains(key) {
215                return Some(region);
216            }
217        }
218        self.region.as_deref()
219    }
220
221    pub fn is_global(&self) -> bool {
222        self.region.as_deref() == Some("global")
223    }
224}
225
226impl FoundryConfig {
227    pub fn from_env() -> Self {
228        Self {
229            resource: env::var("ANTHROPIC_FOUNDRY_RESOURCE")
230                .or_else(|_| env::var("AZURE_RESOURCE_NAME"))
231                .ok(),
232            base_url: env::var("ANTHROPIC_FOUNDRY_BASE_URL").ok(),
233            api_key: env::var("ANTHROPIC_FOUNDRY_API_KEY")
234                .or_else(|_| env::var("AZURE_API_KEY"))
235                .ok(),
236        }
237    }
238
239    /// Builder method to set resource name.
240    pub fn with_resource(mut self, resource: impl Into<String>) -> Self {
241        self.resource = Some(resource.into());
242        self
243    }
244
245    /// Builder method to set base URL (alternative to resource).
246    pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
247        self.base_url = Some(base_url.into());
248        self
249    }
250
251    /// Builder method to set API key.
252    pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
253        self.api_key = Some(api_key.into());
254        self
255    }
256}
257
258fn is_flag_set(var: &str) -> bool {
259    env::var(var)
260        .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
261        .unwrap_or(false)
262}
263
264fn parse_env<T: std::str::FromStr>(var: &str) -> Option<T> {
265    env::var(var).ok().and_then(|v| v.parse().ok())
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn test_cloud_config_default() {
274        let config = CloudConfig::default();
275        assert!(!config.provider.use_bedrock);
276        assert!(!config.provider.use_vertex);
277        assert!(!config.provider.use_foundry);
278        assert_eq!(config.tokens.max_output, DEFAULT_MAX_TOKENS);
279        assert_eq!(config.tokens.max_thinking, MIN_THINKING_BUDGET);
280    }
281
282    #[test]
283    fn test_token_limits_default() {
284        let limits = TokenLimits::default();
285        assert_eq!(limits.max_output, DEFAULT_MAX_TOKENS);
286        assert_eq!(limits.max_thinking, MIN_THINKING_BUDGET);
287    }
288
289    #[test]
290    fn test_vertex_region_for_model() {
291        let mut config = VertexConfig::default();
292        config
293            .model_regions
294            .insert("claude-4-5-sonnet".into(), "us-east5".into());
295
296        assert_eq!(
297            config.region_for_model("claude-4-5-sonnet@20250929"),
298            Some("us-east5")
299        );
300    }
301}