1use serde_json::Value;
2
3use super::translate::request::{
4 ResponsesContentPart, ResponsesFunctionCallOutput, ResponsesFunctionCallOutputContentPart,
5 ResponsesInputItem, ResponsesRequest, ResponsesTool,
6};
7
8#[derive(Debug, Clone, Default, serde::Serialize)]
9pub struct CodexRequestSizeSummary {
10 pub body_json_bytes: u64,
11 pub instructions_bytes: u64,
12 pub input_json_bytes: u64,
13 pub tools_json_bytes: u64,
14 pub text_json_bytes: u64,
15 pub reasoning_json_bytes: u64,
16 pub include_json_bytes: u64,
17 pub client_metadata_json_bytes: u64,
18 pub input_item_count: usize,
19 pub tool_count: usize,
20 pub input_image_part_count: usize,
21 pub input_image_data_url_bytes: u64,
22 pub input_type_counts: std::collections::BTreeMap<String, usize>,
23 pub role_counts: std::collections::BTreeMap<String, usize>,
24 pub largest_input_items: Vec<InputItemSummary>,
25 pub largest_input_images: Vec<InputImageSummary>,
26 pub largest_tools: Vec<ToolSummary>,
27}
28
29#[derive(Debug, Clone, serde::Serialize)]
30pub struct InputItemSummary {
31 pub index: usize,
32 pub r#type: String,
33 pub role: Option<String>,
34 pub json_bytes: u64,
35}
36
37#[derive(Debug, Clone, serde::Serialize)]
38pub struct InputImageSummary {
39 pub item_index: usize,
40 pub part_index: usize,
41 pub json_bytes: u64,
42 pub image_url_bytes: u64,
43 pub data_url: bool,
44}
45
46#[derive(Debug, Clone, serde::Serialize)]
47pub struct ToolSummary {
48 pub index: usize,
49 pub name: String,
50 pub json_bytes: u64,
51}
52
53fn byte_length(s: &str) -> u64 {
54 s.len() as u64
55}
56
57fn json_bytes(value: Option<&Value>) -> u64 {
58 match value {
59 Some(v) => byte_length(&serde_json::to_string(v).unwrap_or_default()),
60 None => 0,
61 }
62}
63
64fn input_image_parts(input: &[ResponsesInputItem]) -> Vec<(usize, usize, &str)> {
65 let mut parts = Vec::new();
66 for (item_idx, item) in input.iter().enumerate() {
67 match item {
68 ResponsesInputItem::Message { content, .. } => {
69 for (part_idx, part) in content.iter().enumerate() {
70 if let ResponsesContentPart::InputImage { image_url, .. } = part {
71 parts.push((item_idx, part_idx, image_url.as_str()));
72 }
73 }
74 }
75 ResponsesInputItem::FunctionCallOutput {
76 output: ResponsesFunctionCallOutput::ContentItems(content),
77 ..
78 } => {
79 for (part_idx, part) in content.iter().enumerate() {
80 if let ResponsesFunctionCallOutputContentPart::InputImage {
81 image_url, ..
82 } = part
83 {
84 parts.push((item_idx, part_idx, image_url.as_str()));
85 }
86 }
87 }
88 _ => {}
89 }
90 }
91 parts
92}
93
94pub fn summarize_codex_request_size(body: &ResponsesRequest) -> CodexRequestSizeSummary {
95 let body_json = serde_json::to_string(body).unwrap_or_default();
96 let image_parts = input_image_parts(&body.input);
97
98 let input_type_counts = count_items_by(&body.input, |item| match item {
99 ResponsesInputItem::AdditionalTools { .. } => Some("additional_tools".to_string()),
100 ResponsesInputItem::Message { .. } => Some("message".to_string()),
101 ResponsesInputItem::FunctionCall { .. } => Some("function_call".to_string()),
102 ResponsesInputItem::FunctionCallOutput { .. } => Some("function_call_output".to_string()),
103 ResponsesInputItem::Reasoning { .. } => Some("reasoning".to_string()),
104 ResponsesInputItem::Compaction { .. } => Some("compaction".to_string()),
105 ResponsesInputItem::CompactionTrigger => Some("compaction_trigger".to_string()),
106 });
107
108 let role_counts = count_items_by(&body.input, |item| match item {
109 ResponsesInputItem::AdditionalTools { role, .. } => Some(role.clone()),
110 ResponsesInputItem::Message { role, .. } => Some(role.clone()),
111 _ => None,
112 });
113
114 let largest_input_items = {
115 let mut items: Vec<InputItemSummary> = body
116 .input
117 .iter()
118 .enumerate()
119 .map(|(i, item)| {
120 let (r#type, role) = match item {
121 ResponsesInputItem::AdditionalTools { role, .. } => {
122 ("additional_tools".to_string(), Some(role.clone()))
123 }
124 ResponsesInputItem::Message { role, .. } => {
125 ("message".to_string(), Some(role.clone()))
126 }
127 ResponsesInputItem::FunctionCall { .. } => ("function_call".to_string(), None),
128 ResponsesInputItem::FunctionCallOutput { .. } => {
129 ("function_call_output".to_string(), None)
130 }
131 ResponsesInputItem::Reasoning { .. } => ("reasoning".to_string(), None),
132 ResponsesInputItem::Compaction { .. } => ("compaction".to_string(), None),
133 ResponsesInputItem::CompactionTrigger => {
134 ("compaction_trigger".to_string(), None)
135 }
136 };
137 let json_bytes_val =
138 json_bytes(Some(&serde_json::to_value(item).unwrap_or_default()));
139 InputItemSummary {
140 index: i,
141 r#type,
142 role,
143 json_bytes: json_bytes_val,
144 }
145 })
146 .collect();
147 items.sort_by_key(|item| std::cmp::Reverse(item.json_bytes));
148 items.truncate(5);
149 items
150 };
151
152 let largest_input_images = {
153 let mut items: Vec<InputImageSummary> = image_parts
154 .iter()
155 .map(|&(item_idx, part_idx, url)| {
156 let json_bytes_val = json_bytes(Some(&serde_json::json!({
157 "type": "input_image",
158 "image_url": url,
159 })));
160 InputImageSummary {
161 item_index: item_idx,
162 part_index: part_idx,
163 json_bytes: json_bytes_val,
164 image_url_bytes: byte_length(url),
165 data_url: url.starts_with("data:"),
166 }
167 })
168 .collect();
169 items.sort_by_key(|item| std::cmp::Reverse(item.image_url_bytes));
170 items.truncate(5);
171 items
172 };
173
174 let largest_tools = {
175 let mut items: Vec<ToolSummary> = Vec::new();
176 if let Some(ref tools) = body.tools {
177 for (i, tool) in tools.iter().enumerate() {
178 let name = match tool {
179 ResponsesTool::Function(f) => f.name.clone(),
180 ResponsesTool::WebSearch(_) => "web_search".to_string(),
181 };
182 let json_bytes_val =
183 json_bytes(Some(&serde_json::to_value(tool).unwrap_or_default()));
184 items.push(ToolSummary {
185 index: i,
186 name,
187 json_bytes: json_bytes_val,
188 });
189 }
190 }
191 items.sort_by_key(|item| std::cmp::Reverse(item.json_bytes));
192 items.truncate(5);
193 items
194 };
195
196 CodexRequestSizeSummary {
197 body_json_bytes: byte_length(&body_json),
198 instructions_bytes: body.instructions.as_ref().map_or(0, |s| byte_length(s)),
199 input_json_bytes: json_bytes(Some(&serde_json::to_value(&body.input).unwrap_or_default())),
200 tools_json_bytes: match &body.tools {
201 Some(tools) => json_bytes(Some(&serde_json::to_value(tools).unwrap_or_default())),
202 None => 0,
203 },
204 text_json_bytes: json_bytes(Some(&serde_json::to_value(&body.text).unwrap_or_default())),
205 reasoning_json_bytes: json_bytes(
206 body.reasoning
207 .as_ref()
208 .map(|r| serde_json::to_value(r).unwrap_or_default())
209 .as_ref(),
210 ),
211 include_json_bytes: json_bytes(
212 body.include
213 .as_ref()
214 .map(|i| serde_json::to_value(i).unwrap_or_default())
215 .as_ref(),
216 ),
217 client_metadata_json_bytes: json_bytes(
218 body.client_metadata
219 .as_ref()
220 .map(|m| serde_json::to_value(m).unwrap_or_default())
221 .as_ref(),
222 ),
223 input_item_count: body.input.len(),
224 tool_count: body.tools.as_ref().map_or(0, |t| t.len()),
225 input_image_part_count: image_parts.len(),
226 input_image_data_url_bytes: image_parts
227 .iter()
228 .filter(|(_, _, url)| url.starts_with("data:"))
229 .map(|(_, _, url)| byte_length(url))
230 .sum(),
231 input_type_counts,
232 role_counts,
233 largest_input_items,
234 largest_input_images,
235 largest_tools,
236 }
237}
238
239fn count_items_by<T, F>(items: &[T], f: F) -> std::collections::BTreeMap<String, usize>
240where
241 F: Fn(&T) -> Option<String>,
242{
243 let mut counts = std::collections::BTreeMap::new();
244 for item in items {
245 if let Some(key) = f(item) {
246 *counts.entry(key).or_insert(0) += 1;
247 }
248 }
249 counts
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use serde_json::json;
256
257 #[test]
258 fn summarize_simple_request() {
259 let input = vec![ResponsesInputItem::Message {
260 role: "user".to_string(),
261 content: vec![ResponsesContentPart::InputText {
262 text: "hello".to_string(),
263 }],
264 }];
265 let req = ResponsesRequest {
266 model: "gpt-5.5".to_string(),
267 instructions: None,
268 input,
269 tools: None,
270 tool_choice: None,
271 store: false,
272 stream: true,
273 parallel_tool_calls: true,
274 include: None,
275 client_metadata: None,
276 service_tier: None,
277 prompt_cache_key: None,
278 text: super::super::translate::request::ResponsesText {
279 verbosity: Some("low".to_string()),
280 format: None,
281 },
282 reasoning: None,
283 };
284 let summary = summarize_codex_request_size(&req);
285 assert_eq!(summary.input_item_count, 1);
286 assert_eq!(summary.tool_count, 0);
287 assert!(summary.body_json_bytes > 0);
288 }
289
290 #[test]
291 fn summarize_with_tools_and_images() {
292 let req: ResponsesRequest = serde_json::from_value(json!({
293 "model": "gpt-5.5",
294 "input": [
295 {
296 "type": "message",
297 "role": "user",
298 "content": [
299 {"type": "input_text", "text": "describe"},
300 {"type": "input_image", "image_url": "data:image/png;base64,abc"}
301 ]
302 },
303 {
304 "type": "function_call_output",
305 "call_id": "call_1",
306 "output": [
307 {"type": "input_text", "text": "tool image"},
308 {"type": "input_image", "image_url": "data:image/jpeg;base64,def"}
309 ]
310 }
311 ],
312 "store": false,
313 "stream": true,
314 "parallel_tool_calls": true,
315 "text": {"verbosity": "low"}
316 }))
317 .unwrap();
318 let summary = summarize_codex_request_size(&req);
319 assert_eq!(summary.input_image_part_count, 2);
320 assert!(summary.input_image_data_url_bytes > 0);
321 }
322}