Skip to main content

vtcode_core/llm/providers/anthropic/
compat.rs

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