codex_convert_proxy/convert/request/
messages.rs1use crate::error::ConversionError;
4use crate::types::chat_api::{
5 ChatMessage, Content, ContentBlock, FunctionCall, MessageRole, ToolCall,
6};
7use crate::types::response_api::{
8 Content as ResponseContent, ContentPart, InputItemOrString,
9};
10
11pub fn convert_input_to_messages(
13 input: InputItemOrString,
14 instructions: Option<String>,
15 enforce_tool_result_adjacency: bool,
16) -> Result<Vec<ChatMessage>, ConversionError> {
17 let mut messages = Vec::new();
18
19 if let Some(inst) = instructions {
21 messages.push(ChatMessage {
22 role: MessageRole::System,
23 content: Content::String(inst),
24 name: None,
25 annotations: None,
26 tool_calls: None,
27 tool_call_id: None,
28 function_call: None,
29 refusal: None,
30 });
31 }
32
33 match input {
35 InputItemOrString::String(s) => {
36 messages.push(ChatMessage {
37 role: MessageRole::User,
38 content: Content::String(s),
39 name: None,
40 annotations: None,
41 tool_calls: None,
42 tool_call_id: None,
43 function_call: None,
44 refusal: None,
45 });
46 }
47 InputItemOrString::Array(items) => {
48 let mut pending_tool_calls: Option<Vec<ToolCall>> = None;
49 let mut emitted_tool_call_ids: std::collections::HashSet<String> =
50 std::collections::HashSet::new();
51 let mut emitted_tool_call_names: std::collections::HashMap<String, String> =
52 std::collections::HashMap::new();
53
54 for item in items {
55 match item.item_type {
56 crate::types::response_api::InputItemType::Message => {
57 let role = match item.role.as_deref() {
58 Some("developer") => MessageRole::Developer,
59 Some("system") => MessageRole::System,
60 Some("assistant") => MessageRole::Assistant,
61 Some("tool") => MessageRole::Tool,
62 _ => MessageRole::User,
63 };
64
65 let content = extract_content(&item.content)?;
66
67 if enforce_tool_result_adjacency && role == MessageRole::Assistant {
71 if let Some(tool_calls) = pending_tool_calls.take() {
72 for tc in &tool_calls {
73 emitted_tool_call_ids.insert(tc.id.clone());
74 emitted_tool_call_names.insert(tc.id.clone(), tc.function.name.clone());
75 }
76 messages.push(ChatMessage {
77 role,
78 content,
79 name: item.name,
80 annotations: None,
81 tool_calls: Some(tool_calls),
82 tool_call_id: item.call_id,
83 function_call: None,
84 refusal: None,
85 });
86 tracing::debug!(
87 "[REQUEST_CONVERT] merged assistant message with pending tool_calls to keep tool result adjacency"
88 );
89 continue;
90 }
91 } else if let Some(tool_calls) = pending_tool_calls.take() {
92 for tc in &tool_calls {
94 emitted_tool_call_ids.insert(tc.id.clone());
95 emitted_tool_call_names.insert(tc.id.clone(), tc.function.name.clone());
96 }
97 messages.push(ChatMessage {
98 role: MessageRole::Assistant,
99 content: Content::String(String::new()),
100 name: None,
101 annotations: None,
102 tool_calls: Some(tool_calls),
103 tool_call_id: None,
104 function_call: None,
105 refusal: None,
106 });
107 }
108
109 messages.push(ChatMessage {
110 role,
111 content,
112 name: item.name,
113 annotations: None,
114 tool_calls: None,
115 tool_call_id: item.call_id,
116 function_call: None,
117 refusal: None,
118 });
119 }
120 crate::types::response_api::InputItemType::FunctionCall => {
121 let arguments = item.arguments.unwrap_or_default();
123 let name = item
124 .name
125 .ok_or_else(|| ConversionError::MissingField("name".to_string()))?;
126 let id = item.call_id.or(item.id).unwrap_or_else(|| format!("call_{}", uuid::Uuid::new_v4()));
128
129 let tool_call = ToolCall {
130 id,
131 tool_type: "function".to_string(),
132 function: FunctionCall { name, arguments },
133 };
134
135 pending_tool_calls.get_or_insert_with(Vec::new).push(tool_call);
136 }
137 crate::types::response_api::InputItemType::FunctionCallOutput => {
138 let call_id = item
139 .call_id
140 .clone()
141 .unwrap_or_else(|| format!("call_{}", uuid::Uuid::new_v4()));
142 let output_name = item
143 .name
144 .clone()
145 .or_else(|| emitted_tool_call_names.get(&call_id).cloned())
146 .unwrap_or_else(|| "unknown_tool".to_string());
147
148 if let Some(tool_calls) = pending_tool_calls.take() {
150 for tc in &tool_calls {
151 emitted_tool_call_ids.insert(tc.id.clone());
152 emitted_tool_call_names.insert(tc.id.clone(), tc.function.name.clone());
153 }
154 messages.push(ChatMessage {
155 role: MessageRole::Assistant,
156 content: Content::String(String::new()),
157 name: None,
158 annotations: None,
159 tool_calls: Some(tool_calls),
160 tool_call_id: None,
161 function_call: None,
162 refusal: None,
163 });
164 }
165
166 if enforce_tool_result_adjacency && !emitted_tool_call_ids.contains(&call_id) {
170 tracing::warn!(
171 "[REQUEST_CONVERT] function_call_output without preceding function_call, synthesizing assistant tool_call (call_id={}, name={})",
172 call_id,
173 output_name
174 );
175 let synthetic_tool_call = ToolCall {
176 id: call_id.clone(),
177 tool_type: "function".to_string(),
178 function: FunctionCall {
179 name: output_name.clone(),
180 arguments: "{}".to_string(),
181 },
182 };
183 messages.push(ChatMessage {
184 role: MessageRole::Assistant,
185 content: Content::String(String::new()),
186 name: None,
187 annotations: None,
188 tool_calls: Some(vec![synthetic_tool_call]),
189 tool_call_id: None,
190 function_call: None,
191 refusal: None,
192 });
193 emitted_tool_call_ids.insert(call_id.clone());
194 }
195
196 messages.push(ChatMessage {
197 role: MessageRole::Tool,
198 content: Content::String(item.output.unwrap_or_default()),
199 name: item.name,
200 annotations: None,
201 tool_calls: None,
202 tool_call_id: Some(call_id.clone()),
203 function_call: None,
204 refusal: None,
205 });
206 tracing::debug!(
207 "[REQUEST_CONVERT] emitted tool result message (call_id={}, name={})",
208 call_id,
209 output_name
210 );
211 }
212 }
213 }
214
215 if let Some(tool_calls) = pending_tool_calls {
217 for tc in &tool_calls {
218 emitted_tool_call_ids.insert(tc.id.clone());
219 emitted_tool_call_names.insert(tc.id.clone(), tc.function.name.clone());
220 }
221 messages.push(ChatMessage {
222 role: MessageRole::Assistant,
223 content: Content::String(String::new()),
224 name: None,
225 annotations: None,
226 tool_calls: Some(tool_calls),
227 tool_call_id: None,
228 function_call: None,
229 refusal: None,
230 });
231 }
232 tracing::debug!(
233 "[REQUEST_CONVERT] input array converted: messages={}, emitted_tool_calls={}",
234 messages.len(),
235 emitted_tool_call_ids.len()
236 );
237 }
238 }
239
240 Ok(messages)
241}
242
243pub fn extract_content(content: &Option<ResponseContent>) -> Result<Content, ConversionError> {
245 match content {
246 Some(ResponseContent::String(s)) => Ok(Content::String(s.clone())),
247 Some(ResponseContent::Array(parts)) => {
248 let mut blocks: Vec<ContentBlock> = Vec::new();
249 for part in parts {
250 match part {
251 ContentPart::InputText { text } => blocks.push(ContentBlock {
252 block_type: "text".to_string(),
253 text: Some(text.clone()),
254 image_url: None,
255 }),
256 ContentPart::OutputText { text, .. } => blocks.push(ContentBlock {
257 block_type: "text".to_string(),
258 text: Some(text.clone()),
259 image_url: None,
260 }),
261 ContentPart::InputImage { image_url } => blocks.push(ContentBlock {
262 block_type: "image_url".to_string(),
263 text: None,
264 image_url: Some(image_url.clone().into()),
265 }),
266 ContentPart::InputFile { file_url, file_id } => {
267 let file_ref = file_url
268 .as_ref()
269 .or(file_id.as_ref())
270 .cloned()
271 .unwrap_or_else(|| "unknown_file".to_string());
272 blocks.push(ContentBlock {
273 block_type: "text".to_string(),
274 text: Some(format!("[input_file] {}", file_ref)),
275 image_url: None,
276 });
277 }
278 }
279 }
280
281 if blocks.is_empty() {
282 Ok(Content::String(String::new()))
283 } else if blocks.len() == 1 && blocks[0].block_type == "text" {
284 Ok(Content::String(blocks[0].text.clone().unwrap_or_default()))
285 } else {
286 Ok(Content::Array(blocks))
287 }
288 }
289 None => Ok(Content::String(String::new())),
290 }
291}