1use serde_json::{json, Value};
2
3use crate::{session::SessionStore, types::*};
4
5pub fn to_chat_request(req: &ResponsesRequest, history: Vec<ChatMessage>, sessions: &SessionStore) -> ChatRequest {
7 let mut messages = history;
8
9 let system_text = req.instructions.as_ref().or(req.system.as_ref());
11 if let Some(system) = system_text {
12 if messages.is_empty() || messages[0].role != "system" {
13 messages.insert(
14 0,
15 ChatMessage {
16 role: "system".into(),
17 content: Some(system.clone()),
18 reasoning_content: None,
19 tool_calls: None,
20 tool_call_id: None,
21 name: None,
22 },
23 );
24 }
25 }
26
27 match &req.input {
29 ResponsesInput::Text(text) => {
30 messages.push(ChatMessage {
31 role: "user".into(),
32 content: Some(text.clone()),
33 reasoning_content: None,
34 tool_calls: None,
35 tool_call_id: None,
36 name: None,
37 });
38 }
39 ResponsesInput::Messages(items) => {
40 let mut i = 0;
44 while i < items.len() {
45 let item = &items[i];
46 let item_type = item.get("type").and_then(|v| v.as_str()).unwrap_or("");
47
48 if item_type == "function_call" {
49 let mut grouped: Vec<Value> = Vec::new();
52 let mut reasoning_content: Option<String> = None;
53
54 while i < items.len() {
55 let cur = &items[i];
56 if cur.get("type").and_then(|v| v.as_str()).unwrap_or("") != "function_call" {
57 break;
58 }
59 let call_id = cur.get("call_id").and_then(|v| v.as_str()).unwrap_or("");
60 let name = cur.get("name").and_then(|v| v.as_str()).unwrap_or("");
61 let args = cur.get("arguments").and_then(|v| v.as_str()).unwrap_or("{}");
62 if reasoning_content.is_none() {
63 reasoning_content = sessions.get_reasoning(call_id);
64 }
65 grouped.push(json!({
66 "id": call_id,
67 "type": "function",
68 "function": { "name": name, "arguments": args }
69 }));
70 i += 1;
71 }
72
73 let mut msg = ChatMessage {
74 role: "assistant".into(),
75 content: None,
76 reasoning_content,
77 tool_calls: Some(grouped),
78 tool_call_id: None,
79 name: None,
80 };
81 if msg.reasoning_content.is_none() {
83 msg.reasoning_content = sessions.get_turn_reasoning(&messages, &msg);
84 }
85 messages.push(msg);
86 } else {
87 match item_type {
88 "function_call_output" => {
89 let call_id = item.get("call_id").and_then(|v| v.as_str()).unwrap_or("");
90 let output = item.get("output").and_then(|v| v.as_str()).unwrap_or("");
91 messages.push(ChatMessage {
92 role: "tool".into(),
93 content: Some(output.to_string()),
94 reasoning_content: None,
95 tool_calls: None,
96 tool_call_id: Some(call_id.to_string()),
97 name: None,
98 });
99 }
100 _ => {
101 let role = item.get("role").and_then(|v| v.as_str()).unwrap_or("user");
103 let role = match role {
104 "developer" => "system",
105 other => other,
106 }
107 .to_string();
108 let content = value_to_text(item.get("content"));
109 let mut msg = ChatMessage {
110 role,
111 content: Some(content),
112 reasoning_content: None,
113 tool_calls: None,
114 tool_call_id: None,
115 name: None,
116 };
117 if msg.role == "assistant" {
121 msg.reasoning_content = sessions.get_turn_reasoning(&messages, &msg);
122 }
123 messages.push(msg);
124 }
125 }
126 i += 1;
127 }
128 }
129 }
130 }
131
132 ChatRequest {
133 model: req.model.clone(),
134 messages,
135 tools: req.tools.iter()
138 .filter(|t| t.get("type").and_then(Value::as_str) == Some("function"))
139 .map(convert_tool)
140 .collect(),
141 temperature: req.temperature,
142 max_tokens: req.max_output_tokens,
143 stream: req.stream,
144 }
145}
146
147fn convert_tool(tool: &Value) -> Value {
155 let Some(obj) = tool.as_object() else {
156 return tool.clone();
157 };
158 if obj.contains_key("function") {
160 return tool.clone();
161 }
162 if obj.get("type").and_then(Value::as_str) == Some("function") {
164 let mut func = serde_json::Map::new();
165 if let Some(v) = obj.get("name") { func.insert("name".into(), v.clone()); }
166 if let Some(v) = obj.get("description") { func.insert("description".into(), v.clone()); }
167 if let Some(v) = obj.get("parameters") { func.insert("parameters".into(), v.clone()); }
168 if let Some(v) = obj.get("strict") { func.insert("strict".into(), v.clone()); }
169 return json!({"type": "function", "function": func});
170 }
171 tool.clone()
172}
173
174pub fn from_chat_response(
176 id: String,
177 model: &str,
178 chat: ChatResponse,
179) -> (ResponsesResponse, Vec<ChatMessage>) {
180 let choice = chat.choices.into_iter().next().unwrap_or_else(|| ChatChoice {
181 message: ChatMessage {
182 role: "assistant".into(),
183 content: Some(String::new()),
184 reasoning_content: None,
185 tool_calls: None,
186 tool_call_id: None,
187 name: None,
188 },
189 });
190
191 let text = choice.message.content.clone().unwrap_or_default();
192 let usage = chat.usage.unwrap_or(ChatUsage {
193 prompt_tokens: 0,
194 completion_tokens: 0,
195 total_tokens: 0,
196 });
197
198 let response = ResponsesResponse {
199 id,
200 object: "response",
201 model: model.to_string(),
202 output: vec![ResponsesOutputItem {
203 kind: "message".into(),
204 role: "assistant".into(),
205 content: vec![ContentPart {
206 kind: "output_text".into(),
207 text: Some(text),
208 }],
209 }],
210 usage: ResponsesUsage {
211 input_tokens: usage.prompt_tokens,
212 output_tokens: usage.completion_tokens,
213 total_tokens: usage.total_tokens,
214 },
215 };
216
217 (response, vec![choice.message])
218}
219
220fn value_to_text(v: Option<&Value>) -> String {
222 match v {
223 None => String::new(),
224 Some(Value::String(s)) => s.clone(),
225 Some(Value::Array(parts)) => parts
226 .iter()
227 .filter_map(|p| p.get("text").and_then(|t| t.as_str()))
228 .collect::<Vec<_>>()
229 .join(""),
230 Some(other) => other.to_string(),
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::*;
237 use serde_json::json;
238
239 fn base_req(input: ResponsesInput) -> ResponsesRequest {
240 ResponsesRequest {
241 model: "test".into(),
242 input,
243 previous_response_id: None,
244 tools: vec![],
245 stream: false,
246 temperature: None,
247 max_output_tokens: None,
248 system: None,
249 instructions: None,
250 }
251 }
252
253 #[test]
254 fn test_text_input_becomes_user_message() {
255 let sessions = SessionStore::new();
256 let req = base_req(ResponsesInput::Text("hello".into()));
257 let chat = to_chat_request(&req, vec![], &sessions);
258 assert_eq!(chat.messages.len(), 1);
259 assert_eq!(chat.messages[0].role, "user");
260 assert_eq!(chat.messages[0].content.as_deref(), Some("hello"));
261 }
262
263 #[test]
264 fn test_system_prompt_from_instructions() {
265 let sessions = SessionStore::new();
266 let mut req = base_req(ResponsesInput::Text("hi".into()));
267 req.instructions = Some("be helpful".into());
268 let chat = to_chat_request(&req, vec![], &sessions);
269 assert_eq!(chat.messages[0].role, "system");
270 assert_eq!(chat.messages[0].content.as_deref(), Some("be helpful"));
271 }
272
273 #[test]
274 fn test_developer_role_mapped_to_system() {
275 let sessions = SessionStore::new();
276 let req = base_req(ResponsesInput::Messages(vec![
277 json!({"type": "message", "role": "developer", "content": "secret instructions"}),
278 ]));
279 let chat = to_chat_request(&req, vec![], &sessions);
280 assert_eq!(chat.messages[0].role, "system");
281 assert_eq!(chat.messages[0].content.as_deref(), Some("secret instructions"));
282 }
283
284 #[test]
285 fn test_function_call_grouping() {
286 let sessions = SessionStore::new();
287 let req = base_req(ResponsesInput::Messages(vec![
288 json!({"type": "function_call", "call_id": "c1", "name": "fn_a", "arguments": "{}"}),
289 json!({"type": "function_call", "call_id": "c2", "name": "fn_b", "arguments": "{}"}),
290 ]));
291 let chat = to_chat_request(&req, vec![], &sessions);
292 assert_eq!(chat.messages.len(), 1);
293 assert_eq!(chat.messages[0].role, "assistant");
294 let calls = chat.messages[0].tool_calls.as_ref().unwrap();
295 assert_eq!(calls.len(), 2);
296 assert_eq!(calls[0]["id"], "c1");
297 assert_eq!(calls[1]["id"], "c2");
298 }
299
300 #[test]
301 fn test_function_call_output_becomes_tool_message() {
302 let sessions = SessionStore::new();
303 let req = base_req(ResponsesInput::Messages(vec![
304 json!({"type": "function_call_output", "call_id": "c1", "output": "result"}),
305 ]));
306 let chat = to_chat_request(&req, vec![], &sessions);
307 assert_eq!(chat.messages[0].role, "tool");
308 assert_eq!(chat.messages[0].content.as_deref(), Some("result"));
309 assert_eq!(chat.messages[0].tool_call_id.as_deref(), Some("c1"));
310 }
311
312 #[test]
313 fn test_convert_tool_flat_to_nested() {
314 let flat = json!({
315 "type": "function",
316 "name": "my_fn",
317 "description": "does stuff",
318 "parameters": {"type": "object"}
319 });
320 let nested = convert_tool(&flat);
321 assert_eq!(nested["type"], "function");
322 assert_eq!(nested["function"]["name"], "my_fn");
323 assert_eq!(nested["function"]["description"], "does stuff");
324 }
325
326 #[test]
327 fn test_convert_tool_already_nested() {
328 let already = json!({
329 "type": "function",
330 "function": {"name": "my_fn", "description": "does stuff"}
331 });
332 let result = convert_tool(&already);
333 assert_eq!(result, already);
334 }
335
336 #[test]
337 fn test_value_to_text_string() {
338 let sessions = SessionStore::new();
339 let req = base_req(ResponsesInput::Messages(vec![
340 json!({"type": "message", "role": "user", "content": "plain text"}),
341 ]));
342 let chat = to_chat_request(&req, vec![], &sessions);
343 assert_eq!(chat.messages[0].content.as_deref(), Some("plain text"));
344 }
345
346 #[test]
347 fn test_value_to_text_parts_array() {
348 let sessions = SessionStore::new();
349 let req = base_req(ResponsesInput::Messages(vec![
350 json!({"type": "message", "role": "user", "content": [
351 {"type": "input_text", "text": "hello "},
352 {"type": "input_text", "text": "world"}
353 ]}),
354 ]));
355 let chat = to_chat_request(&req, vec![], &sessions);
356 assert_eq!(chat.messages[0].content.as_deref(), Some("hello world"));
357 }
358}