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#[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}