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