Skip to main content

claude_codex/providers/codex/
request_summary.rs

1use 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}