Skip to main content

modelmux/converter/
openai_to_anthropic.rs

1//!
2//! OpenAI to Anthropic format converter for API request translation.
3//!
4//! Converts OpenAI-compatible chat completion requests to Anthropic/Vertex AI format.
5//! Handles message conversion, tool calling, and streaming configuration while
6//! maintaining semantic equivalence between the two API formats.
7//!
8//! Authors:
9//!   Jaro <yarenty@gmail.com>
10//!
11//! Copyright (c) 2026 SkyCorp
12
13/* --- uses ------------------------------------------------------------------------------------ */
14
15use serde::{Deserialize, Serialize};
16use serde_json::json;
17
18use crate::config::LogLevel;
19use crate::error::{ProxyError, Result};
20
21/* --- helper functions ----------------------------------------------------------------------- */
22
23///
24/// Custom serialization helper for tools field.
25///
26/// Skips serialization when tools is None or empty to avoid sending invalid data to Vertex AI.
27///
28/// # Arguments
29///  * `tools` - optional tools vector
30///
31/// # Returns
32///  * true if field should be skipped (None or empty), false otherwise
33fn skip_empty_tools(tools: &Option<Vec<AnthropicTool>>) -> bool {
34    match tools {
35        None => true,
36        Some(vec) => vec.is_empty(),
37    }
38}
39
40/* --- types ----------------------------------------------------------------------------------- */
41
42///
43/// OpenAI chat completion request structure.
44///
45/// Represents an incoming request in OpenAI's chat completions API format.
46/// Contains messages, model configuration, and optional tool definitions.
47#[derive(Debug, Deserialize)]
48pub struct OpenAiRequest {
49    /** the model identifier to use for completion */
50    pub model: Option<String>,
51    /** conversation messages array */
52    pub messages: Vec<OpenAiMessage>,
53    /** maximum number of tokens to generate */
54    pub max_tokens: Option<u32>,
55    /** sampling temperature for response generation */
56    pub temperature: Option<f64>,
57    /** whether to stream the response */
58    pub stream: Option<bool>,
59    /** available tools for function calling */
60    pub tools: Option<Vec<OpenAiTool>>,
61    /** tool choice configuration */
62    pub tool_choice: Option<OpenAiToolChoice>,
63}
64
65///
66/// OpenAI message structure within a chat completion request.
67///
68/// Represents a single message in the conversation with role-based content
69/// and optional tool call information.
70#[derive(Debug, Deserialize)]
71pub struct OpenAiMessage {
72    /** message role: system, user, assistant, or tool */
73    pub role: String,
74    /** message content, can be string or structured blocks */
75    pub content: Option<OpenAiContent>,
76    /** tool calls made by the assistant */
77    pub tool_calls: Option<Vec<OpenAiToolCall>>,
78    /** tool call ID for tool response messages */
79    #[serde(rename = "tool_call_id")]
80    pub tool_call_id: Option<String>,
81}
82
83///
84/// OpenAI content union type for flexible message content.
85///
86/// Supports both simple string content and structured content blocks
87/// for multimodal messages including text and images.
88#[derive(Debug, Deserialize)]
89#[serde(untagged)]
90pub enum OpenAiContent {
91    /** simple string content */
92    String(String),
93    /** structured content blocks array */
94    Array(Vec<OpenAiContentBlock>),
95}
96
97///
98/// OpenAI structured content block for multimodal messages.
99///
100/// Represents individual content elements within a message, supporting
101/// text and image content types with appropriate metadata.
102#[derive(Debug, Deserialize)]
103pub struct OpenAiContentBlock {
104    /** content block type: text or image_url */
105    #[serde(rename = "type")]
106    pub block_type: String,
107    /** text content for text blocks */
108    pub text: Option<String>,
109    /** image URL reference for image blocks */
110    #[serde(rename = "image_url")]
111    pub image_url: Option<ImageUrl>,
112}
113
114///
115/// Image URL reference structure for image content blocks.
116///
117/// Contains the URL pointing to the image resource.
118#[derive(Debug, Deserialize)]
119pub struct ImageUrl {
120    /** the image URL */
121    pub url: String,
122}
123
124///
125/// OpenAI tool call structure for function invocations.
126///
127/// Represents a function call made by the assistant during response generation.
128#[derive(Debug, Deserialize)]
129pub struct OpenAiToolCall {
130    /** unique identifier for this tool call */
131    pub id: String,
132    /** tool call type, typically "function" */
133    #[serde(rename = "type")]
134    #[allow(dead_code)]
135    pub call_type: String,
136    /** function call details */
137    pub function: OpenAiFunction,
138}
139
140///
141/// OpenAI function call details within a tool call.
142///
143/// Contains the function name and arguments for execution.
144#[derive(Debug, Deserialize)]
145pub struct OpenAiFunction {
146    /** function name to call */
147    pub name: String,
148    /** function arguments as JSON value */
149    pub arguments: serde_json::Value,
150}
151
152///
153/// OpenAI tool definition for available functions.
154///
155/// Describes a function that can be called by the model during response generation.
156#[derive(Debug, Deserialize)]
157pub struct OpenAiTool {
158    /** tool type, typically "function" */
159    #[serde(rename = "type")]
160    #[allow(dead_code)]
161    pub tool_type: String,
162    /** function definition and schema */
163    pub function: OpenAiToolFunction,
164}
165
166///
167/// OpenAI function definition within a tool.
168///
169/// Contains function metadata and parameter schema for validation.
170#[derive(Debug, Deserialize)]
171pub struct OpenAiToolFunction {
172    /** function name */
173    pub name: String,
174    /** function description */
175    pub description: String,
176    /** JSON schema for function parameters */
177    pub parameters: serde_json::Value,
178}
179
180///
181/// OpenAI tool choice configuration.
182///
183/// Controls how the model should choose which tools to use during generation.
184#[derive(Debug, Deserialize)]
185#[serde(untagged)]
186pub enum OpenAiToolChoice {
187    /** string choice: "auto", "none", etc. */
188    String(String),
189    /** object choice with specific function */
190    Object(OpenAiToolChoiceObject),
191}
192
193///
194/// OpenAI tool choice object for specific function selection.
195///
196/// Allows forcing the model to use a specific function.
197#[derive(Debug, Deserialize)]
198pub struct OpenAiToolChoiceObject {
199    /** choice type */
200    #[serde(rename = "type")]
201    #[allow(dead_code)]
202    pub choice_type: String,
203    /** specific function to choose */
204    pub function: Option<OpenAiToolChoiceFunction>,
205}
206
207///
208/// OpenAI specific function choice within tool choice object.
209///
210/// Identifies the exact function to use.
211#[derive(Debug, Deserialize)]
212pub struct OpenAiToolChoiceFunction {
213    /** function name to force */
214    pub name: String,
215}
216
217///
218/// Anthropic chat completion request structure.
219///
220/// Target format for requests to Anthropic's Claude API via Vertex AI.
221/// Contains converted messages and configuration from OpenAI format.
222#[derive(Debug, Serialize)]
223pub struct AnthropicRequest {
224    /** Anthropic API version identifier */
225    #[serde(rename = "anthropic_version")]
226    pub anthropic_version: String,
227    /** conversation messages in Anthropic format */
228    pub messages: Vec<AnthropicMessage>,
229    /** maximum tokens to generate */
230    #[serde(rename = "max_tokens")]
231    pub max_tokens: u32,
232    /** sampling temperature */
233    pub temperature: f64,
234    /** whether to stream the response */
235    pub stream: bool,
236    /** available tools in Anthropic format */
237    #[serde(skip_serializing_if = "skip_empty_tools")]
238    pub tools: Option<Vec<AnthropicTool>>,
239    /** tool choice configuration in Anthropic format */
240    #[serde(rename = "tool_choice", skip_serializing_if = "Option::is_none")]
241    pub tool_choice: Option<AnthropicToolChoice>,
242}
243
244///
245/// Anthropic message structure for chat conversations.
246///
247/// Contains role and content blocks in Anthropic's preferred format.
248#[derive(Debug, Serialize)]
249pub struct AnthropicMessage {
250    /** message role: user or assistant */
251    pub role: String,
252    /** message content as structured blocks */
253    pub content: Vec<AnthropicContentBlock>,
254}
255
256///
257/// Anthropic content block for message content.
258///
259/// Supports text, tool usage, tool results, and image content types
260/// with proper tagging for serialization.
261#[derive(Debug, Serialize)]
262#[serde(tag = "type")]
263pub enum AnthropicContentBlock {
264    /** text content block */
265    #[serde(rename = "text")]
266    Text {
267        /** the text content */
268        text: String,
269    },
270    /** tool usage block for function calls */
271    #[serde(rename = "tool_use")]
272    ToolUse {
273        /** tool call identifier */
274        id: String,
275        /** function name */
276        name: String,
277        /** function input arguments */
278        input: serde_json::Value,
279    },
280    /** tool result block for function responses */
281    #[serde(rename = "tool_result")]
282    ToolResult {
283        /** corresponding tool use identifier */
284        #[serde(rename = "tool_use_id")]
285        tool_use_id: String,
286        /** tool execution result */
287        content: AnthropicToolResultContent,
288    },
289    /** image content block */
290    #[serde(rename = "image")]
291    Image {
292        /** image source information */
293        source: ImageSource,
294    },
295}
296
297///
298/// Anthropic tool result content union type.
299///
300/// Supports both simple string results and structured array results
301/// for complex tool responses.
302#[derive(Debug, Serialize)]
303#[serde(untagged)]
304pub enum AnthropicToolResultContent {
305    /** simple string result */
306    String(String),
307    /** structured array result */
308    Array(Vec<serde_json::Value>),
309}
310
311///
312/// Image source information for Anthropic image blocks.
313///
314/// Contains metadata about image resources.
315#[derive(Debug, Serialize)]
316pub struct ImageSource {
317    /** source type identifier */
318    #[serde(rename = "type")]
319    pub source_type: String,
320    /** image URL */
321    pub url: String,
322}
323
324///
325/// Anthropic tool definition for function calling.
326///
327/// Describes available functions in Anthropic's format.
328#[derive(Debug, Serialize)]
329pub struct AnthropicTool {
330    /** function name */
331    pub name: String,
332    /** function description */
333    pub description: String,
334    /** function input schema */
335    #[serde(rename = "input_schema")]
336    pub input_schema: serde_json::Value,
337}
338
339///
340/// Anthropic tool choice configuration.
341///
342/// Controls tool selection behavior in Anthropic format.
343#[derive(Debug, Serialize)]
344#[serde(tag = "type")]
345pub enum AnthropicToolChoice {
346    /** automatic tool selection */
347    #[serde(rename = "auto")]
348    Auto,
349    /** force specific tool usage */
350    #[serde(rename = "tool")]
351    Tool {
352        /** tool name to force */
353        name: String,
354    },
355}
356
357///
358/// Converter from OpenAI format to Anthropic format.
359///
360/// Follows Single Responsibility Principle - handles only format conversion
361/// from OpenAI chat completions to Anthropic message format.
362pub struct OpenAiToAnthropicConverter {
363    /** logging level for debug output */
364    log_level: LogLevel,
365}
366
367/* --- constants ------------------------------------------------------------------------------ */
368
369/** Anthropic API version to use for requests */
370const ANTHROPIC_VERSION: &str = "vertex-2023-10-16";
371
372/** Default maximum tokens if not specified */
373const DEFAULT_MAX_TOKENS: u32 = 8000;
374
375/** Default temperature if not specified */
376const DEFAULT_TEMPERATURE: f64 = 0.9;
377
378/* --- start of code -------------------------------------------------------------------------- */
379
380impl OpenAiToAnthropicConverter {
381    ///
382    /// Create a new OpenAI to Anthropic converter.
383    ///
384    /// # Arguments
385    ///  * `log_level` - logging level for debug output
386    ///
387    /// # Returns
388    ///  * New converter instance
389    pub fn new(log_level: LogLevel) -> Self {
390        Self { log_level }
391    }
392
393    ///
394    /// Convert OpenAI request to Anthropic request format.
395    ///
396    /// Transforms the entire request structure including messages, tools, and
397    /// configuration parameters. Handles system messages, tool calls, and
398    /// multimodal content appropriately.
399    ///
400    /// # Arguments
401    ///  * `request` - OpenAI format request to convert
402    ///
403    /// # Returns
404    ///  * Converted Anthropic format request
405    ///  * `ProxyError::Conversion` if conversion fails
406    pub fn convert(&self, request: OpenAiRequest) -> Result<AnthropicRequest> {
407        self.debug(&format!(
408            "Converting {} message(s) from OpenAI to Anthropic format",
409            request.messages.len()
410        ));
411
412        let mut anthropic_messages = Vec::new();
413        let mut pending_tool_results = Vec::new();
414        let mut last_assistant_message: Option<&'_ OpenAiMessage> = None;
415        let mut system_messages = Vec::new();
416
417        self.process_messages(
418            &request.messages,
419            &mut anthropic_messages,
420            &mut pending_tool_results,
421            &mut last_assistant_message,
422            &mut system_messages,
423        )?;
424
425        self.handle_remaining_tool_results(
426            &mut anthropic_messages,
427            &mut pending_tool_results,
428            last_assistant_message,
429        )?;
430
431        self.prepend_system_messages(&mut anthropic_messages, system_messages);
432
433        let tools = self.convert_tools(request.tools);
434        let tool_choice = self.convert_tool_choice(request.tool_choice);
435
436        let anthropic_request = AnthropicRequest {
437            anthropic_version: ANTHROPIC_VERSION.to_string(),
438            messages: anthropic_messages,
439            max_tokens: request.max_tokens.unwrap_or(DEFAULT_MAX_TOKENS),
440            temperature: request.temperature.unwrap_or(DEFAULT_TEMPERATURE),
441            stream: request.stream.unwrap_or(false),
442            tools,
443            tool_choice,
444        };
445
446        self.debug(&format!(
447            "Converted Anthropic request with {} messages",
448            anthropic_request.messages.len()
449        ));
450
451        Ok(anthropic_request)
452    }
453
454    ///
455    /// Process all messages in the OpenAI request.
456    ///
457    /// Iterates through messages and converts them based on role type,
458    /// managing tool calls and results properly.
459    ///
460    /// # Arguments
461    ///  * `messages` - OpenAI messages to process
462    ///  * `anthropic_messages` - output Anthropic messages
463    ///  * `pending_tool_results` - accumulated tool results
464    ///  * `last_assistant_message` - reference to last assistant message
465    ///  * `system_messages` - accumulated system messages
466    ///
467    /// # Returns
468    ///  * `Ok(())` on successful processing
469    ///  * `ProxyError::Conversion` if message conversion fails
470    fn process_messages<'a>(
471        &self,
472        messages: &'a [OpenAiMessage],
473        anthropic_messages: &mut Vec<AnthropicMessage>,
474        pending_tool_results: &mut Vec<(String, AnthropicToolResultContent)>,
475        last_assistant_message: &mut Option<&'a OpenAiMessage>,
476        system_messages: &mut Vec<String>,
477    ) -> Result<()> {
478        for msg in messages {
479            self.debug(&format!("Processing message with role: {}", msg.role));
480
481            match msg.role.as_str() {
482                "system" => {
483                    self.process_system_message(msg, system_messages);
484                }
485                "assistant" => {
486                    self.process_assistant_message(
487                        msg,
488                        anthropic_messages,
489                        pending_tool_results,
490                        last_assistant_message,
491                    )?;
492                }
493                "tool" => {
494                    self.process_tool_message(msg, pending_tool_results);
495                }
496                "user" => {
497                    self.process_user_message(
498                        msg,
499                        anthropic_messages,
500                        pending_tool_results,
501                        *last_assistant_message,
502                    )?;
503                }
504                _ => {
505                    return Err(ProxyError::Conversion(format!(
506                        "Unknown message role: {}",
507                        msg.role
508                    )));
509                }
510            }
511        }
512        Ok(())
513    }
514
515    ///
516    /// Process a system message by extracting its content.
517    ///
518    /// # Arguments
519    ///  * `msg` - system message to process
520    ///  * `system_messages` - collection to add system content to
521    fn process_system_message(&self, msg: &OpenAiMessage, system_messages: &mut Vec<String>) {
522        if let Some(OpenAiContent::String(content)) = &msg.content {
523            system_messages.push(content.clone());
524        }
525    }
526
527    ///
528    /// Process an assistant message with optional tool calls.
529    ///
530    /// # Arguments
531    ///  * `msg` - assistant message to process
532    ///  * `anthropic_messages` - output Anthropic messages
533    ///  * `pending_tool_results` - accumulated tool results
534    ///  * `last_assistant_message` - reference to last assistant message
535    ///
536    /// # Returns
537    ///  * `Ok(())` on successful processing
538    ///  * `ProxyError::Conversion` if conversion fails
539    fn process_assistant_message<'a>(
540        &self,
541        msg: &'a OpenAiMessage,
542        anthropic_messages: &mut Vec<AnthropicMessage>,
543        pending_tool_results: &mut Vec<(String, AnthropicToolResultContent)>,
544        last_assistant_message: &mut Option<&'a OpenAiMessage>,
545    ) -> Result<()> {
546        if last_assistant_message.is_some() && !pending_tool_results.is_empty() {
547            self.attach_tool_results(anthropic_messages, pending_tool_results)?;
548        }
549
550        let anthropic_msg = self.convert_assistant_message(msg)?;
551        anthropic_messages.push(anthropic_msg);
552        *last_assistant_message = Some(msg);
553        Ok(())
554    }
555
556    ///
557    /// Process a tool message by collecting its result.
558    ///
559    /// # Arguments
560    ///  * `msg` - tool message to process
561    ///  * `pending_tool_results` - collection to add tool result to
562    fn process_tool_message(
563        &self,
564        msg: &OpenAiMessage,
565        pending_tool_results: &mut Vec<(String, AnthropicToolResultContent)>,
566    ) {
567        if let Some(tool_call_id) = &msg.tool_call_id {
568            let content = self.convert_tool_result_content(&msg.content);
569            pending_tool_results.push((tool_call_id.clone(), content));
570            self.debug(&format!("Collected tool result for tool_call_id: {}", tool_call_id));
571        }
572    }
573
574    ///
575    /// Process a user message and attach any pending tool results.
576    ///
577    /// # Arguments
578    ///  * `msg` - user message to process
579    ///  * `anthropic_messages` - output Anthropic messages
580    ///  * `pending_tool_results` - accumulated tool results
581    ///  * `last_assistant_message` - optional reference to last assistant message
582    ///
583    /// # Returns
584    ///  * `Ok(())` on successful processing
585    ///  * `ProxyError::Conversion` if conversion fails
586    fn process_user_message<'a>(
587        &self,
588        msg: &'a OpenAiMessage,
589        anthropic_messages: &mut Vec<AnthropicMessage>,
590        pending_tool_results: &mut Vec<(String, AnthropicToolResultContent)>,
591        last_assistant_message: Option<&'a OpenAiMessage>,
592    ) -> Result<()> {
593        if last_assistant_message.is_some() && !pending_tool_results.is_empty() {
594            self.debug(&format!(
595                "Attaching {} tool result(s) before user message",
596                pending_tool_results.len()
597            ));
598            self.attach_tool_results(anthropic_messages, pending_tool_results)?;
599        }
600
601        let anthropic_msg = self.convert_user_message(msg)?;
602        anthropic_messages.push(anthropic_msg);
603        Ok(())
604    }
605
606    ///
607    /// Convert tool result content from OpenAI to Anthropic format.
608    ///
609    /// # Arguments
610    ///  * `content` - OpenAI message content to convert
611    ///
612    /// # Returns
613    ///  * Converted tool result content
614    fn convert_tool_result_content(
615        &self,
616        content: &Option<OpenAiContent>,
617    ) -> AnthropicToolResultContent {
618        match content {
619            Some(OpenAiContent::String(s)) => AnthropicToolResultContent::String(s.clone()),
620            Some(OpenAiContent::Array(arr)) => {
621                let mut json_blocks = Vec::new();
622                for block in arr {
623                    match block.block_type.as_str() {
624                        "text" => {
625                            if let Some(text) = &block.text {
626                                json_blocks.push(json!({ "type": "text", "text": text }));
627                            }
628                        }
629                        "image_url" => {
630                            if let Some(img) = &block.image_url {
631                                json_blocks.push(
632                                    json!({ "type": "image_url", "image_url": { "url": img.url } }),
633                                );
634                            }
635                        }
636                        _ => {}
637                    }
638                }
639                AnthropicToolResultContent::Array(json_blocks)
640            }
641            None => AnthropicToolResultContent::String(String::new()),
642        }
643    }
644
645    ///
646    /// Handle any remaining tool results after processing all messages.
647    ///
648    /// # Arguments
649    ///  * `anthropic_messages` - output Anthropic messages
650    ///  * `pending_tool_results` - accumulated tool results
651    ///  * `last_assistant_message` - optional reference to last assistant message
652    ///
653    /// # Returns
654    ///  * `Ok(())` on successful processing
655    ///  * `ProxyError::Conversion` if attachment fails
656    fn handle_remaining_tool_results(
657        &self,
658        anthropic_messages: &mut Vec<AnthropicMessage>,
659        pending_tool_results: &mut Vec<(String, AnthropicToolResultContent)>,
660        last_assistant_message: Option<&OpenAiMessage>,
661    ) -> Result<()> {
662        if last_assistant_message.is_some() && !pending_tool_results.is_empty() {
663            self.attach_tool_results(anthropic_messages, pending_tool_results)?;
664        }
665        Ok(())
666    }
667
668    ///
669    /// Prepend system messages to the first user message.
670    ///
671    /// # Arguments
672    ///  * `anthropic_messages` - output Anthropic messages to modify
673    ///  * `system_messages` - system messages to prepend
674    fn prepend_system_messages(
675        &self,
676        anthropic_messages: &mut [AnthropicMessage],
677        system_messages: Vec<String>,
678    ) {
679        if !system_messages.is_empty() && !anthropic_messages.is_empty() {
680            let system_text = system_messages.join("\n\n");
681            if let Some(first_user_msg) = anthropic_messages.iter_mut().find(|m| m.role == "user") {
682                self.prepend_system_text(first_user_msg, &system_text);
683            }
684        }
685    }
686
687    ///
688    /// Convert OpenAI tools to Anthropic format.
689    ///
690    /// # Arguments
691    ///  * `tools` - optional OpenAI tools to convert
692    ///
693    /// # Returns
694    ///  * Converted Anthropic tools or None
695    fn convert_tools(&self, tools: Option<Vec<OpenAiTool>>) -> Option<Vec<AnthropicTool>> {
696        tools.map(|tools| {
697            self.debug(&format!(
698                "Converting {} tool(s) from OpenAI to Anthropic format",
699                tools.len()
700            ));
701            tools
702                .into_iter()
703                .map(|tool| AnthropicTool {
704                    name: tool.function.name,
705                    description: tool.function.description,
706                    input_schema: tool.function.parameters,
707                })
708                .collect()
709        })
710    }
711
712    ///
713    /// Convert OpenAI tool choice to Anthropic format.
714    ///
715    /// # Arguments
716    ///  * `tool_choice` - optional OpenAI tool choice to convert
717    ///
718    /// # Returns
719    ///  * Converted Anthropic tool choice or None
720    fn convert_tool_choice(
721        &self,
722        tool_choice: Option<OpenAiToolChoice>,
723    ) -> Option<AnthropicToolChoice> {
724        tool_choice.and_then(|choice| {
725            self.debug(&format!("Tool choice: {:?}", choice));
726            match choice {
727                OpenAiToolChoice::String(s) if s == "auto" => Some(AnthropicToolChoice::Auto),
728                OpenAiToolChoice::String(s) if s == "none" => {
729                    self.debug("Tool choice 'none' not supported by Anthropic, omitting");
730                    None
731                }
732                OpenAiToolChoice::Object(obj) => {
733                    if let Some(function) = obj.function {
734                        self.debug(&format!("Forced tool choice: {}", function.name));
735                        Some(AnthropicToolChoice::Tool { name: function.name })
736                    } else {
737                        None
738                    }
739                }
740                _ => None,
741            }
742        })
743    }
744
745    ///
746    /// Convert an OpenAI assistant message to Anthropic format.
747    ///
748    /// Handles both text content and tool calls within the message.
749    ///
750    /// # Arguments
751    ///  * `msg` - OpenAI assistant message to convert
752    ///
753    /// # Returns
754    ///  * Converted Anthropic message
755    ///  * `ProxyError::Conversion` if conversion fails
756    fn convert_assistant_message(&self, msg: &OpenAiMessage) -> Result<AnthropicMessage> {
757        let mut content = Vec::new();
758
759        self.add_text_content(&mut content, &msg.content);
760        self.add_tool_calls(&mut content, &msg.tool_calls)?;
761
762        if content.is_empty() {
763            content.push(AnthropicContentBlock::Text { text: String::new() });
764        }
765
766        Ok(AnthropicMessage { role: "assistant".to_string(), content })
767    }
768
769    ///
770    /// Add text content from OpenAI message to Anthropic content blocks.
771    ///
772    /// # Arguments
773    ///  * `content` - content blocks to add to
774    ///  * `openai_content` - OpenAI content to extract text from
775    fn add_text_content(
776        &self,
777        content: &mut Vec<AnthropicContentBlock>,
778        openai_content: &Option<OpenAiContent>,
779    ) {
780        match openai_content {
781            Some(OpenAiContent::String(text)) if !text.is_empty() => {
782                content.push(AnthropicContentBlock::Text { text: text.clone() });
783            }
784            Some(OpenAiContent::Array(blocks)) => {
785                for block in blocks {
786                    if block.block_type == "text"
787                        && let Some(text) = &block.text {
788                            content.push(AnthropicContentBlock::Text { text: text.clone() });
789                        }
790                }
791            }
792            _ => {}
793        }
794    }
795
796    ///
797    /// Add tool calls from OpenAI message to Anthropic content blocks.
798    ///
799    /// # Arguments
800    ///  * `content` - content blocks to add to
801    ///  * `tool_calls` - OpenAI tool calls to convert
802    ///
803    /// # Returns
804    ///  * `Ok(())` on successful addition
805    ///  * `ProxyError::Conversion` if tool call conversion fails
806    fn add_tool_calls(
807        &self,
808        content: &mut Vec<AnthropicContentBlock>,
809        tool_calls: &Option<Vec<OpenAiToolCall>>,
810    ) -> Result<()> {
811        if let Some(tool_calls) = tool_calls {
812            self.debug(&format!(
813                "Converting {} tool call(s) from assistant message",
814                tool_calls.len()
815            ));
816            for tool_call in tool_calls {
817                let args = self.parse_tool_arguments(&tool_call.function.arguments);
818                content.push(AnthropicContentBlock::ToolUse {
819                    id: tool_call.id.clone(),
820                    name: tool_call.function.name.clone(),
821                    input: args,
822                });
823            }
824        }
825        Ok(())
826    }
827
828    ///
829    /// Parse tool call arguments from JSON value.
830    ///
831    /// # Arguments
832    ///  * `arguments` - JSON arguments value
833    ///
834    /// # Returns
835    ///  * Parsed JSON value for tool input
836    fn parse_tool_arguments(&self, arguments: &serde_json::Value) -> serde_json::Value {
837        match arguments {
838            serde_json::Value::String(s) => {
839                serde_json::from_str(s).unwrap_or_else(|_| arguments.clone())
840            }
841            _ => arguments.clone(),
842        }
843    }
844
845    ///
846    /// Convert an OpenAI user message to Anthropic format.
847    ///
848    /// Handles text, image, and multimodal content appropriately.
849    ///
850    /// # Arguments
851    ///  * `msg` - OpenAI user message to convert
852    ///
853    /// # Returns
854    ///  * Converted Anthropic message
855    ///  * `ProxyError::Conversion` if conversion fails
856    fn convert_user_message(&self, msg: &OpenAiMessage) -> Result<AnthropicMessage> {
857        let content = match &msg.content {
858            Some(OpenAiContent::String(text)) => {
859                vec![AnthropicContentBlock::Text { text: text.clone() }]
860            }
861            Some(OpenAiContent::Array(blocks)) => self.convert_content_blocks(blocks),
862            None => vec![AnthropicContentBlock::Text { text: String::new() }],
863        };
864
865        Ok(AnthropicMessage { role: "user".to_string(), content })
866    }
867
868    ///
869    /// Convert OpenAI content blocks to Anthropic content blocks.
870    ///
871    /// # Arguments
872    ///  * `blocks` - OpenAI content blocks to convert
873    ///
874    /// # Returns
875    ///  * Converted Anthropic content blocks
876    fn convert_content_blocks(&self, blocks: &[OpenAiContentBlock]) -> Vec<AnthropicContentBlock> {
877        blocks
878            .iter()
879            .filter_map(|block| match block.block_type.as_str() {
880                "text" => {
881                    block.text.as_ref().map(|t| AnthropicContentBlock::Text { text: t.clone() })
882                }
883                "image_url" => block.image_url.as_ref().map(|img| AnthropicContentBlock::Image {
884                    source: ImageSource { source_type: "url".to_string(), url: img.url.clone() },
885                }),
886                _ => None,
887            })
888            .collect()
889    }
890
891    ///
892    /// Attach pending tool results to the conversation.
893    ///
894    /// Creates a user message containing tool result blocks and adds it
895    /// to the conversation after the last assistant message.
896    ///
897    /// # Arguments
898    ///  * `anthropic_messages` - messages to add tool results to
899    ///  * `pending_tool_results` - tool results to attach
900    ///
901    /// # Returns
902    ///  * `Ok(())` on successful attachment
903    ///  * `ProxyError::Conversion` if attachment fails
904    fn attach_tool_results(
905        &self,
906        anthropic_messages: &mut Vec<AnthropicMessage>,
907        pending_tool_results: &mut Vec<(String, AnthropicToolResultContent)>,
908    ) -> Result<()> {
909        if let Some(last_msg) = anthropic_messages.last() {
910            if last_msg.role == "assistant" {
911                let tool_results: Vec<AnthropicContentBlock> = pending_tool_results
912                    .drain(..)
913                    .map(|(tool_use_id, content)| AnthropicContentBlock::ToolResult {
914                        tool_use_id,
915                        content,
916                    })
917                    .collect();
918
919                self.debug(&format!(
920                    "Adding tool results user message with {} result(s)",
921                    tool_results.len()
922                ));
923
924                anthropic_messages
925                    .push(AnthropicMessage { role: "user".to_string(), content: tool_results });
926            } else {
927                self.debug("WARNING: Last message is not assistant, cannot attach tool results");
928            }
929        }
930        Ok(())
931    }
932
933    ///
934    /// Prepend system text to the first text block of a message.
935    ///
936    /// Either modifies the first existing text block or inserts a new
937    /// text block at the beginning with the system content.
938    ///
939    /// # Arguments
940    ///  * `msg` - message to prepend system text to
941    ///  * `system_text` - system text to prepend
942    fn prepend_system_text(&self, msg: &mut AnthropicMessage, system_text: &str) {
943        if let Some(first_text_block) =
944            msg.content.iter_mut().find(|c| matches!(c, AnthropicContentBlock::Text { .. }))
945        {
946            if let AnthropicContentBlock::Text { text } = first_text_block {
947                *text = format!("{}\n\n{}", system_text, text);
948            }
949        } else {
950            msg.content.insert(0, AnthropicContentBlock::Text { text: system_text.to_string() });
951        }
952    }
953
954    ///
955    /// Log debug message if trace logging is enabled.
956    ///
957    /// # Arguments
958    ///  * `msg` - debug message to log
959    pub(crate) fn debug(&self, msg: &str) {
960        if self.log_level.is_trace_enabled() {
961            tracing::debug!("[TRACE] {}", msg);
962        }
963    }
964}