1use crate::error_display;
2use crate::provider::{
3 AnthropicOptionalStringOverride, AnthropicOptionalU32Override, AnthropicRequestOverrides,
4 AnthropicThinkingDisplayOverride, AnthropicThinkingModeOverride, ContentPart, FinishReason, LLMError, LLMRequest,
5 LLMResponse, Message, MessageContent, MessageRole, ParallelToolConfig, ToolCall, ToolChoice, ToolDefinition,
6};
7use crate::providers::anthropic_types::{
8 AnthropicOutputConfig, AnthropicOutputFormat, ThinkingConfig, ThinkingDisplay,
9};
10use crate::providers::common::normalize_reasoning_detail_object;
11use serde::ser::SerializeMap;
12use serde::{Deserialize, Serialize, Serializer};
13use serde_json::{Value, json};
14use std::sync::Arc;
15
16#[derive(Debug, Deserialize, Clone)]
18pub struct AnthropicMessagesRequest {
19 pub(crate) model: String,
20 pub(crate) max_tokens: u32,
21 pub(crate) messages: Vec<AnthropicMessage>,
22 #[serde(default)]
23 pub(crate) system: Option<AnthropicSystemPrompt>,
24 #[serde(default)]
25 pub(crate) stream: bool,
26 #[serde(default)]
27 pub(crate) stop_sequences: Option<Vec<String>>,
28 #[serde(default)]
33 pub(crate) temperature: Option<f32>,
34 #[serde(default)]
35 pub(crate) top_p: Option<f32>,
36 #[serde(default)]
37 pub(crate) top_k: Option<i32>,
38 #[serde(default)]
39 pub(crate) tools: Option<Vec<AnthropicTool>>,
40 #[serde(default)]
41 pub(crate) tool_choice: Option<Value>,
42 #[serde(default)]
43 pub(crate) thinking: Option<ThinkingConfig>,
44 #[serde(default)]
45 pub(crate) betas: Option<Vec<String>>,
46 #[serde(default)]
47 pub(crate) context_management: Option<Value>,
48 #[serde(default)]
49 pub(crate) output_config: Option<AnthropicOutputConfig>,
50}
51
52#[derive(Debug, Deserialize, Serialize, Clone)]
53pub struct AnthropicMessage {
54 pub(crate) role: String,
55 pub(crate) content: AnthropicContent,
56}
57
58#[derive(Debug, Deserialize, Serialize, Clone)]
59#[serde(untagged)]
60pub enum AnthropicContent {
61 Text(String),
62 Blocks(Vec<AnthropicContentBlock>),
63}
64
65#[derive(Debug, Deserialize, Serialize, Clone)]
66#[serde(tag = "type")]
67pub enum AnthropicContentBlock {
68 #[serde(rename = "text")]
69 Text {
70 text: String,
71 #[serde(default)]
72 citations: Option<Value>,
73 #[serde(default)]
74 cache_control: Option<Value>,
75 },
76 #[serde(rename = "image")]
77 Image { source: AnthropicImageSource },
78 #[serde(rename = "tool_use")]
79 ToolUse { id: String, name: String, input: Value },
80 #[serde(rename = "document")]
85 Document { source: Value },
86 #[serde(rename = "tool_result")]
87 ToolResult {
88 tool_use_id: String,
89 content: AnthropicContent,
90 is_error: Option<bool>,
91 },
92 #[serde(rename = "thinking")]
93 Thinking {
94 thinking: String,
95 #[serde(default)]
96 signature: Option<String>,
97 },
98 #[serde(rename = "redacted_thinking")]
99 RedactedThinking { data: String },
100 #[serde(rename = "server_tool_use")]
101 ServerToolUse { id: String, name: String, input: Value },
102 #[serde(rename = "container_upload")]
103 ContainerUpload { file_id: String },
104 #[serde(rename = "code_execution_tool_result")]
105 CodeExecutionToolResult { tool_use_id: String, content: Value },
106 #[serde(rename = "bash_code_execution_tool_result")]
107 BashCodeExecutionToolResult { tool_use_id: String, content: Value },
108 #[serde(rename = "text_editor_code_execution_tool_result")]
109 TextEditorCodeExecutionToolResult { tool_use_id: String, content: Value },
110 #[serde(rename = "web_search_tool_result")]
111 WebSearchToolResult { tool_use_id: String, content: Value },
112 #[serde(rename = "advisor_tool_result")]
113 AdvisorToolResult { tool_use_id: String, content: Value },
114 #[serde(other)]
116 Unknown,
117}
118
119#[derive(Debug, Deserialize, Serialize, Clone)]
120pub struct AnthropicImageSource {
121 r#type: String,
122 media_type: String,
123 data: String,
124}
125
126#[derive(Debug, Deserialize, Serialize, Clone)]
127#[serde(untagged)]
128pub enum AnthropicSystemPrompt {
129 Text(String),
130 Blocks(Vec<AnthropicContentBlock>),
131}
132
133#[derive(Debug, Deserialize, Clone)]
134#[serde(untagged)]
135pub enum AnthropicTool {
136 Advisor {
137 #[serde(rename = "type")]
138 tool_type: String,
139 name: String,
140 model: String,
141 #[serde(default)]
142 max_uses: Option<u32>,
143 #[serde(default)]
144 max_tokens: Option<u32>,
145 #[serde(default)]
146 caching: Option<AnthropicAdvisorCachingCompat>,
147 },
148 Function {
149 name: String,
150 description: Option<String>,
151 input_schema: Value,
152 #[serde(default)]
153 input_examples: Option<Vec<Value>>,
154 #[serde(default)]
155 strict: Option<bool>,
156 #[serde(default)]
157 allowed_callers: Option<Vec<String>>,
158 },
159 Native {
160 #[serde(rename = "type")]
161 tool_type: String,
162 name: String,
163 #[serde(flatten, default)]
164 options: serde_json::Map<String, Value>,
165 },
166}
167
168impl Serialize for AnthropicTool {
169 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
170 where
171 S: Serializer,
172 {
173 match self {
174 Self::Advisor {
175 tool_type,
176 name,
177 model,
178 max_uses,
179 max_tokens,
180 caching,
181 } => {
182 let mut map = serializer.serialize_map(None)?;
183 map.serialize_entry("type", tool_type)?;
184 map.serialize_entry("name", name)?;
185 map.serialize_entry("model", model)?;
186 map.serialize_entry("max_uses", max_uses)?;
187 map.serialize_entry("max_tokens", max_tokens)?;
188 map.serialize_entry("caching", caching)?;
189 map.end()
190 }
191 Self::Function {
192 name,
193 description,
194 input_schema,
195 input_examples,
196 strict,
197 allowed_callers,
198 } => {
199 let mut map = serializer.serialize_map(None)?;
200 map.serialize_entry("name", name)?;
201 map.serialize_entry("description", description)?;
202 map.serialize_entry("input_schema", input_schema)?;
203 map.serialize_entry("input_examples", input_examples)?;
204 map.serialize_entry("strict", strict)?;
205 map.serialize_entry("allowed_callers", allowed_callers)?;
206 map.end()
207 }
208 Self::Native { tool_type, name, options } => {
209 if let Some(key) = ["type", "name"].into_iter().find(|key| options.contains_key(*key)) {
210 return Err(serde::ser::Error::custom(format!(
211 "Anthropic native tool extension key '{key}' collides with a reserved wire field"
212 )));
213 }
214 let mut map = serializer.serialize_map(None)?;
215 map.serialize_entry("type", tool_type)?;
216 map.serialize_entry("name", name)?;
217 for (key, value) in options {
218 map.serialize_entry(key, value)?;
219 }
220 map.end()
221 }
222 }
223 }
224}
225
226#[derive(Debug, Deserialize, Serialize, Clone)]
227pub struct AnthropicAdvisorCachingCompat {
228 #[serde(rename = "type")]
229 cache_type: String,
230 ttl: String,
231}
232
233#[derive(Debug, Serialize, Clone)]
234pub struct AnthropicMessagesResponse {
235 pub(crate) id: String,
236 pub(crate) r#type: String,
237 pub(crate) role: String,
238 pub(crate) model: String,
239 pub(crate) content: Vec<AnthropicContentBlock>,
240 pub(crate) stop_reason: Option<String>,
241 pub(crate) stop_sequence: Option<String>,
242 pub(crate) usage: AnthropicUsage,
243}
244
245#[derive(Debug, Serialize, Clone)]
246pub struct AnthropicUsage {
247 pub(crate) input_tokens: u32,
248 pub(crate) output_tokens: u32,
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn native_tool_rejects_reserved_extension_keys() {
257 let mut options = serde_json::Map::new();
258 options.insert("type".to_string(), json!("unexpected"));
259 let tool = AnthropicTool::Native {
260 tool_type: "web_search_20250305".to_string(),
261 name: "web_search".to_string(),
262 options,
263 };
264
265 let error = serde_json::to_value(tool).expect_err("reserved extension keys must be rejected");
266 assert!(error.to_string().contains("collides with a reserved wire field"));
267 }
268
269 #[test]
270 fn updates_display_is_only_forwarded_to_models_that_accept_it() {
271 use crate::providers::anthropic::request_builder::{RequestBuilderContext, convert_to_anthropic_format};
272 use vtcode_config::core::{AnthropicConfig, AnthropicPromptCacheSettings};
273
274 let payload_thinking = |model: &str| {
275 let request: AnthropicMessagesRequest = serde_json::from_value(json!({
276 "model": model,
277 "max_tokens": 1024,
278 "messages": [{ "role": "user", "content": "hello" }],
279 "thinking": { "type": "adaptive", "display": "updates" },
280 }))
281 .expect("compat request");
282 let llm_request = convert_anthropic_to_llm_request(request).expect("converted request");
283 let anthropic_config = AnthropicConfig::default();
284 let prompt_cache_settings = AnthropicPromptCacheSettings::default();
285 let ctx = RequestBuilderContext {
286 prompt_cache_enabled: false,
287 prompt_cache_settings: &prompt_cache_settings,
288 anthropic_config: &anthropic_config,
289 model,
290 server_side_fallbacks_available: false,
291 };
292 convert_to_anthropic_format(&llm_request, &ctx).expect("payload conversion")["thinking"].clone()
293 };
294
295 assert_eq!(payload_thinking("claude-opus-5-5"), json!({ "type": "adaptive", "display": "updates" }));
296 for model in ["claude-opus-4-8", "claude-sonnet-5", "claude-opus-5"] {
297 assert_eq!(payload_thinking(model), json!({ "type": "adaptive" }), "{model}");
298 }
299 }
300}
301
302#[derive(Debug, Serialize)]
303#[serde(tag = "type")]
304pub enum AnthropicStreamEvent {
305 #[serde(rename = "message_start")]
306 MessageStart { message: AnthropicMessagesResponse },
307 #[serde(rename = "content_block_start")]
308 ContentBlockStart {
309 index: u32,
310 content_block: AnthropicContentBlock,
311 },
312 #[serde(rename = "content_block_delta")]
313 ContentBlockDelta { index: u32, delta: AnthropicContentDelta },
314 #[serde(rename = "content_block_stop")]
315 ContentBlockStop { index: u32 },
316 #[serde(rename = "message_delta")]
317 MessageDelta {
318 delta: AnthropicDelta,
319 usage: AnthropicUsage,
320 },
321 #[serde(rename = "message_stop")]
322 MessageStop,
323 #[serde(rename = "ping")]
324 Ping {},
325 #[serde(rename = "error")]
326 Error { error: AnthropicError },
327}
328
329#[derive(Debug, Serialize)]
330#[serde(tag = "type")]
331pub enum AnthropicContentDelta {
332 #[serde(rename = "text_delta")]
333 TextDelta { text: String },
334 #[serde(rename = "input_json_delta")]
335 InputJsonDelta { partial_json: String },
336 #[serde(rename = "thinking_delta")]
337 ThinkingDelta { thinking: String },
338 #[serde(rename = "signature_delta")]
339 SignatureDelta { signature: String },
340}
341
342#[derive(Debug, Serialize)]
343pub struct AnthropicDelta {
344 pub(crate) stop_reason: Option<String>,
345 pub(crate) stop_sequence: Option<String>,
346}
347
348#[derive(Debug, Serialize)]
349pub struct AnthropicError {
350 pub(crate) r#type: String,
351 pub(crate) message: String,
352}
353
354#[derive(Default)]
355struct ConvertedAnthropicBlocks {
356 content_parts: Vec<ContentPart>,
357 tool_calls: Vec<ToolCall>,
358 reasoning_chunks: Vec<String>,
359 reasoning_details: Vec<Value>,
360 emitted_messages: Vec<Message>,
361}
362
363pub(crate) fn convert_anthropic_to_llm_request(request: AnthropicMessagesRequest) -> Result<LLMRequest, LLMError> {
364 let (tool_choice, parallel_tool_config) = parse_anthropic_tool_choice(request.tool_choice.as_ref());
365 let effort = request.output_config.as_ref().and_then(|config| config.effort.clone());
366 let output_format = request.output_config.as_ref().and_then(|config| {
367 config.format.as_ref().and_then(|format| match format {
368 AnthropicOutputFormat::JsonSchema { schema } => Some(schema.clone()),
369 AnthropicOutputFormat::Unknown => None,
370 })
371 });
372 let task_budget_tokens = request
373 .output_config
374 .as_ref()
375 .and_then(|config| config.task_budget.as_ref())
376 .map(|budget| budget.total);
377 let anthropic_request_overrides = Some(AnthropicRequestOverrides {
378 speed: None,
379 thinking_mode: compatibility_thinking_mode(&request.model, request.thinking.as_ref()),
380 thinking_display: compatibility_thinking_display(request.thinking.as_ref()),
381 effort: effort
382 .as_ref()
383 .map(|effort| AnthropicOptionalStringOverride::Explicit(effort.clone()))
384 .unwrap_or(AnthropicOptionalStringOverride::Omit),
385 task_budget_tokens: task_budget_tokens
386 .map(AnthropicOptionalU32Override::Explicit)
387 .unwrap_or(AnthropicOptionalU32Override::Omit),
388 });
389
390 let system_prompt = request
391 .system
392 .map(extract_system_prompt_text)
393 .filter(|value| !value.is_empty())
394 .map(Arc::from);
395
396 let mut messages = Vec::new();
397 for anthropic_msg in request.messages {
398 let role = anthropic_role_to_message_role(&anthropic_msg.role);
399
400 match anthropic_msg.content {
401 AnthropicContent::Text(text) => {
402 if !text.is_empty() {
403 messages.push(Message::base(role, MessageContent::Text(text)));
404 }
405 }
406 AnthropicContent::Blocks(blocks) => {
407 if role == MessageRole::Assistant
411 && blocks
412 .iter()
413 .any(|block| matches!(block, AnthropicContentBlock::Document { .. }))
414 {
415 let formatted_error = error_display::format_llm_error(
416 "Anthropic",
417 "document content blocks are only accepted in user messages; send the document in a user message instead.",
418 );
419 return Err(LLMError::InvalidRequest { message: formatted_error, metadata: None });
420 }
421
422 let converted = convert_anthropic_blocks(&blocks);
423 messages.extend(converted.emitted_messages);
424
425 if !converted.content_parts.is_empty()
426 || !converted.tool_calls.is_empty()
427 || !converted.reasoning_chunks.is_empty()
428 {
429 let mut message = Message::base(role, message_content_from_parts(converted.content_parts));
430 if !converted.tool_calls.is_empty() {
431 message.tool_calls = Some(converted.tool_calls);
432 }
433 if !converted.reasoning_chunks.is_empty() && message.role == MessageRole::Assistant {
434 message.reasoning = Some(converted.reasoning_chunks.join("\n"));
435 }
436 if !converted.reasoning_details.is_empty() && message.role == MessageRole::Assistant {
437 message.reasoning_details = Some(converted.reasoning_details);
438 }
439 messages.push(message);
440 }
441 }
442 }
443 }
444
445 let tools = if let Some(anthropic_tools) = request.tools {
446 let mut converted_tools = Vec::new();
447 for tool in anthropic_tools {
448 let tool_def = match tool {
449 AnthropicTool::Advisor {
450 tool_type: _,
451 name: _,
452 model,
453 max_uses,
454 max_tokens,
455 caching,
456 } => {
457 let mut options = serde_json::Map::new();
458 options.insert("model".to_string(), Value::String(model));
459 if let Some(max_uses) = max_uses {
460 options.insert("max_uses".to_string(), Value::Number(max_uses.into()));
461 }
462 if let Some(max_tokens) = max_tokens {
463 options.insert("max_tokens".to_string(), Value::Number(max_tokens.into()));
464 }
465 if let Some(caching) = caching {
466 options.insert(
467 "caching".to_string(),
468 json!({
469 "type": caching.cache_type,
470 "ttl": caching.ttl,
471 }),
472 );
473 }
474 ToolDefinition {
475 tool_type: "advisor_20260301".to_string(),
476 function: None,
477 allowed_callers: None,
478 input_examples: None,
479 web_search: None,
480 hosted_tool_config: None,
481 shell: None,
482 grammar: None,
483 strict: None,
484 defer_loading: None,
485 namespace: None,
486 advisor: Some(Value::Object(options)),
487 }
488 }
489 AnthropicTool::Function {
490 name,
491 description,
492 input_schema,
493 input_examples,
494 strict,
495 allowed_callers,
496 } => {
497 let mut tool = ToolDefinition::function(name, description.unwrap_or_default(), input_schema);
498 tool.input_examples = input_examples;
499 tool.strict = strict;
500 tool.allowed_callers = allowed_callers;
501 tool
502 }
503 AnthropicTool::Native { tool_type, options, .. } => {
504 if tool_type.starts_with("web_search_") {
505 ToolDefinition {
506 tool_type,
507 function: None,
508 allowed_callers: None,
509 input_examples: None,
510 web_search: (!options.is_empty()).then_some(Value::Object(options)),
511 hosted_tool_config: None,
512 shell: None,
513 grammar: None,
514 strict: None,
515 defer_loading: None,
516 namespace: None,
517 advisor: None,
518 }
519 } else if tool_type.starts_with("code_execution_") || tool_type.starts_with("memory_") {
520 ToolDefinition {
521 tool_type,
522 function: None,
523 allowed_callers: None,
524 input_examples: None,
525 web_search: None,
526 hosted_tool_config: None,
527 shell: None,
528 grammar: None,
529 strict: None,
530 defer_loading: None,
531 namespace: None,
532 advisor: None,
533 }
534 } else {
535 continue;
536 }
537 }
538 };
539 converted_tools.push(tool_def);
540 }
541 if converted_tools.is_empty() {
542 None
543 } else {
544 Some(Arc::new(converted_tools))
545 }
546 } else {
547 None
548 };
549
550 Ok(LLMRequest {
551 messages: Arc::new(messages),
552 system_prompt,
553 tools,
554 model: request.model,
555 max_tokens: Some(request.max_tokens),
556 temperature: None,
557 stream: request.stream,
558 output_format,
559 tool_choice,
560 parallel_tool_calls: None,
561 parallel_tool_config,
562 reasoning_effort: None,
563 effort,
564 verbosity: None,
565 do_sample: None,
566 top_p: None,
567 top_k: None,
568 presence_penalty: None,
569 frequency_penalty: None,
570 stop_sequences: request.stop_sequences,
571 thinking_budget: None,
572 betas: request.betas,
573 context_management: request.context_management,
574 coding_agent_settings: None,
575 metadata: None,
576 previous_response_id: None,
577 response_store: None,
578 responses_include: None,
579 service_tier: None,
580 prompt_cache_key: None,
581 prompt_cache_profile: None,
582 fallbacks: None,
583 fallback_credit_token: None,
584 anthropic_request_overrides,
585 })
586}
587
588pub(crate) fn convert_llm_to_anthropic_response(response: LLMResponse) -> AnthropicMessagesResponse {
589 use uuid::Uuid;
590
591 let mut content_blocks = Vec::new();
592 let mut preserved_reasoning = false;
593
594 if let Some(reasoning_details) = response.reasoning_details.as_ref() {
595 for detail in reasoning_details {
596 let Some(normalized) = normalize_reasoning_detail_object(&Value::String(detail.clone())) else {
597 continue;
598 };
599
600 match normalized.get("type").and_then(|value| value.as_str()) {
601 Some("thinking") => {
602 let thinking = normalized
603 .get("thinking")
604 .and_then(|value| value.as_str())
605 .unwrap_or_default()
606 .to_string();
607 let signature = normalized
608 .get("signature")
609 .and_then(|value| value.as_str())
610 .map(ToOwned::to_owned);
611 content_blocks.push(AnthropicContentBlock::Thinking { thinking, signature });
612 preserved_reasoning = true;
613 }
614 Some("redacted_thinking") => {
615 let data = normalized
616 .get("data")
617 .and_then(|value| value.as_str())
618 .unwrap_or_default()
619 .to_string();
620 content_blocks.push(AnthropicContentBlock::RedactedThinking { data });
621 preserved_reasoning = true;
622 }
623 _ => {}
624 }
625 }
626 }
627
628 if !preserved_reasoning
629 && let Some(reasoning) = response.reasoning.as_ref()
630 && !reasoning.trim().is_empty()
631 {
632 content_blocks.push(AnthropicContentBlock::Thinking { thinking: reasoning.clone(), signature: None });
633 }
634
635 if let Some(content) = response.content.as_ref()
636 && !content.is_empty()
637 {
638 content_blocks.push(AnthropicContentBlock::Text {
639 text: content.clone(),
640 citations: None,
641 cache_control: None,
642 });
643 }
644
645 if let Some(tool_calls) = response.tool_calls.as_ref() {
646 for call in tool_calls {
647 if let Some(func) = &call.function {
648 let input = call
649 .parsed_arguments()
650 .unwrap_or_else(|_| Value::String(func.arguments.clone()));
651 content_blocks.push(AnthropicContentBlock::ToolUse {
652 id: call.id.clone(),
653 name: func.name.clone(),
654 input,
655 });
656 }
657 }
658 }
659
660 let usage = response.usage.unwrap_or_default();
661 let model = if response.model.trim().is_empty() {
662 "unknown".to_string()
663 } else {
664 response.model
665 };
666
667 AnthropicMessagesResponse {
668 id: Uuid::new_v4().to_string(),
669 r#type: "message".to_string(),
670 role: "assistant".to_string(),
671 model,
672 content: content_blocks,
673 stop_reason: Some(anthropic_stop_reason(response.finish_reason)),
674 stop_sequence: None,
675 usage: AnthropicUsage {
676 input_tokens: usage.prompt_tokens,
677 output_tokens: usage.completion_tokens,
678 },
679 }
680}
681
682pub(crate) fn anthropic_stop_reason(finish_reason: FinishReason) -> String {
683 match finish_reason {
684 FinishReason::Stop => "end_turn".to_string(),
685 FinishReason::Length => "max_tokens".to_string(),
686 FinishReason::ToolCalls => "tool_use".to_string(),
687 FinishReason::ContentFilter => "content_filter".to_string(),
688 FinishReason::Pause => "pause_turn".to_string(),
689 FinishReason::Refusal => "refusal".to_string(),
690 FinishReason::Error(message) => message,
691 }
692}
693
694fn parse_anthropic_tool_choice(tool_choice: Option<&Value>) -> (Option<ToolChoice>, Option<Box<ParallelToolConfig>>) {
695 let Some(choice) = tool_choice else {
696 return (None, None);
697 };
698 let Some(choice_obj) = choice.as_object() else {
699 return (None, None);
700 };
701
702 let disable_parallel_tool_use = choice_obj
703 .get("disable_parallel_tool_use")
704 .and_then(Value::as_bool)
705 .unwrap_or(false);
706
707 let parsed_tool_choice = match choice_obj.get("type").and_then(Value::as_str) {
708 Some("auto") => Some(ToolChoice::Auto),
709 Some("none") => Some(ToolChoice::None),
710 Some("any") => Some(ToolChoice::Any),
711 Some("tool") => choice_obj
712 .get("name")
713 .and_then(Value::as_str)
714 .map(|name| ToolChoice::function(name.to_string())),
715 _ => None,
716 };
717
718 let parallel_tool_config = disable_parallel_tool_use.then(|| {
719 Box::new(ParallelToolConfig {
720 disable_parallel_tool_use: true,
721 max_parallel_tools: Some(1),
722 encourage_parallel: false,
723 })
724 });
725
726 (parsed_tool_choice, parallel_tool_config)
727}
728
729fn anthropic_role_to_message_role(role: &str) -> MessageRole {
730 match role {
731 "assistant" => MessageRole::Assistant,
732 "system" => MessageRole::System,
733 "tool" => MessageRole::Tool,
734 _ => MessageRole::User,
735 }
736}
737
738fn extract_system_prompt_text(system_prompt: AnthropicSystemPrompt) -> String {
739 match system_prompt {
740 AnthropicSystemPrompt::Text(text) => text,
741 AnthropicSystemPrompt::Blocks(blocks) => blocks
742 .iter()
743 .map(anthropic_block_text)
744 .filter(|text| !text.is_empty())
745 .collect::<Vec<_>>()
746 .join("\n"),
747 }
748}
749
750fn convert_anthropic_blocks(blocks: &[AnthropicContentBlock]) -> ConvertedAnthropicBlocks {
751 let mut converted = ConvertedAnthropicBlocks::default();
752
753 for block in blocks {
754 match block {
755 AnthropicContentBlock::Text { text, .. } => {
756 converted.content_parts.push(ContentPart::text(text.clone()));
757 }
758 AnthropicContentBlock::Image { source } => {
759 converted
760 .content_parts
761 .push(ContentPart::image(source.data.clone(), source.media_type.clone()));
762 }
763 AnthropicContentBlock::ToolUse { id, name, input }
764 | AnthropicContentBlock::ServerToolUse { id, name, input } => {
765 converted
766 .tool_calls
767 .push(ToolCall::function(id.clone(), name.clone(), input.to_string()));
768 }
769 AnthropicContentBlock::ToolResult { tool_use_id, content, .. } => converted
770 .emitted_messages
771 .push(Message::tool_response(tool_use_id.clone(), anthropic_content_text(content))),
772 AnthropicContentBlock::Thinking { thinking, signature } => {
773 converted.reasoning_chunks.push(thinking.clone());
774 let mut detail = json!({
775 "type": "thinking",
776 "thinking": thinking,
777 });
778 if let Some(signature) = signature
779 && let Some(obj) = detail.as_object_mut()
780 {
781 obj.insert("signature".to_string(), Value::String(signature.clone()));
782 }
783 converted.reasoning_details.push(detail);
784 }
785 AnthropicContentBlock::RedactedThinking { data } => {
786 converted.reasoning_details.push(json!({
787 "type": "redacted_thinking",
788 "data": data,
789 }));
790 }
791 AnthropicContentBlock::ContainerUpload { file_id } => {
792 converted.content_parts.push(ContentPart::file_from_id(file_id.clone()));
793 }
794 AnthropicContentBlock::Document { .. } => {
795 converted.content_parts.push(ContentPart::text("[Document]".to_string()));
796 }
797 AnthropicContentBlock::CodeExecutionToolResult { tool_use_id, content }
798 | AnthropicContentBlock::BashCodeExecutionToolResult { tool_use_id, content }
799 | AnthropicContentBlock::TextEditorCodeExecutionToolResult { tool_use_id, content }
800 | AnthropicContentBlock::WebSearchToolResult { tool_use_id, content }
801 | AnthropicContentBlock::AdvisorToolResult { tool_use_id, content } => converted
802 .emitted_messages
803 .push(Message::tool_response(tool_use_id.clone(), serialize_value(content))),
804 AnthropicContentBlock::Unknown => {}
806 }
807 }
808
809 converted
810}
811
812fn message_content_from_parts(parts: Vec<ContentPart>) -> MessageContent {
813 if parts.len() == 1
814 && let ContentPart::Text { text } = &parts[0]
815 {
816 return MessageContent::Text(text.clone());
817 }
818
819 MessageContent::Parts(parts)
820}
821
822fn anthropic_content_text(content: &AnthropicContent) -> String {
823 match content {
824 AnthropicContent::Text(text) => text.clone(),
825 AnthropicContent::Blocks(blocks) => blocks
826 .iter()
827 .map(anthropic_block_text)
828 .filter(|text| !text.is_empty())
829 .collect::<Vec<_>>()
830 .join("\n"),
831 }
832}
833
834fn anthropic_block_text(block: &AnthropicContentBlock) -> String {
835 match block {
836 AnthropicContentBlock::Text { text, .. } => text.clone(),
837 AnthropicContentBlock::Thinking { thinking, .. } => thinking.clone(),
838 AnthropicContentBlock::RedactedThinking { .. } => "[REDACTED THINKING]".to_string(),
839 AnthropicContentBlock::Image { .. } => "[Image]".to_string(),
840 AnthropicContentBlock::ContainerUpload { file_id } => format!("[File: {file_id}]"),
841 AnthropicContentBlock::Document { .. } => "[Document]".to_string(),
842 AnthropicContentBlock::ToolUse { name, input, .. }
843 | AnthropicContentBlock::ServerToolUse { name, input, .. } => {
844 format!("[Tool call: {name} with args: {input}]")
845 }
846 AnthropicContentBlock::ToolResult { tool_use_id, content, .. } => {
847 format!("[Tool result {}: {}]", tool_use_id, anthropic_content_text(content))
848 }
849 AnthropicContentBlock::CodeExecutionToolResult { tool_use_id, content }
850 | AnthropicContentBlock::BashCodeExecutionToolResult { tool_use_id, content }
851 | AnthropicContentBlock::TextEditorCodeExecutionToolResult { tool_use_id, content }
852 | AnthropicContentBlock::WebSearchToolResult { tool_use_id, content }
853 | AnthropicContentBlock::AdvisorToolResult { tool_use_id, content } => {
854 format!("[Tool result {}: {}]", tool_use_id, serialize_value(content))
855 }
856 AnthropicContentBlock::Unknown => "[Unknown Content Block]".to_string(),
857 }
858}
859
860fn compatibility_thinking_mode(_model: &str, thinking: Option<&ThinkingConfig>) -> AnthropicThinkingModeOverride {
861 match thinking {
862 Some(ThinkingConfig::Adaptive { .. }) => AnthropicThinkingModeOverride::Adaptive,
863 Some(ThinkingConfig::Enabled { budget_tokens, .. }) => {
864 AnthropicThinkingModeOverride::ManualBudget(*budget_tokens)
865 }
866 Some(ThinkingConfig::Disabled) | Some(ThinkingConfig::BetweenTools) | Some(ThinkingConfig::Unknown) => {
869 AnthropicThinkingModeOverride::Disabled
870 }
871 None => AnthropicThinkingModeOverride::Disabled,
872 }
873}
874
875fn compatibility_thinking_display(thinking: Option<&ThinkingConfig>) -> AnthropicThinkingDisplayOverride {
876 let display = match thinking {
877 Some(ThinkingConfig::Adaptive { display }) | Some(ThinkingConfig::Enabled { display, .. }) => *display,
878 Some(ThinkingConfig::Disabled) | Some(ThinkingConfig::BetweenTools) | Some(ThinkingConfig::Unknown) | None => {
879 None
880 }
881 };
882
883 match display {
884 Some(ThinkingDisplay::Summarized) => AnthropicThinkingDisplayOverride::Summarized,
885 Some(ThinkingDisplay::Omitted) => AnthropicThinkingDisplayOverride::Omitted,
886 Some(ThinkingDisplay::Updates) => AnthropicThinkingDisplayOverride::Updates,
887 Some(ThinkingDisplay::Unknown) | None => AnthropicThinkingDisplayOverride::Inherit,
888 }
889}
890
891fn serialize_value(value: &Value) -> String {
892 serde_json::to_string(value).unwrap_or_else(|_| json!({ "value": value }).to_string())
893}