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