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