Skip to main content

vtcode_llm/providers/anthropic/
compat.rs

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/// Anthropic Messages API request.
16#[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    /// Catch-all for unknown content block types added by the Anthropic API.
104    #[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            // Unknown content block types are silently skipped.
776            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        // `between_tools` is the "thinking off" spelling for the models that
837        // reject `disabled`; the builder rewrites it back for such a model.
838        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}