|
| 1 | +use crate::client::AIClient; |
1 | 2 | use crate::client::utils::{ |
2 | 3 | build_request_body_subset, is_trim_custom_request_body_mode, merge_json_value, |
3 | 4 | }; |
4 | | -use crate::client::AIClient; |
5 | 5 | use reqwest::RequestBuilder; |
6 | 6 |
|
7 | 7 | pub(crate) fn apply_header_policy<F>( |
@@ -104,12 +104,175 @@ pub(crate) fn log_extra_body_keys( |
104 | 104 | ); |
105 | 105 | } |
106 | 106 |
|
| 107 | +pub(crate) fn summarize_request_body_for_log( |
| 108 | + request_body: &serde_json::Value, |
| 109 | +) -> serde_json::Value { |
| 110 | + let mut summary = serde_json::Map::new(); |
| 111 | + |
| 112 | + if let Some(model) = request_body |
| 113 | + .get("model") |
| 114 | + .and_then(serde_json::Value::as_str) |
| 115 | + { |
| 116 | + summary.insert( |
| 117 | + "model".to_string(), |
| 118 | + serde_json::Value::String(model.to_string()), |
| 119 | + ); |
| 120 | + } |
| 121 | + if let Some(stream) = request_body |
| 122 | + .get("stream") |
| 123 | + .and_then(serde_json::Value::as_bool) |
| 124 | + { |
| 125 | + summary.insert("stream".to_string(), serde_json::Value::Bool(stream)); |
| 126 | + } |
| 127 | + if let Some(max_tokens) = request_body |
| 128 | + .get("max_tokens") |
| 129 | + .and_then(|value| value.as_u64()) |
| 130 | + { |
| 131 | + summary.insert( |
| 132 | + "max_tokens".to_string(), |
| 133 | + serde_json::Value::Number(max_tokens.into()), |
| 134 | + ); |
| 135 | + } |
| 136 | + if let Some(tool_stream) = request_body |
| 137 | + .get("tool_stream") |
| 138 | + .and_then(serde_json::Value::as_bool) |
| 139 | + { |
| 140 | + summary.insert( |
| 141 | + "tool_stream".to_string(), |
| 142 | + serde_json::Value::Bool(tool_stream), |
| 143 | + ); |
| 144 | + } |
| 145 | + if let Some(system) = request_body |
| 146 | + .get("system") |
| 147 | + .and_then(serde_json::Value::as_str) |
| 148 | + { |
| 149 | + summary.insert( |
| 150 | + "system_chars".to_string(), |
| 151 | + serde_json::Value::Number((system.chars().count() as u64).into()), |
| 152 | + ); |
| 153 | + } |
| 154 | + if let Some(messages) = request_body |
| 155 | + .get("messages") |
| 156 | + .and_then(serde_json::Value::as_array) |
| 157 | + { |
| 158 | + summary.insert( |
| 159 | + "message_count".to_string(), |
| 160 | + serde_json::Value::Number((messages.len() as u64).into()), |
| 161 | + ); |
| 162 | + summary.insert( |
| 163 | + "messages".to_string(), |
| 164 | + serde_json::Value::Array(messages.iter().map(summarize_message_for_log).collect()), |
| 165 | + ); |
| 166 | + } |
| 167 | + if let Some(tools) = request_body |
| 168 | + .get("tools") |
| 169 | + .and_then(serde_json::Value::as_array) |
| 170 | + { |
| 171 | + summary.insert( |
| 172 | + "tool_count".to_string(), |
| 173 | + serde_json::Value::Number((tools.len() as u64).into()), |
| 174 | + ); |
| 175 | + } |
| 176 | + if let Some(object) = request_body.as_object() { |
| 177 | + let mut top_level_keys = object.keys().cloned().collect::<Vec<_>>(); |
| 178 | + top_level_keys.sort(); |
| 179 | + summary.insert( |
| 180 | + "top_level_keys".to_string(), |
| 181 | + serde_json::Value::Array( |
| 182 | + top_level_keys |
| 183 | + .into_iter() |
| 184 | + .map(serde_json::Value::String) |
| 185 | + .collect(), |
| 186 | + ), |
| 187 | + ); |
| 188 | + } |
| 189 | + |
| 190 | + serde_json::Value::Object(summary) |
| 191 | +} |
| 192 | + |
| 193 | +fn summarize_message_for_log(message: &serde_json::Value) -> serde_json::Value { |
| 194 | + let mut summary = serde_json::Map::new(); |
| 195 | + let content = message.get("content"); |
| 196 | + |
| 197 | + if let Some(role) = message.get("role").and_then(serde_json::Value::as_str) { |
| 198 | + summary.insert( |
| 199 | + "role".to_string(), |
| 200 | + serde_json::Value::String(role.to_string()), |
| 201 | + ); |
| 202 | + } |
| 203 | + if let Some(content) = content { |
| 204 | + summary.insert( |
| 205 | + "content_chars".to_string(), |
| 206 | + serde_json::Value::Number((content_text_chars(content) as u64).into()), |
| 207 | + ); |
| 208 | + if let Some(items) = content.as_array() { |
| 209 | + summary.insert( |
| 210 | + "content_items".to_string(), |
| 211 | + serde_json::Value::Number((items.len() as u64).into()), |
| 212 | + ); |
| 213 | + let mut content_types = items |
| 214 | + .iter() |
| 215 | + .filter_map(|item| item.get("type").and_then(serde_json::Value::as_str)) |
| 216 | + .map(str::to_string) |
| 217 | + .collect::<Vec<_>>(); |
| 218 | + content_types.sort(); |
| 219 | + content_types.dedup(); |
| 220 | + if !content_types.is_empty() { |
| 221 | + summary.insert( |
| 222 | + "content_types".to_string(), |
| 223 | + serde_json::Value::Array( |
| 224 | + content_types |
| 225 | + .into_iter() |
| 226 | + .map(serde_json::Value::String) |
| 227 | + .collect(), |
| 228 | + ), |
| 229 | + ); |
| 230 | + } |
| 231 | + } |
| 232 | + } |
| 233 | + |
| 234 | + serde_json::Value::Object(summary) |
| 235 | +} |
| 236 | + |
| 237 | +fn content_text_chars(content: &serde_json::Value) -> usize { |
| 238 | + if let Some(text) = content.as_str() { |
| 239 | + return text.chars().count(); |
| 240 | + } |
| 241 | + |
| 242 | + content |
| 243 | + .as_array() |
| 244 | + .map(|items| { |
| 245 | + items |
| 246 | + .iter() |
| 247 | + .filter_map(|item| item.get("text").and_then(serde_json::Value::as_str)) |
| 248 | + .map(|text| text.chars().count()) |
| 249 | + .sum() |
| 250 | + }) |
| 251 | + .unwrap_or(0) |
| 252 | +} |
| 253 | + |
| 254 | +fn should_log_full_request_body(include_sensitive_diagnostics: bool) -> bool { |
| 255 | + include_sensitive_diagnostics |
| 256 | +} |
| 257 | + |
107 | 258 | pub(crate) fn log_request_body(target: &str, label: &str, request_body: &serde_json::Value) { |
| 259 | + if should_log_full_request_body(crate::diagnostics::include_sensitive_diagnostics()) { |
| 260 | + log::debug!( |
| 261 | + target: target, |
| 262 | + "{}\n{}", |
| 263 | + label, |
| 264 | + serde_json::to_string_pretty(request_body) |
| 265 | + .unwrap_or_else(|_| "serialization failed".to_string()) |
| 266 | + ); |
| 267 | + return; |
| 268 | + } |
| 269 | + |
| 270 | + let summary_label = label.trim_end_matches(':'); |
108 | 271 | log::debug!( |
109 | 272 | target: target, |
110 | | - "{}\n{}", |
111 | | - label, |
112 | | - serde_json::to_string_pretty(request_body) |
| 273 | + "{} summary:\n{}", |
| 274 | + summary_label, |
| 275 | + serde_json::to_string_pretty(&summarize_request_body_for_log(request_body)) |
113 | 276 | .unwrap_or_else(|_| "serialization failed".to_string()) |
114 | 277 | ); |
115 | 278 | } |
@@ -146,3 +309,50 @@ pub(crate) fn collect_function_declaration_names_or_object_keys( |
146 | 309 | .collect() |
147 | 310 | } |
148 | 311 | } |
| 312 | + |
| 313 | +#[cfg(test)] |
| 314 | +mod tests { |
| 315 | + use super::should_log_full_request_body; |
| 316 | + use super::summarize_request_body_for_log; |
| 317 | + |
| 318 | + #[test] |
| 319 | + fn request_body_log_summary_keeps_shape_without_message_contents() { |
| 320 | + let request_body = serde_json::json!({ |
| 321 | + "model": "kimi-k2.6", |
| 322 | + "stream": true, |
| 323 | + "max_tokens": 32000, |
| 324 | + "system": "secret system context", |
| 325 | + "messages": [ |
| 326 | + { "role": "user", "content": "secret user message" }, |
| 327 | + { |
| 328 | + "role": "assistant", |
| 329 | + "content": [ |
| 330 | + { "type": "text", "text": "secret assistant message" }, |
| 331 | + { "type": "tool_use", "id": "tool-1", "name": "Read" } |
| 332 | + ] |
| 333 | + } |
| 334 | + ] |
| 335 | + }); |
| 336 | + |
| 337 | + let summary = summarize_request_body_for_log(&request_body); |
| 338 | + let summary_text = serde_json::to_string(&summary).unwrap(); |
| 339 | + |
| 340 | + assert!(!summary_text.contains("secret system context")); |
| 341 | + assert!(!summary_text.contains("secret user message")); |
| 342 | + assert!(!summary_text.contains("secret assistant message")); |
| 343 | + assert_eq!(summary["model"], "kimi-k2.6"); |
| 344 | + assert_eq!(summary["stream"], true); |
| 345 | + assert_eq!(summary["max_tokens"], 32000); |
| 346 | + assert_eq!(summary["system_chars"], 21); |
| 347 | + assert_eq!(summary["message_count"], 2); |
| 348 | + assert_eq!(summary["messages"][0]["role"], "user"); |
| 349 | + assert_eq!(summary["messages"][0]["content_chars"], 19); |
| 350 | + assert_eq!(summary["messages"][1]["content_items"], 2); |
| 351 | + } |
| 352 | + |
| 353 | + #[test] |
| 354 | + fn request_body_logging_keeps_full_payload_when_sensitive_diagnostics_are_enabled() { |
| 355 | + assert!(should_log_full_request_body(true)); |
| 356 | + assert!(!should_log_full_request_body(false)); |
| 357 | + } |
| 358 | +} |
0 commit comments