reflex_server/gateway/
adapter.rs

1use async_openai::types::chat::{
2    ChatChoice, ChatCompletionMessageToolCall, ChatCompletionMessageToolCalls,
3    ChatCompletionRequestAssistantMessageContent, ChatCompletionRequestDeveloperMessageContent,
4    ChatCompletionRequestMessage, ChatCompletionRequestSystemMessageContent,
5    ChatCompletionRequestToolMessageContent, ChatCompletionRequestUserMessageContent,
6    ChatCompletionRequestUserMessageContentPart, ChatCompletionResponseMessage,
7    ChatCompletionToolChoiceOption, ChatCompletionTools, CompletionUsage,
8    CreateChatCompletionRequest, CreateChatCompletionResponse, FinishReason, FunctionCall,
9    ToolChoiceOptions,
10};
11use genai::chat::{
12    ChatMessage, ChatRequest, ChatResponse, MessageContent, Tool, ToolCall, ToolResponse,
13};
14use serde_json::Value;
15
16pub fn adapt_openai_to_genai(req: CreateChatCompletionRequest) -> ChatRequest {
17    let messages: Vec<ChatMessage> = req
18        .messages
19        .iter()
20        .filter_map(|m| openai_message_to_genai_message(m.clone()))
21        .collect();
22
23    let mut chat_req = ChatRequest::new(messages);
24
25    if let Some(tools) = &req.tools {
26        let genai_tools: Vec<Tool> = tools.iter().filter_map(openai_tool_to_genai_tool).collect();
27        if !genai_tools.is_empty() {
28            chat_req = chat_req.with_tools(genai_tools);
29        }
30    }
31
32    if let Some(tool_choice) = &req.tool_choice {
33        match tool_choice {
34            ChatCompletionToolChoiceOption::Mode(ToolChoiceOptions::None) => {}
35            ChatCompletionToolChoiceOption::Mode(ToolChoiceOptions::Auto) => {}
36            ChatCompletionToolChoiceOption::Mode(ToolChoiceOptions::Required) => {}
37            ChatCompletionToolChoiceOption::Function(named) => {
38                tracing::debug!(
39                    "tool_choice specifies function '{}', but genai does not support forced tool selection",
40                    named.function.name
41                );
42            }
43            _ => {}
44        }
45    }
46
47    chat_req
48}
49
50fn openai_tool_to_genai_tool(tool: &ChatCompletionTools) -> Option<Tool> {
51    match tool {
52        ChatCompletionTools::Function(func_tool) => {
53            let func = &func_tool.function;
54            let mut genai_tool = Tool::new(&func.name);
55
56            if let Some(desc) = &func.description {
57                genai_tool = genai_tool.with_description(desc);
58            }
59
60            if let Some(params) = &func.parameters {
61                genai_tool = genai_tool.with_schema(params.clone());
62            }
63
64            Some(genai_tool)
65        }
66        ChatCompletionTools::Custom(_) => None,
67    }
68}
69
70pub fn adapt_genai_to_openai(resp: ChatResponse, model: String) -> CreateChatCompletionResponse {
71    let tool_calls = resp.tool_calls();
72    let content = resp.first_text().unwrap_or_default().to_string();
73
74    let openai_tool_calls: Vec<ChatCompletionMessageToolCalls> = tool_calls
75        .into_iter()
76        .map(|tc| {
77            ChatCompletionMessageToolCalls::Function(ChatCompletionMessageToolCall {
78                id: tc.call_id.clone(),
79                function: FunctionCall {
80                    name: tc.fn_name.clone(),
81                    arguments: serde_json::to_string(&tc.fn_arguments)
82                        .unwrap_or_else(|_| "{}".to_string()),
83                },
84            })
85        })
86        .collect();
87
88    let message_value = serde_json::json!({
89        "role": "assistant",
90        "content": if content.trim().is_empty() { serde_json::Value::Null } else { serde_json::Value::String(content) },
91        "tool_calls": if openai_tool_calls.is_empty() { serde_json::Value::Null } else { serde_json::to_value(openai_tool_calls).unwrap_or(serde_json::Value::Null) },
92    });
93
94    let message: ChatCompletionResponseMessage =
95        serde_json::from_value(message_value).expect("constructed OpenAI message is valid");
96
97    let response_value = serde_json::json!({
98        "id": format!("chatcmpl-{}", uuid::Uuid::new_v4()),
99        "object": "chat.completion",
100        "created": chrono::Utc::now().timestamp() as u32,
101        "model": model,
102        "choices": vec![ChatChoice {
103            index: 0,
104            message,
105            finish_reason: Some(FinishReason::Stop),
106            logprobs: None,
107        }],
108        "usage": Some(CompletionUsage {
109            prompt_tokens: 0,
110            completion_tokens: 0,
111            total_tokens: 0,
112            prompt_tokens_details: None,
113            completion_tokens_details: None,
114        }),
115    });
116
117    serde_json::from_value(response_value).expect("constructed OpenAI response is valid")
118}
119
120fn openai_message_to_genai_message(m: ChatCompletionRequestMessage) -> Option<ChatMessage> {
121    match m {
122        ChatCompletionRequestMessage::Developer(dev) => Some(ChatMessage::system(
123            openai_developer_content_to_text(dev.content),
124        )),
125        ChatCompletionRequestMessage::System(sys) => Some(ChatMessage::system(
126            openai_system_content_to_text(sys.content),
127        )),
128        ChatCompletionRequestMessage::User(user) => {
129            Some(ChatMessage::user(openai_user_content_to_text(user.content)))
130        }
131        ChatCompletionRequestMessage::Assistant(asst) => {
132            let mut content = MessageContent::default();
133
134            if let Some(tool_calls) = asst.tool_calls {
135                for tc in tool_calls {
136                    match tc {
137                        ChatCompletionMessageToolCalls::Function(tc) => {
138                            let args: Value = serde_json::from_str(&tc.function.arguments)
139                                .unwrap_or_else(|_| Value::String(tc.function.arguments));
140                            content.push(genai::chat::ContentPart::ToolCall(ToolCall {
141                                call_id: tc.id,
142                                fn_name: tc.function.name,
143                                fn_arguments: args,
144                            }));
145                        }
146                        ChatCompletionMessageToolCalls::Custom(tc) => {
147                            content.push(genai::chat::ContentPart::ToolCall(ToolCall {
148                                call_id: tc.id,
149                                fn_name: tc.custom_tool.name,
150                                fn_arguments: serde_json::json!({ "input": tc.custom_tool.input }),
151                            }));
152                        }
153                    }
154                }
155            }
156
157            if let Some(asst_content) = asst.content {
158                let text = openai_assistant_content_to_text(asst_content);
159                if !text.trim().is_empty() {
160                    content.push(genai::chat::ContentPart::Text(text));
161                }
162            }
163
164            if let Some(refusal) = asst.refusal
165                && !refusal.trim().is_empty()
166            {
167                content.push(genai::chat::ContentPart::Text(refusal));
168            }
169
170            if content.is_empty() {
171                return None;
172            }
173
174            Some(ChatMessage::assistant(content))
175        }
176        ChatCompletionRequestMessage::Tool(tool) => Some(ChatMessage::from(ToolResponse::new(
177            tool.tool_call_id,
178            openai_tool_content_to_text(tool.content),
179        ))),
180        ChatCompletionRequestMessage::Function(_) => None,
181    }
182}
183
184fn openai_developer_content_to_text(
185    content: ChatCompletionRequestDeveloperMessageContent,
186) -> String {
187    match content {
188        ChatCompletionRequestDeveloperMessageContent::Text(t) => t,
189        ChatCompletionRequestDeveloperMessageContent::Array(parts) => parts
190            .into_iter()
191            .map(|p| {
192                match p {
193                async_openai::types::chat::ChatCompletionRequestDeveloperMessageContentPart::Text(
194                    t,
195                ) => t.text,
196            }
197            })
198            .collect::<Vec<_>>()
199            .join(" "),
200    }
201}
202
203fn openai_system_content_to_text(content: ChatCompletionRequestSystemMessageContent) -> String {
204    match content {
205        ChatCompletionRequestSystemMessageContent::Text(t) => t,
206        ChatCompletionRequestSystemMessageContent::Array(parts) => parts
207            .into_iter()
208            .map(|p| match p {
209                async_openai::types::chat::ChatCompletionRequestSystemMessageContentPart::Text(
210                    t,
211                ) => t.text,
212            })
213            .collect::<Vec<_>>()
214            .join(" "),
215    }
216}
217
218fn openai_assistant_content_to_text(
219    content: ChatCompletionRequestAssistantMessageContent,
220) -> String {
221    match content {
222        ChatCompletionRequestAssistantMessageContent::Text(t) => t,
223        ChatCompletionRequestAssistantMessageContent::Array(parts) => parts
224            .into_iter()
225            .map(|p| match p {
226                async_openai::types::chat::ChatCompletionRequestAssistantMessageContentPart::Text(
227                    t,
228                ) => t.text,
229                async_openai::types::chat::ChatCompletionRequestAssistantMessageContentPart::Refusal(
230                    r,
231                ) => r.refusal,
232            })
233            .collect::<Vec<_>>()
234            .join(" "),
235    }
236}
237
238fn openai_tool_content_to_text(content: ChatCompletionRequestToolMessageContent) -> String {
239    match content {
240        ChatCompletionRequestToolMessageContent::Text(t) => t,
241        ChatCompletionRequestToolMessageContent::Array(parts) => parts
242            .into_iter()
243            .map(|p| match p {
244                async_openai::types::chat::ChatCompletionRequestToolMessageContentPart::Text(t) => {
245                    t.text
246                }
247            })
248            .collect::<Vec<_>>()
249            .join(" "),
250    }
251}
252
253fn openai_user_content_to_text(content: ChatCompletionRequestUserMessageContent) -> String {
254    match content {
255        ChatCompletionRequestUserMessageContent::Text(t) => t,
256        ChatCompletionRequestUserMessageContent::Array(parts) => parts
257            .into_iter()
258            .map(|p| match p {
259                ChatCompletionRequestUserMessageContentPart::Text(t) => t.text,
260                ChatCompletionRequestUserMessageContentPart::ImageUrl(img) => {
261                    format!("[image_url:{}]", img.image_url.url)
262                }
263                ChatCompletionRequestUserMessageContentPart::InputAudio(_) => {
264                    "[input_audio]".into()
265                }
266                ChatCompletionRequestUserMessageContentPart::File(_) => "[file]".into(),
267            })
268            .collect::<Vec<_>>()
269            .join(" "),
270    }
271}