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}