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