claude_agent/client/adapter/
config.rs1use std::collections::{HashMap, HashSet};
4use std::env;
5
6use crate::client::messages::{DEFAULT_MAX_TOKENS, MIN_THINKING_BUDGET};
7
8pub const DEFAULT_MODEL: &str = "claude-sonnet-4-5-20250929";
10pub const DEFAULT_SMALL_MODEL: &str = "claude-haiku-4-5-20251001";
11pub const DEFAULT_REASONING_MODEL: &str = "claude-opus-4-5-20251101";
12pub const FRONTIER_MODEL: &str = DEFAULT_REASONING_MODEL;
13
14#[cfg(feature = "aws")]
16pub const BEDROCK_MODEL: &str = "global.anthropic.claude-sonnet-4-5-20250929-v1:0";
17#[cfg(feature = "aws")]
18pub const BEDROCK_SMALL_MODEL: &str = "global.anthropic.claude-haiku-4-5-20251001-v1:0";
19#[cfg(feature = "aws")]
20pub const BEDROCK_REASONING_MODEL: &str = "global.anthropic.claude-opus-4-5-20251101-v1:0";
21
22#[cfg(feature = "gcp")]
24pub const VERTEX_MODEL: &str = "claude-sonnet-4-5@20250929";
25#[cfg(feature = "gcp")]
26pub const VERTEX_SMALL_MODEL: &str = "claude-haiku-4-5@20251001";
27#[cfg(feature = "gcp")]
28pub const VERTEX_REASONING_MODEL: &str = "claude-opus-4-5@20251101";
29
30#[cfg(feature = "azure")]
32pub const FOUNDRY_MODEL: &str = "claude-sonnet-4-5";
33#[cfg(feature = "azure")]
34pub const FOUNDRY_SMALL_MODEL: &str = "claude-haiku-4-5";
35#[cfg(feature = "azure")]
36pub const FOUNDRY_REASONING_MODEL: &str = "claude-opus-4-5";
37
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
39#[serde(rename_all = "lowercase")]
40pub enum ModelType {
41 #[default]
42 Primary,
43 Small,
44 Reasoning,
45}
46
47#[derive(Clone, Debug)]
48pub struct ModelConfig {
49 pub primary: String,
50 pub small: String,
51 pub reasoning: Option<String>,
52}
53
54impl ModelConfig {
55 pub fn new(primary: impl Into<String>, small: impl Into<String>) -> Self {
56 Self {
57 primary: primary.into(),
58 small: small.into(),
59 reasoning: None,
60 }
61 }
62
63 pub fn anthropic() -> Self {
64 Self::from_env_with_defaults(DEFAULT_MODEL, DEFAULT_SMALL_MODEL, DEFAULT_REASONING_MODEL)
65 }
66
67 fn from_env_with_defaults(
68 default_primary: &str,
69 default_small: &str,
70 default_reasoning: &str,
71 ) -> Self {
72 Self {
73 primary: env::var("ANTHROPIC_MODEL").unwrap_or_else(|_| default_primary.into()),
74 small: env::var("ANTHROPIC_SMALL_FAST_MODEL").unwrap_or_else(|_| default_small.into()),
75 reasoning: Some(
76 env::var("ANTHROPIC_REASONING_MODEL").unwrap_or_else(|_| default_reasoning.into()),
77 ),
78 }
79 }
80
81 #[cfg(feature = "aws")]
82 pub fn bedrock() -> Self {
83 Self::from_env_with_defaults(BEDROCK_MODEL, BEDROCK_SMALL_MODEL, BEDROCK_REASONING_MODEL)
84 }
85
86 #[cfg(feature = "gcp")]
87 pub fn vertex() -> Self {
88 Self::from_env_with_defaults(VERTEX_MODEL, VERTEX_SMALL_MODEL, VERTEX_REASONING_MODEL)
89 }
90
91 #[cfg(feature = "azure")]
92 pub fn foundry() -> Self {
93 Self::from_env_with_defaults(FOUNDRY_MODEL, FOUNDRY_SMALL_MODEL, FOUNDRY_REASONING_MODEL)
94 }
95
96 pub fn with_primary(mut self, model: impl Into<String>) -> Self {
97 self.primary = model.into();
98 self
99 }
100
101 pub fn with_small(mut self, model: impl Into<String>) -> Self {
102 self.small = model.into();
103 self
104 }
105
106 pub fn with_reasoning(mut self, model: impl Into<String>) -> Self {
107 self.reasoning = Some(model.into());
108 self
109 }
110
111 pub fn get(&self, model_type: ModelType) -> &str {
112 match model_type {
113 ModelType::Primary => &self.primary,
114 ModelType::Small => &self.small,
115 ModelType::Reasoning => self.reasoning.as_deref().unwrap_or(&self.primary),
116 }
117 }
118
119 pub fn resolve_alias<'a>(&'a self, alias: &'a str) -> &'a str {
120 match alias {
121 "sonnet" => &self.primary,
122 "haiku" => &self.small,
123 "opus" => self.reasoning.as_deref().unwrap_or(&self.primary),
124 other => other,
125 }
126 }
127
128 pub fn model_type_from_alias(alias: &str) -> Option<ModelType> {
129 match alias {
130 "sonnet" => Some(ModelType::Primary),
131 "haiku" => Some(ModelType::Small),
132 "opus" => Some(ModelType::Reasoning),
133 _ => None,
134 }
135 }
136}
137
138impl Default for ModelConfig {
139 fn default() -> Self {
140 Self::anthropic()
141 }
142}
143
144#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
145pub enum BetaFeature {
146 InterleavedThinking,
147 ContextManagement,
148 StructuredOutputs,
149 PromptCaching,
150 MaxTokens128k,
151 CodeExecution,
152 Mcp,
153 WebSearch,
154 WebFetch,
155 OAuth,
156 FilesApi,
157 Effort,
158 Context1M,
160 AdvancedToolUse,
162}
163
164impl BetaFeature {
165 pub fn header_value(&self) -> &'static str {
166 match self {
167 Self::InterleavedThinking => "interleaved-thinking-2025-05-14",
168 Self::ContextManagement => "context-management-2025-06-27",
169 Self::StructuredOutputs => "structured-outputs-2025-11-13",
170 Self::PromptCaching => "prompt-caching-2024-07-31",
171 Self::MaxTokens128k => "max-tokens-3-5-sonnet-2024-07-15",
172 Self::CodeExecution => "code-execution-2025-01-24",
173 Self::Mcp => "mcp-2025-04-08",
174 Self::WebSearch => "web-search-2025-03-05",
175 Self::WebFetch => "web-fetch-2025-09-10",
176 Self::OAuth => "oauth-2025-04-20",
177 Self::FilesApi => "files-api-2025-04-14",
178 Self::Effort => "effort-2025-11-24",
179 Self::Context1M => "context-1m-2025-08-07",
180 Self::AdvancedToolUse => "advanced-tool-use-2025-11-20",
181 }
182 }
183
184 fn from_header(value: &str) -> Option<Self> {
185 match value {
186 "interleaved-thinking-2025-05-14" => Some(Self::InterleavedThinking),
187 "context-management-2025-06-27" => Some(Self::ContextManagement),
188 "structured-outputs-2025-11-13" => Some(Self::StructuredOutputs),
189 "prompt-caching-2024-07-31" => Some(Self::PromptCaching),
190 "max-tokens-3-5-sonnet-2024-07-15" => Some(Self::MaxTokens128k),
191 "code-execution-2025-01-24" => Some(Self::CodeExecution),
192 "mcp-2025-04-08" => Some(Self::Mcp),
193 "web-search-2025-03-05" => Some(Self::WebSearch),
194 "web-fetch-2025-09-10" => Some(Self::WebFetch),
195 "oauth-2025-04-20" => Some(Self::OAuth),
196 "files-api-2025-04-14" => Some(Self::FilesApi),
197 "effort-2025-11-24" => Some(Self::Effort),
198 "context-1m-2025-08-07" => Some(Self::Context1M),
199 "advanced-tool-use-2025-11-20" => Some(Self::AdvancedToolUse),
200 _ => None,
201 }
202 }
203
204 pub fn all() -> &'static [BetaFeature] {
205 &[
206 Self::InterleavedThinking,
207 Self::ContextManagement,
208 Self::StructuredOutputs,
209 Self::PromptCaching,
210 Self::MaxTokens128k,
211 Self::CodeExecution,
212 Self::Mcp,
213 Self::WebSearch,
214 Self::WebFetch,
215 Self::OAuth,
216 Self::FilesApi,
217 Self::Effort,
218 Self::Context1M,
219 Self::AdvancedToolUse,
220 ]
221 }
222}
223
224#[derive(Clone, Debug, Default)]
225pub struct BetaConfig {
226 features: HashSet<BetaFeature>,
227 custom: Vec<String>,
228}
229
230impl BetaConfig {
231 pub fn new() -> Self {
232 Self::default()
233 }
234
235 pub fn all() -> Self {
236 Self {
237 features: BetaFeature::all().iter().copied().collect(),
238 custom: Vec::new(),
239 }
240 }
241
242 pub fn with(mut self, feature: BetaFeature) -> Self {
243 self.features.insert(feature);
244 self
245 }
246
247 pub fn with_custom(mut self, flag: impl Into<String>) -> Self {
248 self.custom.push(flag.into());
249 self
250 }
251
252 pub fn add(&mut self, feature: BetaFeature) {
253 self.features.insert(feature);
254 }
255
256 pub fn add_custom(&mut self, flag: impl Into<String>) {
257 self.custom.push(flag.into());
258 }
259
260 pub fn from_env() -> Self {
261 let mut config = Self::new();
262
263 if let Ok(flags) = env::var("ANTHROPIC_BETA_FLAGS") {
264 for flag in flags.split(',').map(str::trim).filter(|s| !s.is_empty()) {
265 if let Some(feature) = BetaFeature::from_header(flag) {
266 config.features.insert(feature);
267 } else {
268 config.custom.push(flag.to_string());
269 }
270 }
271 }
272
273 config
274 }
275
276 pub fn header_value(&self) -> Option<String> {
277 let mut flags: Vec<&str> = self.features.iter().map(|f| f.header_value()).collect();
278 flags.sort();
279
280 for custom in &self.custom {
281 if !flags.contains(&custom.as_str()) {
282 flags.push(custom);
283 }
284 }
285
286 if flags.is_empty() {
287 None
288 } else {
289 Some(flags.join(","))
290 }
291 }
292
293 pub fn is_empty(&self) -> bool {
294 self.features.is_empty() && self.custom.is_empty()
295 }
296
297 pub fn has(&self, feature: BetaFeature) -> bool {
298 self.features.contains(&feature)
299 }
300}
301
302#[derive(Clone, Debug)]
303pub struct ProviderConfig {
304 pub models: ModelConfig,
305 pub max_tokens: u32,
306 pub thinking_budget: Option<u32>,
307 pub enable_caching: bool,
308 pub api_version: String,
309 pub beta: BetaConfig,
310 pub extra_headers: HashMap<String, String>,
311}
312
313impl ProviderConfig {
314 pub fn new(models: ModelConfig) -> Self {
315 Self {
316 models,
317 max_tokens: DEFAULT_MAX_TOKENS,
318 thinking_budget: None,
319 enable_caching: !env::var("DISABLE_PROMPT_CACHING")
320 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
321 .unwrap_or(false),
322 api_version: "2023-06-01".into(),
323 beta: BetaConfig::from_env(),
324 extra_headers: HashMap::new(),
325 }
326 }
327
328 pub fn with_max_tokens(mut self, tokens: u32) -> Self {
329 self.max_tokens = tokens;
330 if tokens > DEFAULT_MAX_TOKENS {
331 self.beta.add(BetaFeature::MaxTokens128k);
332 }
333 self
334 }
335
336 pub fn with_thinking(mut self, budget: u32) -> Self {
337 self.thinking_budget = Some(budget.max(MIN_THINKING_BUDGET));
338 self.beta.add(BetaFeature::InterleavedThinking);
339 self
340 }
341
342 pub fn disable_caching(mut self) -> Self {
343 self.enable_caching = false;
344 self
345 }
346
347 pub fn with_api_version(mut self, version: impl Into<String>) -> Self {
348 self.api_version = version.into();
349 self
350 }
351
352 pub fn with_beta(mut self, feature: BetaFeature) -> Self {
353 self.beta.add(feature);
354 self
355 }
356
357 pub fn with_beta_config(mut self, config: BetaConfig) -> Self {
358 self.beta = config;
359 self
360 }
361
362 pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
363 self.extra_headers.insert(key.into(), value.into());
364 self
365 }
366
367 pub fn requires_128k_beta(&self) -> bool {
368 self.max_tokens > DEFAULT_MAX_TOKENS
369 }
370}
371
372impl Default for ProviderConfig {
373 fn default() -> Self {
374 Self::new(ModelConfig::default())
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn test_model_config_get() {
384 let config = ModelConfig::anthropic();
385 assert!(config.get(ModelType::Primary).contains("sonnet"));
386 assert!(config.get(ModelType::Small).contains("haiku"));
387 assert!(config.get(ModelType::Reasoning).contains("opus"));
388 }
389
390 #[test]
391 fn test_provider_config_default_max_tokens() {
392 let config = ProviderConfig::default();
393 assert_eq!(config.max_tokens, DEFAULT_MAX_TOKENS);
394 assert!(!config.requires_128k_beta());
395 }
396
397 #[test]
398 fn test_provider_config_builder() {
399 let config = ProviderConfig::new(ModelConfig::anthropic())
400 .with_max_tokens(16384)
401 .with_thinking(10000)
402 .disable_caching();
403
404 assert_eq!(config.max_tokens, 16384);
405 assert_eq!(config.thinking_budget, Some(10000));
406 assert!(!config.enable_caching);
407 assert!(config.requires_128k_beta());
408 assert!(config.beta.has(BetaFeature::MaxTokens128k));
409 assert!(config.beta.has(BetaFeature::InterleavedThinking));
410 }
411
412 #[test]
413 fn test_provider_config_auto_128k_beta() {
414 let config = ProviderConfig::default().with_max_tokens(DEFAULT_MAX_TOKENS);
415 assert!(!config.beta.has(BetaFeature::MaxTokens128k));
416
417 let config = ProviderConfig::default().with_max_tokens(DEFAULT_MAX_TOKENS + 1);
418 assert!(config.beta.has(BetaFeature::MaxTokens128k));
419 }
420
421 #[test]
422 fn test_provider_config_thinking_auto_beta() {
423 let config = ProviderConfig::default().with_thinking(5000);
424 assert!(config.beta.has(BetaFeature::InterleavedThinking));
425 assert_eq!(config.thinking_budget, Some(5000));
426 }
427
428 #[test]
429 fn test_provider_config_thinking_min_budget() {
430 let config = ProviderConfig::default().with_thinking(500);
431 assert_eq!(config.thinking_budget, Some(MIN_THINKING_BUDGET));
432 }
433
434 #[test]
435 fn test_beta_feature_header() {
436 assert_eq!(
437 BetaFeature::InterleavedThinking.header_value(),
438 "interleaved-thinking-2025-05-14"
439 );
440 assert_eq!(
441 BetaFeature::MaxTokens128k.header_value(),
442 "max-tokens-3-5-sonnet-2024-07-15"
443 );
444 }
445
446 #[test]
447 fn test_beta_config_with_features() {
448 let config = BetaConfig::new()
449 .with(BetaFeature::InterleavedThinking)
450 .with(BetaFeature::ContextManagement);
451
452 assert!(config.has(BetaFeature::InterleavedThinking));
453 assert!(config.has(BetaFeature::ContextManagement));
454 assert!(!config.has(BetaFeature::MaxTokens128k));
455
456 let header = config.header_value().unwrap();
457 assert!(header.contains("interleaved-thinking"));
458 assert!(header.contains("context-management"));
459 }
460
461 #[test]
462 fn test_beta_config_custom() {
463 let config = BetaConfig::new()
464 .with(BetaFeature::InterleavedThinking)
465 .with_custom("new-feature-2026-01-01");
466
467 let header = config.header_value().unwrap();
468 assert!(header.contains("interleaved-thinking"));
469 assert!(header.contains("new-feature-2026-01-01"));
470 }
471
472 #[test]
473 fn test_beta_config_all() {
474 let config = BetaConfig::all();
475 assert!(config.has(BetaFeature::InterleavedThinking));
476 assert!(config.has(BetaFeature::ContextManagement));
477 assert!(config.has(BetaFeature::MaxTokens128k));
478 }
479
480 #[test]
481 fn test_provider_config_beta() {
482 let config = ProviderConfig::default()
483 .with_beta(BetaFeature::InterleavedThinking)
484 .with_beta_config(
485 BetaConfig::new()
486 .with(BetaFeature::InterleavedThinking)
487 .with_custom("experimental-feature"),
488 );
489
490 assert!(config.beta.has(BetaFeature::InterleavedThinking));
491 let header = config.beta.header_value().unwrap();
492 assert!(header.contains("experimental-feature"));
493 }
494
495 #[test]
496 fn test_beta_config_empty() {
497 let config = BetaConfig::new();
498 assert!(config.is_empty());
499 assert!(config.header_value().is_none());
500 }
501
502 #[test]
503 fn test_provider_config_extra_headers() {
504 let config = ProviderConfig::default()
505 .with_header("x-custom", "value")
506 .with_header("x-another", "test");
507
508 assert_eq!(config.extra_headers.get("x-custom"), Some(&"value".into()));
509 assert_eq!(config.extra_headers.get("x-another"), Some(&"test".into()));
510 }
511}