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