Skip to main content

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: PromptCachingConfig,
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 PromptCachingConfig {
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: PromptCachingConfig::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 PromptCachingConfig {
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 global_endpoint(mut self, enable: bool) -> Self {
167        self.use_global_endpoint = enable;
168        self
169    }
170
171    /// Builder method to enable extended context (1M tokens).
172    pub fn extended_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_4_5_SONNET", "claude-4-5-sonnet"),
184            ("VERTEX_REGION_CLAUDE_4_5_HAIKU", "claude-4-5-haiku"),
185            ("VERTEX_REGION_CLAUDE_4_6_OPUS", "claude-4-6-opus"),
186        ];
187
188        for (env_var, model_key) in region_vars {
189            if let Ok(region) = env::var(env_var) {
190                model_regions.insert(model_key.to_string(), region);
191            }
192        }
193
194        Self {
195            project_id: env::var("ANTHROPIC_VERTEX_PROJECT_ID")
196                .or_else(|_| env::var("GOOGLE_CLOUD_PROJECT"))
197                .or_else(|_| env::var("GCLOUD_PROJECT"))
198                .ok(),
199            region: env::var("CLOUD_ML_REGION")
200                .or_else(|_| env::var("GOOGLE_CLOUD_REGION"))
201                .ok(),
202            model_regions,
203            enable_1m_context: is_flag_set("VERTEX_ENABLE_1M_CONTEXT"),
204        }
205    }
206
207    pub fn region_for_model(&self, model: &str) -> Option<&str> {
208        for (key, region) in &self.model_regions {
209            if model.contains(key) {
210                return Some(region);
211            }
212        }
213        self.region.as_deref()
214    }
215
216    pub fn is_global(&self) -> bool {
217        self.region.as_deref() == Some("global")
218    }
219}
220
221impl FoundryConfig {
222    pub fn from_env() -> Self {
223        Self {
224            resource: env::var("ANTHROPIC_FOUNDRY_RESOURCE")
225                .or_else(|_| env::var("AZURE_RESOURCE_NAME"))
226                .ok(),
227            base_url: env::var("ANTHROPIC_FOUNDRY_BASE_URL").ok(),
228            api_key: env::var("ANTHROPIC_FOUNDRY_API_KEY")
229                .or_else(|_| env::var("AZURE_API_KEY"))
230                .ok(),
231        }
232    }
233
234    /// Builder method to set resource name.
235    pub fn resource(mut self, resource: impl Into<String>) -> Self {
236        self.resource = Some(resource.into());
237        self
238    }
239
240    /// Builder method to set base URL (alternative to resource).
241    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
242        self.base_url = Some(base_url.into());
243        self
244    }
245
246    /// Builder method to set API key.
247    pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
248        self.api_key = Some(api_key.into());
249        self
250    }
251}
252
253fn is_flag_set(var: &str) -> bool {
254    env::var(var)
255        .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
256        .unwrap_or(false)
257}
258
259fn parse_env<T: std::str::FromStr>(var: &str) -> Option<T> {
260    env::var(var).ok().and_then(|v| v.parse().ok())
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_cloud_config_default() {
269        let config = CloudConfig::default();
270        assert!(!config.provider.use_bedrock);
271        assert!(!config.provider.use_vertex);
272        assert!(!config.provider.use_foundry);
273        assert_eq!(config.tokens.max_output, DEFAULT_MAX_TOKENS);
274        assert_eq!(config.tokens.max_thinking, MIN_THINKING_BUDGET);
275    }
276
277    #[test]
278    fn test_token_limits_default() {
279        let limits = TokenLimits::default();
280        assert_eq!(limits.max_output, DEFAULT_MAX_TOKENS);
281        assert_eq!(limits.max_thinking, MIN_THINKING_BUDGET);
282    }
283
284    #[test]
285    fn test_vertex_region_for_model() {
286        let mut config = VertexConfig::default();
287        config
288            .model_regions
289            .insert("claude-4-5-sonnet".into(), "us-east5".into());
290
291        assert_eq!(
292            config.region_for_model("claude-4-5-sonnet@20250929"),
293            Some("us-east5")
294        );
295    }
296}