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        anthropic_request_overrides,
410    }
411}
412
413pub fn convert_llm_to_anthropic_response(response: LLMResponse) -> AnthropicMessagesResponse {
414    use uuid::Uuid;
415
416    let mut content_blocks = Vec::new();
417    let mut preserved_reasoning = false;
418
419    if let Some(reasoning_details) = response.reasoning_details.as_ref() {
420        for detail in reasoning_details {
421            let Some(normalized) =
422                normalize_reasoning_detail_object(&Value::String(detail.clone()))
423            else {
424                continue;
425            };
426
427            match normalized.get("type").and_then(|value| value.as_str()) {
428                Some("thinking") => {
429                    let thinking = normalized
430                        .get("thinking")
431                        .and_then(|value| value.as_str())
432                        .unwrap_or_default()
433                        .to_string();
434                    let signature = normalized
435                        .get("signature")
436                        .and_then(|value| value.as_str())
437                        .map(ToOwned::to_owned);
438                    content_blocks.push(AnthropicContentBlock::Thinking {
439                        thinking,
440                        signature,
441                    });
442                    preserved_reasoning = true;
443                }
444                Some("redacted_thinking") => {
445                    let data = normalized
446                        .get("data")
447                        .and_then(|value| value.as_str())
448                        .unwrap_or_default()
449                        .to_string();
450                    content_blocks.push(AnthropicContentBlock::RedactedThinking { data });
451                    preserved_reasoning = true;
452                }
453                _ => {}
454            }
455        }
456    }
457
458    if !preserved_reasoning
459        && let Some(reasoning) = response.reasoning.as_ref()
460        && !reasoning.trim().is_empty()
461    {
462        content_blocks.push(AnthropicContentBlock::Thinking {
463            thinking: reasoning.clone(),
464            signature: None,
465        });
466    }
467
468    if let Some(content) = response.content.as_ref()
469        && !content.is_empty()
470    {
471        content_blocks.push(AnthropicContentBlock::Text {
472            text: content.clone(),
473            citations: None,
474            cache_control: None,
475        });
476    }
477
478    if let Some(tool_calls) = response.tool_calls.as_ref() {
479        for call in tool_calls {
480            if let Some(func) = &call.function {
481                let input = call
482                    .parsed_arguments()
483                    .unwrap_or_else(|_| Value::String(func.arguments.clone()));
484                content_blocks.push(AnthropicContentBlock::ToolUse {
485                    id: call.id.clone(),
486                    name: func.name.clone(),
487                    input,
488                });
489            }
490        }
491    }
492
493    let usage = response.usage.unwrap_or_default();
494    let model = if response.model.trim().is_empty() {
495        "unknown".to_string()
496    } else {
497        response.model
498    };
499
500    AnthropicMessagesResponse {
501        id: Uuid::new_v4().to_string(),
502        r#type: "message".to_string(),
503        role: "assistant".to_string(),
504        model,
505        content: content_blocks,
506        stop_reason: Some(anthropic_stop_reason(response.finish_reason)),
507        stop_sequence: None,
508        usage: AnthropicUsage {
509            input_tokens: usage.prompt_tokens,
510            output_tokens: usage.completion_tokens,
511        },
512    }
513}
514
515pub(crate) fn anthropic_stop_reason(finish_reason: FinishReason) -> String {
516    match finish_reason {
517        FinishReason::Stop => "end_turn".to_string(),
518        FinishReason::Length => "max_tokens".to_string(),
519        FinishReason::ToolCalls => "tool_use".to_string(),
520        FinishReason::ContentFilter => "content_filter".to_string(),
521        FinishReason::Pause => "pause_turn".to_string(),
522        FinishReason::Refusal => "refusal".to_string(),
523        FinishReason::Error(message) => message,
524    }
525}
526
527fn parse_anthropic_tool_choice(
528    tool_choice: Option<&Value>,
529) -> (Option<ToolChoice>, Option<Box<ParallelToolConfig>>) {
530    let Some(choice) = tool_choice else {
531        return (None, None);
532    };
533    let Some(choice_obj) = choice.as_object() else {
534        return (None, None);
535    };
536
537    let disable_parallel_tool_use = choice_obj
538        .get("disable_parallel_tool_use")
539        .and_then(Value::as_bool)
540        .unwrap_or(false);
541
542    let parsed_tool_choice = match choice_obj.get("type").and_then(Value::as_str) {
543        Some("auto") => Some(ToolChoice::Auto),
544        Some("none") => Some(ToolChoice::None),
545        Some("any") => Some(ToolChoice::Any),
546        Some("tool") => choice_obj
547            .get("name")
548            .and_then(Value::as_str)
549            .map(|name| ToolChoice::function(name.to_string())),
550        _ => None,
551    };
552
553    let parallel_tool_config = disable_parallel_tool_use.then(|| {
554        Box::new(ParallelToolConfig {
555            disable_parallel_tool_use: true,
556            max_parallel_tools: Some(1),
557            encourage_parallel: false,
558        })
559    });
560
561    (parsed_tool_choice, parallel_tool_config)
562}
563
564fn anthropic_role_to_message_role(role: &str) -> MessageRole {
565    match role {
566        "assistant" => MessageRole::Assistant,
567        "system" => MessageRole::System,
568        "tool" => MessageRole::Tool,
569        _ => MessageRole::User,
570    }
571}
572
573fn extract_system_prompt_text(system_prompt: AnthropicSystemPrompt) -> String {
574    match system_prompt {
575        AnthropicSystemPrompt::Text(text) => text,
576        AnthropicSystemPrompt::Blocks(blocks) => blocks
577            .iter()
578            .map(anthropic_block_text)
579            .filter(|text| !text.is_empty())
580            .collect::<Vec<_>>()
581            .join("\n"),
582    }
583}
584
585fn convert_anthropic_blocks(blocks: &[AnthropicContentBlock]) -> ConvertedAnthropicBlocks {
586    let mut converted = ConvertedAnthropicBlocks::default();
587
588    for block in blocks {
589        match block {
590            AnthropicContentBlock::Text { text, .. } => {
591                converted
592                    .content_parts
593                    .push(ContentPart::text(text.clone()));
594            }
595            AnthropicContentBlock::Image { source } => {
596                converted.content_parts.push(ContentPart::image(
597                    source.data.clone(),
598                    source.media_type.clone(),
599                ));
600            }
601            AnthropicContentBlock::ToolUse { id, name, input }
602            | AnthropicContentBlock::ServerToolUse { id, name, input } => {
603                converted.tool_calls.push(ToolCall::function(
604                    id.clone(),
605                    name.clone(),
606                    input.to_string(),
607                ));
608            }
609            AnthropicContentBlock::ToolResult {
610                tool_use_id,
611                content,
612                ..
613            } => converted.emitted_messages.push(Message::tool_response(
614                tool_use_id.clone(),
615                anthropic_content_text(content),
616            )),
617            AnthropicContentBlock::Thinking {
618                thinking,
619                signature,
620            } => {
621                converted.reasoning_chunks.push(thinking.clone());
622                let mut detail = json!({
623                    "type": "thinking",
624                    "thinking": thinking,
625                });
626                if let Some(signature) = signature
627                    && let Some(obj) = detail.as_object_mut()
628                {
629                    obj.insert("signature".to_string(), Value::String(signature.clone()));
630                }
631                converted.reasoning_details.push(detail);
632            }
633            AnthropicContentBlock::RedactedThinking { data } => {
634                converted.reasoning_details.push(json!({
635                    "type": "redacted_thinking",
636                    "data": data,
637                }));
638            }
639            AnthropicContentBlock::ContainerUpload { file_id } => {
640                converted
641                    .content_parts
642                    .push(ContentPart::file_from_id(file_id.clone()));
643            }
644            AnthropicContentBlock::CodeExecutionToolResult {
645                tool_use_id,
646                content,
647            }
648            | AnthropicContentBlock::BashCodeExecutionToolResult {
649                tool_use_id,
650                content,
651            }
652            | AnthropicContentBlock::TextEditorCodeExecutionToolResult {
653                tool_use_id,
654                content,
655            }
656            | AnthropicContentBlock::WebSearchToolResult {
657                tool_use_id,
658                content,
659            } => converted.emitted_messages.push(Message::tool_response(
660                tool_use_id.clone(),
661                serialize_value(content),
662            )),
663        }
664    }
665
666    converted
667}
668
669fn message_content_from_parts(parts: Vec<ContentPart>) -> MessageContent {
670    if parts.len() == 1
671        && let ContentPart::Text { text } = &parts[0]
672    {
673        return MessageContent::Text(text.clone());
674    }
675
676    MessageContent::Parts(parts)
677}
678
679fn anthropic_content_text(content: &AnthropicContent) -> String {
680    match content {
681        AnthropicContent::Text(text) => text.clone(),
682        AnthropicContent::Blocks(blocks) => blocks
683            .iter()
684            .map(anthropic_block_text)
685            .filter(|text| !text.is_empty())
686            .collect::<Vec<_>>()
687            .join("\n"),
688    }
689}
690
691fn anthropic_block_text(block: &AnthropicContentBlock) -> String {
692    match block {
693        AnthropicContentBlock::Text { text, .. } => text.clone(),
694        AnthropicContentBlock::Thinking { thinking, .. } => thinking.clone(),
695        AnthropicContentBlock::RedactedThinking { .. } => "[REDACTED THINKING]".to_string(),
696        AnthropicContentBlock::Image { .. } => "[Image]".to_string(),
697        AnthropicContentBlock::ContainerUpload { file_id } => format!("[File: {file_id}]"),
698        AnthropicContentBlock::ToolUse { name, input, .. }
699        | AnthropicContentBlock::ServerToolUse { name, input, .. } => {
700            format!("[Tool call: {name} with args: {input}]")
701        }
702        AnthropicContentBlock::ToolResult {
703            tool_use_id,
704            content,
705            ..
706        } => format!(
707            "[Tool result {}: {}]",
708            tool_use_id,
709            anthropic_content_text(content)
710        ),
711        AnthropicContentBlock::CodeExecutionToolResult {
712            tool_use_id,
713            content,
714        }
715        | AnthropicContentBlock::BashCodeExecutionToolResult {
716            tool_use_id,
717            content,
718        }
719        | AnthropicContentBlock::TextEditorCodeExecutionToolResult {
720            tool_use_id,
721            content,
722        }
723        | AnthropicContentBlock::WebSearchToolResult {
724            tool_use_id,
725            content,
726        } => {
727            format!(
728                "[Tool result {}: {}]",
729                tool_use_id,
730                serialize_value(content)
731            )
732        }
733    }
734}
735
736fn compatibility_thinking_mode(
737    model: &str,
738    thinking: Option<&ThinkingConfig>,
739) -> AnthropicThinkingModeOverride {
740    match thinking {
741        Some(ThinkingConfig::Adaptive { .. }) => AnthropicThinkingModeOverride::Adaptive,
742        Some(ThinkingConfig::Enabled { budget_tokens, .. }) => {
743            AnthropicThinkingModeOverride::ManualBudget(*budget_tokens)
744        }
745        Some(ThinkingConfig::Disabled) => AnthropicThinkingModeOverride::Disabled,
746        None => {
747            let is_mythos = model
748                == crate::config::constants::models::anthropic::CLAUDE_MYTHOS_PREVIEW
749                || model
750                    .contains(crate::config::constants::models::anthropic::CLAUDE_MYTHOS_PREVIEW);
751            if is_mythos {
752                AnthropicThinkingModeOverride::Adaptive
753            } else {
754                AnthropicThinkingModeOverride::Disabled
755            }
756        }
757    }
758}
759
760fn compatibility_thinking_display(
761    thinking: Option<&ThinkingConfig>,
762) -> AnthropicThinkingDisplayOverride {
763    let display = match thinking {
764        Some(ThinkingConfig::Adaptive { display })
765        | Some(ThinkingConfig::Enabled { display, .. }) => *display,
766        Some(ThinkingConfig::Disabled) | None => None,
767    };
768
769    match display {
770        Some(ThinkingDisplay::Summarized) => AnthropicThinkingDisplayOverride::Summarized,
771        Some(ThinkingDisplay::Omitted) => AnthropicThinkingDisplayOverride::Omitted,
772        None => AnthropicThinkingDisplayOverride::Inherit,
773    }
774}
775
776fn serialize_value(value: &Value) -> String {
777    serde_json::to_string(value).unwrap_or_else(|_| json!({ "value": value }).to_string())
778}