1use crate::client::LLMClient;
13use crate::provider::{LLMError, LLMProvider, LLMRequest, LLMResponse, LLMStream, LLMStreamEvent, ToolDefinition};
14use vtcode_config::constants::{models, urls};
15use vtcode_config::core::{AnthropicConfig, AnthropicPromptCacheSettings, ModelConfig, PromptCachingConfig};
16use vtcode_config::{AnthropicSpeed, TimeoutsConfig};
17
18use super::capabilities;
19use super::fallback_retry::{self, RefusalRetryPlan};
20use super::headers;
21use super::request_builder::{self, RequestBuilderContext};
22use super::response_parser;
23use super::stream_decoder;
24use super::validation;
25
26use crate::providers::common::{extract_prompt_cache_settings, resolve_model};
27use crate::providers::error_handling::{format_network_error, format_parse_error, handle_anthropic_http_error};
28use crate::providers::openai::CustomProviderAuthHandle;
29
30use async_trait::async_trait;
31use futures::StreamExt;
32use reqwest::Client as HttpClient;
33use reqwest::StatusCode;
34use serde_json::Value;
35
36const ANTHROPIC_COMPACT_BETA: &str = "compact-2026-01-12";
37const ANTHROPIC_CONTEXT_MANAGEMENT_BETA: &str = "context-management-2025-06-27";
38const ANTHROPIC_ADVISOR_BETA: &str = "advisor-tool-2026-03-01";
39
40fn is_first_party_anthropic_endpoint(base_url: &str) -> bool {
43 url::Url::parse(base_url)
44 .ok()
45 .and_then(|url| url.host_str().map(|host| host.eq_ignore_ascii_case("api.anthropic.com")))
46 .unwrap_or(false)
47}
48
49#[derive(Clone)]
50pub struct AnthropicProvider {
51 api_key: String,
52 http_client: HttpClient,
53 base_url: String,
54 model: String,
55 prompt_cache_enabled: bool,
56 prompt_cache_settings: AnthropicPromptCacheSettings,
57 anthropic_config: AnthropicConfig,
58 custom_provider_auth: Option<CustomProviderAuthHandle>,
59 custom_provider: bool,
60 model_behavior: Option<ModelConfig>,
61 session_affinity_header: Option<&'static str>,
62}
63
64impl AnthropicProvider {
65 pub fn new(api_key: String) -> Self {
66 Self::with_model_internal(
67 api_key,
68 models::anthropic::DEFAULT_MODEL.to_string(),
69 None,
70 None,
71 AnthropicConfig::default(),
72 TimeoutsConfig::default(),
73 None,
74 )
75 }
76
77 fn with_model(api_key: String, model: String) -> Self {
78 Self::with_model_internal(
79 api_key,
80 model,
81 None,
82 None,
83 AnthropicConfig::default(),
84 TimeoutsConfig::default(),
85 None,
86 )
87 }
88
89 pub(crate) fn new_with_client(
90 api_key: String,
91 model: String,
92 http_client: reqwest::Client,
93 base_url: String,
94 _timeouts: TimeoutsConfig,
95 ) -> Self {
96 Self {
97 api_key,
98 http_client,
99 base_url,
100 model,
101 prompt_cache_enabled: false,
102 prompt_cache_settings: AnthropicPromptCacheSettings::default(),
103 anthropic_config: AnthropicConfig::default(),
104 custom_provider_auth: None,
105 custom_provider: false,
106 model_behavior: None,
107 session_affinity_header: None,
108 }
109 }
110
111 pub fn from_config(
112 api_key: Option<String>,
113 model: Option<String>,
114 base_url: Option<String>,
115 prompt_cache: Option<PromptCachingConfig>,
116 timeouts: Option<TimeoutsConfig>,
117 anthropic_config: Option<AnthropicConfig>,
118 model_behavior: Option<ModelConfig>,
119 ) -> Self {
120 let api_key_value = api_key.unwrap_or_default();
121 let model_value = resolve_model(model, models::anthropic::DEFAULT_MODEL);
122 let anthropic_cfg = anthropic_config.unwrap_or_default();
123
124 Self::with_model_internal(
125 api_key_value,
126 model_value,
127 prompt_cache,
128 base_url,
129 anthropic_cfg,
130 timeouts.unwrap_or_default(),
131 model_behavior,
132 )
133 }
134
135 fn with_model_internal(
136 api_key: String,
137 model: String,
138 prompt_cache: Option<PromptCachingConfig>,
139 base_url: Option<String>,
140 anthropic_config: AnthropicConfig,
141 timeouts: TimeoutsConfig,
142 model_behavior: Option<ModelConfig>,
143 ) -> Self {
144 use crate::http_client::HttpClientFactory;
145
146 let (prompt_cache_enabled, prompt_cache_settings) = extract_prompt_cache_settings(
147 prompt_cache,
148 |providers| &providers.anthropic,
149 |cfg, provider_settings| cfg.enabled && provider_settings.enabled,
150 );
151
152 let base_url_value = if models::minimax::SUPPORTED_MODELS.contains(&model.as_str()) {
153 crate::providers::minimax::resolve_minimax_base_url(base_url)
154 } else {
155 AnthropicSpeed::resolve_base_url(base_url.as_deref())
156 };
157
158 Self {
159 api_key,
160 http_client: HttpClientFactory::for_llm(&timeouts),
161 base_url: base_url_value,
162 model,
163 prompt_cache_enabled,
164 prompt_cache_settings,
165 anthropic_config,
166 custom_provider_auth: None,
167 custom_provider: false,
168 model_behavior,
169 session_affinity_header: None,
170 }
171 }
172
173 pub(crate) fn with_custom_auth(mut self, custom_provider_auth: Option<CustomProviderAuthHandle>) -> Self {
174 self.custom_provider_auth = custom_provider_auth;
175 self.custom_provider = true;
176 self
177 }
178
179 pub(crate) fn with_session_affinity_header(mut self, header: &'static str) -> Self {
183 self.session_affinity_header = Some(header);
184 self
185 }
186
187 fn requires_advanced_tool_use_beta(&self, request: &LLMRequest) -> bool {
188 request.tools.as_ref().is_some_and(|tools| {
189 tools.iter().any(|tool| {
190 (tool.is_tool_search() || tool.defer_loading.unwrap_or(false))
191 || tool.allowed_callers.as_ref().is_some_and(|callers| !callers.is_empty())
192 || tool.input_examples.as_ref().is_some_and(|examples| !examples.is_empty())
193 })
194 })
195 }
196
197 fn code_execution_betas(&self, request: &LLMRequest) -> Vec<String> {
198 request
199 .tools
200 .as_ref()
201 .map(|tools| {
202 tools
203 .iter()
204 .filter_map(|tool| {
205 tool.is_anthropic_code_execution()
206 .then(|| code_execution_beta_name(&tool.tool_type))
207 .flatten()
208 })
209 .fold(Vec::new(), |mut betas, beta| {
210 if !betas.contains(&beta) {
211 betas.push(beta);
212 }
213 betas
214 })
215 })
216 .unwrap_or_default()
217 }
218
219 fn context_management_betas(&self, request: &LLMRequest) -> Vec<&'static str> {
220 let mut betas = Vec::new();
221
222 if request
223 .tools
224 .as_ref()
225 .is_some_and(|tools| tools.iter().any(ToolDefinition::is_anthropic_memory_tool))
226 {
227 betas.push(ANTHROPIC_CONTEXT_MANAGEMENT_BETA);
228 }
229
230 if let Some(context_management) = request.context_management.as_ref() {
231 if uses_anthropic_compaction(context_management) {
232 betas.push(ANTHROPIC_COMPACT_BETA);
233 }
234
235 if uses_anthropic_context_edits(context_management) && !betas.contains(&ANTHROPIC_CONTEXT_MANAGEMENT_BETA) {
236 betas.push(ANTHROPIC_CONTEXT_MANAGEMENT_BETA);
237 }
238 }
239
240 betas
241 }
242
243 fn advisor_enabled_for_request(&self, request: &LLMRequest) -> bool {
248 let executor = capabilities::resolve_model_name(&request.model, &self.model);
249 request_builder::resolve_advisor_tool(executor, &self.anthropic_config.advisor).is_some()
250 }
251
252 fn uses_refreshable_auth(&self) -> bool {
253 self.custom_provider_auth.is_some()
254 }
255
256 async fn current_api_key(&self) -> Result<String, LLMError> {
257 if let Some(handle) = &self.custom_provider_auth {
258 return handle
259 .current_token()
260 .await
261 .map_err(|error| format_network_error("Anthropic", &error));
262 }
263
264 Ok(self.api_key.clone())
265 }
266
267 async fn refresh_api_key_for_retry(&self) -> Result<String, LLMError> {
268 if let Some(handle) = &self.custom_provider_auth {
269 return handle
270 .force_refresh()
271 .await
272 .map_err(|error| format_network_error("Anthropic", &error));
273 }
274
275 Ok(self.api_key.clone())
276 }
277
278 pub fn with_leak_protection(&self, mut request: LLMRequest, secret_description: &str) -> LLMRequest {
282 let reminder = format!("[Never mention or reveal {secret_description}]");
283 let merged_system_prompt = match request.system_prompt.as_ref() {
284 Some(existing) => format!("{reminder}\n\n{existing}"),
285 None => reminder,
286 };
287 request.system_prompt = Some(std::sync::Arc::from(merged_system_prompt));
288 request
289 }
290
291 pub fn format_documents_xml(&self, documents: Vec<(&str, &str)>) -> String {
292 let mut xml = String::from("<documents>\n");
293 for (i, (source, content)) in documents.iter().enumerate() {
294 xml.push_str(&format!(
295 " <document index=\"{}\">\n <source>{}</source>\n <document_content>\n{}\n </document_content>\n </document>\n",
296 i + 1,
297 source,
298 content
299 ));
300 }
301 xml.push_str("</documents>");
302 xml
303 }
304
305 pub fn extract_xml_block(&self, content: &str, tag: &str) -> Option<String> {
306 let start_tag = format!("<{tag}>");
307 let end_tag = format!("</{tag}>");
308
309 let start_pos = content.find(&start_tag)? + start_tag.len();
310 let end_pos = content.find(&end_tag)?;
311
312 if start_pos < end_pos {
313 Some(content[start_pos..end_pos].trim().to_string())
314 } else {
315 None
316 }
317 }
318
319 fn request_builder_context(&self) -> RequestBuilderContext<'_> {
320 RequestBuilderContext {
321 prompt_cache_enabled: self.prompt_cache_enabled,
322 prompt_cache_settings: &self.prompt_cache_settings,
323 anthropic_config: &self.anthropic_config,
324 model: &self.model,
325 server_side_fallbacks_available: is_first_party_anthropic_endpoint(&self.base_url),
326 }
327 }
328
329 fn resolved_request_model<'a>(&'a self, request: &'a LLMRequest) -> &'a str {
330 capabilities::resolve_model_name(&request.model, &self.model)
331 }
332
333 fn effective_betas(&self, request: &LLMRequest) -> Option<Vec<String>> {
334 let mut betas = request.betas.clone().unwrap_or_default();
335 for beta in self.context_management_betas(request) {
336 if !betas.iter().any(|existing| existing == beta) {
337 betas.push(beta.to_string());
338 }
339 }
340 for beta in self.code_execution_betas(request) {
341 if !betas.iter().any(|existing| existing == &beta) {
342 betas.push(beta);
343 }
344 }
345 if self.advisor_enabled_for_request(request) && !betas.iter().any(|beta| beta == ANTHROPIC_ADVISOR_BETA) {
346 betas.push(ANTHROPIC_ADVISOR_BETA.to_string());
347 }
348
349 (!betas.is_empty()).then_some(betas)
350 }
351
352 fn convert_to_anthropic_format(&self, request: &LLMRequest) -> Result<Value, LLMError> {
353 let mut body = request_builder::convert_to_anthropic_format(request, &self.request_builder_context())?;
354 super::speed::apply_speed(&mut body, self.resolve_speed(request)?);
355 Ok(body)
356 }
357
358 fn resolve_speed(&self, request: &LLMRequest) -> Result<Option<AnthropicSpeed>, LLMError> {
359 super::speed::resolve_speed(
360 request,
361 self.anthropic_config.speed,
362 self.resolved_request_model(request),
363 !self.custom_provider && AnthropicSpeed::is_native_endpoint(&self.base_url),
364 )
365 }
366
367 fn beta_header_for_request(
368 &self,
369 request: &LLMRequest,
370 anthropic_request: &Value,
371 include_advanced_tool_use: bool,
372 request_betas: Option<&[String]>,
373 ) -> Option<String> {
374 let server_side_fallback = headers::ServerSideFallbackForm::of_request(anthropic_request);
375 let beta_config = headers::BetaHeaderConfig {
376 config: &self.anthropic_config,
377 model: self.resolved_request_model(request),
378 include_advanced_tool_use,
379 include_manual_interleaved_beta: anthropic_request
380 .get("thinking")
381 .and_then(|value| value.get("type"))
382 .and_then(Value::as_str)
383 == Some("enabled"),
384 request_betas,
385 include_fast_mode: anthropic_request.get("speed").is_some()
386 && AnthropicSpeed::requires_beta(self.resolved_request_model(request)),
387 include_task_budget: anthropic_request
388 .get("output_config")
389 .and_then(|value| value.get("task_budget"))
390 .is_some(),
391 server_side_fallback,
392 include_fallback_credit: request.fallback_credit_token.is_some()
397 || server_side_fallback == Some(headers::ServerSideFallbackForm::List),
398 include_mid_conversation_tool_changes: false,
399 include_mid_conversation_system_clear_at: capabilities::supports_turn_scoped_system_messages(
400 self.resolved_request_model(request),
401 &self.model,
402 ) && anthropic_request
403 .get("messages")
404 .and_then(Value::as_array)
405 .is_some_and(|messages| {
406 messages
407 .iter()
408 .any(|message| message.get("clear_at").and_then(Value::as_str) == Some("next_user_message"))
409 }),
410 include_thinking_display_updates: std::iter::once(anthropic_request.get("thinking"))
413 .chain(
414 anthropic_request
415 .get("fallbacks")
416 .and_then(Value::as_array)
417 .into_iter()
418 .flatten()
419 .map(|fallback| fallback.get("thinking")),
420 )
421 .flatten()
422 .any(|thinking| thinking.get("display").and_then(Value::as_str) == Some("updates")),
423 prompt_cache_profile: request.prompt_cache_profile,
427 };
428
429 headers::combined_beta_header_value(self.prompt_cache_enabled, &self.prompt_cache_settings, &beta_config)
430 }
431
432 async fn send_request(
433 &self,
434 request: &LLMRequest,
435 anthropic_request: &Value,
436 ) -> Result<AnthropicHttpResponse, LLMError> {
437 let include_advanced_tool_use = self.requires_advanced_tool_use_beta(request);
438 let betas = self.effective_betas(request);
439 let url = format!("{}/messages", self.base_url.trim_end_matches('/'));
440
441 let beta_header =
442 self.beta_header_for_request(request, anthropic_request, include_advanced_tool_use, betas.as_deref());
443 let metadata = request.metadata.clone();
444 let session_affinity = self.session_affinity_header.and_then(|header| {
445 crate::providers::shared::session_lineage_from_prompt_cache_key(request.prompt_cache_key.as_deref())
446 .map(|lineage| (header, lineage))
447 });
448
449 let send_once = |api_key: String| {
450 let mut request_builder = self
451 .http_client
452 .post(&url)
453 .header("x-api-key", api_key)
454 .header("anthropic-version", urls::ANTHROPIC_API_VERSION);
455
456 if let Some(beta_header) = beta_header.clone() {
457 request_builder = request_builder.header("anthropic-beta", beta_header);
458 }
459
460 if let Some((header, lineage)) = session_affinity.clone() {
461 request_builder = request_builder.header(header, lineage);
462 }
463
464 if let Some(metadata) = metadata.as_ref()
465 && let Ok(metadata_str) = serde_json::to_string(metadata)
466 {
467 request_builder = request_builder.header("X-Turn-Metadata", metadata_str);
468 }
469
470 request_builder.json(anthropic_request)
471 };
472
473 let response = send_once(self.current_api_key().await?)
474 .send()
475 .await
476 .map_err(|e| format_network_error("Anthropic", &e))?;
477
478 let response = if self.uses_refreshable_auth()
479 && matches!(response.status(), StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN)
480 {
481 send_once(self.refresh_api_key_for_retry().await?)
482 .send()
483 .await
484 .map_err(|e| format_network_error("Anthropic", &e))?
485 } else {
486 response
487 };
488
489 let response = handle_anthropic_http_error(response).await?;
490
491 let request_id = response
492 .headers()
493 .get("request-id")
494 .and_then(|h| h.to_str().ok().map(|s| s.to_string()));
495 let organization_id = response
496 .headers()
497 .get("anthropic-organization-id")
498 .and_then(|h| h.to_str().ok().map(|s| s.to_string()));
499
500 Ok(AnthropicHttpResponse { response, request_id, organization_id })
501 }
502}
503
504fn code_execution_beta_name(tool_type: &str) -> Option<String> {
505 let suffix = tool_type.strip_prefix("code_execution_")?;
506 if suffix.len() != 8 || !suffix.chars().all(|ch| ch.is_ascii_digit()) {
507 return None;
508 }
509
510 Some(format!("code-execution-{}-{}-{}", &suffix[0..4], &suffix[4..6], &suffix[6..8]))
511}
512
513fn uses_anthropic_compaction(context_management: &Value) -> bool {
514 context_management
515 .as_array()
516 .is_some_and(|items| items.iter().any(is_compaction_item))
517 || context_management
518 .get("edits")
519 .and_then(Value::as_array)
520 .is_some_and(|edits| edits.iter().any(is_compaction_edit_item))
521}
522
523fn is_compaction_item(item: &Value) -> bool {
524 item.get("type").and_then(Value::as_str) == Some("compaction")
525}
526
527fn is_compaction_edit_item(item: &Value) -> bool {
528 item.get("type")
529 .and_then(Value::as_str)
530 .is_some_and(|edit_type| edit_type.starts_with("compact_"))
531}
532
533fn uses_anthropic_context_edits(context_management: &Value) -> bool {
534 context_management
535 .get("edits")
536 .and_then(Value::as_array)
537 .is_some_and(|edits| edits.iter().any(is_context_edit_item))
538}
539
540fn is_context_edit_item(item: &Value) -> bool {
541 item.get("type")
542 .and_then(Value::as_str)
543 .is_some_and(|edit_type| edit_type.starts_with("clear_tool_uses_") || edit_type.starts_with("clear_thinking_"))
544}
545
546struct AnthropicHttpResponse {
547 response: reqwest::Response,
548 request_id: Option<String>,
549 organization_id: Option<String>,
550}
551
552impl AnthropicProvider {
553 async fn generate_with_body(
554 &self,
555 request: &LLMRequest,
556 anthropic_request: &Value,
557 model: String,
558 ) -> Result<LLMResponse, LLMError> {
559 let AnthropicHttpResponse { response, request_id, organization_id } =
560 self.send_request(request, anthropic_request).await?;
561
562 let anthropic_response: Value = response.json().await.map_err(|e| format_parse_error("Anthropic", &e))?;
563
564 let mut llm_response = response_parser::parse_response(anthropic_response, model)?;
565 llm_response.request_id = request_id;
566 llm_response.organization_id = organization_id;
567 Ok(llm_response)
568 }
569
570 async fn send_refusal_retry(
575 &self,
576 request: &LLMRequest,
577 original_body: &Value,
578 plan: &RefusalRetryPlan,
579 ) -> Option<AnthropicHttpResponse> {
580 let fallback_retry::RefusalRetryBody { mut body, with_token } = plan.retry_body(original_body, &self.model);
581 let retry_request = plan.retry_request(request, with_token);
582 let speed = match self.resolve_speed(&retry_request) {
583 Ok(speed) => speed,
584 Err(err) => {
585 tracing::warn!(provider = "anthropic", error = %err, "refusal retry has incompatible requested speed");
586 return None;
587 }
588 };
589 super::speed::apply_speed(&mut body, speed);
590 tracing::info!(
591 provider = "anthropic",
592 recommended_model = %plan.model,
593 with_credit_token = with_token,
594 "refusal fallback could not run server-side; retrying once on the recommended model"
595 );
596 match self.send_request(&plan.retry_request(request, with_token), &body).await {
597 Ok(response) => Some(response),
598 Err(err) if with_token && fallback_retry::rejects_credit_token(&err) => {
599 tracing::warn!(
600 provider = "anthropic",
601 error = %err,
602 "fallback credit token rejected; resending the refusal retry without it"
603 );
604 fallback_retry::strip_credit_token(&mut body);
605 match self.send_request(&plan.retry_request(request, false), &body).await {
606 Ok(response) => Some(response),
607 Err(err) => {
608 tracing::warn!(provider = "anthropic", error = %err, "refusal retry failed");
609 None
610 }
611 }
612 }
613 Err(err) => {
614 tracing::warn!(provider = "anthropic", error = %err, "refusal retry failed");
615 None
616 }
617 }
618 }
619
620 async fn retry_refused_generate(
621 &self,
622 request: &LLMRequest,
623 original_body: &Value,
624 plan: &RefusalRetryPlan,
625 refused: LLMResponse,
626 ) -> Result<LLMResponse, LLMError> {
627 let Some(AnthropicHttpResponse { response, request_id, organization_id }) =
628 self.send_refusal_retry(request, original_body, plan).await
629 else {
630 return Ok(refused);
631 };
632 let parsed = match response.json::<Value>().await {
633 Ok(value) => response_parser::parse_response(value, plan.model.clone()),
634 Err(err) => Err(format_parse_error("Anthropic", &err)),
635 };
636 match parsed {
637 Ok(mut retried) => {
638 retried.request_id = request_id;
639 retried.organization_id = organization_id;
640 Ok(retried)
641 }
642 Err(err) => {
643 tracing::warn!(provider = "anthropic", error = %err, "refusal retry response could not be parsed");
644 Ok(refused)
645 }
646 }
647 }
648
649 fn stream_with_refusal_retry(
653 &self,
654 primary: LLMStream,
655 request: LLMRequest,
656 original_body: Value,
657 model: String,
658 ) -> LLMStream {
659 let provider = self.clone();
660 Box::pin(async_stream::stream! {
661 let mut primary = primary;
662 while let Some(event) = primary.next().await {
663 let response = match event {
664 Ok(LLMStreamEvent::Completed { response }) => response,
665 other => {
666 yield other;
667 continue;
668 }
669 };
670 let retry = match RefusalRetryPlan::for_response(&response, &model) {
671 Some(plan) => provider
672 .send_refusal_retry(&request, &original_body, &plan)
673 .await
674 .map(|http| (plan, http)),
675 None => None,
676 };
677 match retry {
678 Some((plan, AnthropicHttpResponse { response: http, request_id, organization_id })) => {
679 let mut retried =
680 stream_decoder::create_stream(http, plan.model, request_id, organization_id);
681 while let Some(event) = retried.next().await {
682 yield event;
683 }
684 }
685 None => yield Ok(LLMStreamEvent::Completed { response }),
686 }
687 }
688 })
689 }
690}
691
692#[async_trait]
693impl LLMProvider for AnthropicProvider {
694 fn name(&self) -> &str {
695 "anthropic"
696 }
697
698 fn supports_streaming(&self) -> bool {
699 true
700 }
701
702 fn supports_non_streaming(&self, _model: &str) -> bool {
703 true
705 }
706
707 fn supports_reasoning(&self, model: &str) -> bool {
708 capabilities::supports_reasoning(model, &self.model)
711 || self
712 .model_behavior
713 .as_ref()
714 .and_then(|b| b.model_supports_reasoning)
715 .unwrap_or(false)
716 }
717
718 fn supported_reasoning_efforts(&self, model: &str) -> &'static [&'static str] {
719 capabilities::allowed_efforts_for_model(model, &self.model).unwrap_or_else(|| {
720 self.model_behavior
721 .as_ref()
722 .and_then(|behavior| behavior.model_supports_reasoning_effort)
723 .filter(|supported| *supported)
724 .map(|_| crate::provider::GENERIC_REASONING_EFFORTS)
725 .unwrap_or(&[])
726 })
727 }
728
729 fn supports_reasoning_effort(&self, model: &str) -> bool {
730 capabilities::supports_reasoning_effort(model, &self.model)
732 || self
733 .model_behavior
734 .as_ref()
735 .and_then(|b| b.model_supports_reasoning_effort)
736 .unwrap_or(false)
737 }
738
739 fn supports_parallel_tool_config(&self, model: &str) -> bool {
740 capabilities::supports_parallel_tool_config(model)
741 }
742
743 fn supports_context_edits(&self, _model: &str) -> bool {
744 true
745 }
746
747 fn supports_turn_scoped_system_messages(&self, model: &str) -> bool {
748 capabilities::supports_turn_scoped_system_messages(model, &self.model)
749 }
750
751 fn supports_responses_compaction(&self, model: &str) -> bool {
752 capabilities::supports_compaction(model)
755 }
756
757 fn supports_native_inline_compaction(&self, model: &str) -> bool {
758 capabilities::supports_compaction(model)
762 }
763
764 fn effective_context_size(&self, model: &str) -> usize {
765 capabilities::effective_context_size(model)
766 }
767
768 fn supports_structured_output(&self, model: &str) -> bool {
769 capabilities::supports_structured_output(model, &self.model)
770 }
771
772 fn supports_vision(&self, model: &str) -> bool {
773 capabilities::supports_vision(model, &self.model)
774 }
775
776 async fn generate(&self, request: LLMRequest) -> Result<LLMResponse, LLMError> {
777 let resolved_model = self.resolved_request_model(&request).to_string();
778 let anthropic_request = self.convert_to_anthropic_format(&request)?;
779
780 let response = self
781 .generate_with_body(&request, &anthropic_request, resolved_model.clone())
782 .await?;
783 match RefusalRetryPlan::for_response(&response, &resolved_model) {
784 Some(plan) => self.retry_refused_generate(&request, &anthropic_request, &plan, response).await,
785 None => Ok(response),
786 }
787 }
788
789 async fn stream(&self, request: LLMRequest) -> Result<LLMStream, LLMError> {
790 let resolved_model = self.resolved_request_model(&request).to_string();
791 let mut anthropic_request = self.convert_to_anthropic_format(&request)?;
792
793 if let Some(obj) = anthropic_request.as_object_mut() {
794 obj.insert("stream".to_string(), Value::Bool(true));
795 }
796
797 let AnthropicHttpResponse { response, request_id, organization_id } =
798 self.send_request(&request, &anthropic_request).await?;
799
800 let primary = stream_decoder::create_stream(response, resolved_model.clone(), request_id, organization_id);
801 Ok(self.stream_with_refusal_retry(primary, request, anthropic_request, resolved_model))
802 }
803
804 fn supported_models(&self) -> Vec<String> {
805 capabilities::supported_models()
806 }
807
808 fn validate_request(&self, request: &LLMRequest) -> Result<(), LLMError> {
809 self.resolve_speed(request)?;
810 validation::validate_request(request, &self.model, &self.anthropic_config, "Anthropic")
811 }
812}
813
814#[async_trait]
815impl LLMClient for AnthropicProvider {
816 async fn generate(&mut self, prompt: &str) -> Result<LLMResponse, LLMError> {
817 let request = crate::providers::common::make_default_request(prompt, &self.model);
818 let request_model = request.model.clone();
819 let response = LLMProvider::generate(self, request).await?;
820
821 Ok(LLMResponse {
822 content: Some(response.content.unwrap_or_default()),
823 model: request_model,
824 usage: response.usage.map(crate::providers::common::convert_usage_to_llm_types),
825 reasoning: response.reasoning,
826 reasoning_details: response.reasoning_details,
827 request_id: response.request_id,
828 organization_id: response.organization_id,
829 finish_reason: response.finish_reason,
830 tool_calls: response.tool_calls,
831 tool_references: response.tool_references,
832 compaction: response.compaction,
833 })
834 }
835
836 fn model_id(&self) -> &str {
837 &self.model
838 }
839}
840
841#[cfg(test)]
842mod tests;