Skip to main content

claude_codex/providers/codex/
request_summary.rs

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}