1use crate::id::{IdentityError, ToolName, MAX_IDENTITY_BYTES};
6use crate::limits::InputLimits;
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
12pub struct CanonicalInput {
13 messages: Vec<CanonicalMessage>,
14}
15
16impl CanonicalInput {
17 pub fn try_new(
19 messages: Vec<CanonicalMessage>,
20 limits: &InputLimits,
21 ) -> Result<Self, InputValidationError> {
22 if messages.is_empty() {
23 return Err(InputValidationError::EmptyMessages);
24 }
25 if messages.len() > limits.max_messages {
26 return Err(InputValidationError::TooManyMessages {
27 count: messages.len(),
28 max: limits.max_messages,
29 });
30 }
31
32 let mut aggregate_text = 0usize;
33 let mut seen_tool_call_ids: Vec<String> = Vec::new();
34
35 for (index, msg) in messages.iter().enumerate() {
36 msg.validate(limits, index, &mut aggregate_text, &mut seen_tool_call_ids)?;
37 }
38
39 if aggregate_text > limits.max_aggregate_text_bytes {
40 return Err(InputValidationError::AggregateTextTooLarge {
41 bytes: aggregate_text,
42 max: limits.max_aggregate_text_bytes,
43 });
44 }
45
46 Ok(Self { messages })
47 }
48
49 pub fn messages(&self) -> &[CanonicalMessage] {
51 &self.messages
52 }
53
54 pub fn into_messages(self) -> Vec<CanonicalMessage> {
56 self.messages
57 }
58}
59
60#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
62pub enum CanonicalMessage {
63 System {
65 content: Vec<TextPart>,
67 name: Option<String>,
69 },
70 User {
72 content: Vec<TextPart>,
74 name: Option<String>,
76 },
77 Assistant {
79 content: Vec<TextPart>,
81 tool_calls: Vec<CanonicalAssistantToolCall>,
83 },
84 Tool {
86 tool_call_id: String,
88 content: Vec<TextPart>,
90 },
91}
92
93#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
95pub struct TextPart {
96 text: String,
97}
98
99impl TextPart {
100 pub fn try_new(
102 text: impl Into<String>,
103 max_bytes: usize,
104 ) -> Result<Self, InputValidationError> {
105 let text = text.into();
106 if text.is_empty() {
107 return Err(InputValidationError::EmptyTextPart);
108 }
109 if text.len() > max_bytes {
110 return Err(InputValidationError::TextPartTooLarge {
111 bytes: text.len(),
112 max: max_bytes,
113 });
114 }
115 Ok(Self { text })
116 }
117
118 pub fn text(&self) -> &str {
120 &self.text
121 }
122}
123
124#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
126pub struct CanonicalAssistantToolCall {
127 pub tool_call_id: String,
129 pub tool_name: ToolName,
131 pub arguments: serde_json::Value,
133}
134
135impl CanonicalMessage {
136 fn validate(
137 &self,
138 limits: &InputLimits,
139 index: usize,
140 aggregate_text: &mut usize,
141 seen_tool_call_ids: &mut Vec<String>,
142 ) -> Result<(), InputValidationError> {
143 match self {
144 Self::System { content, name } | Self::User { content, name } => {
145 validate_name(name, limits)?;
146 validate_text_parts(content, limits, true, aggregate_text)?;
147 }
148 Self::Assistant {
149 content,
150 tool_calls,
151 } => {
152 if content.is_empty() && tool_calls.is_empty() {
153 return Err(InputValidationError::EmptyAssistant { index });
154 }
155 validate_text_parts(content, limits, false, aggregate_text)?;
156 if tool_calls.len() > limits.max_tool_calls {
157 return Err(InputValidationError::TooManyToolCalls {
158 count: tool_calls.len(),
159 max: limits.max_tool_calls,
160 });
161 }
162 for call in tool_calls {
163 validate_tool_call_id(&call.tool_call_id, limits)?;
164 if seen_tool_call_ids.iter().any(|id| id == &call.tool_call_id) {
165 return Err(InputValidationError::DuplicateToolCallId {
166 id: call.tool_call_id.clone(),
167 });
168 }
169 seen_tool_call_ids.push(call.tool_call_id.clone());
170 validate_json_value(
171 &call.arguments,
172 limits.max_json_depth,
173 limits.max_tool_argument_bytes,
174 )?;
175 let _ = call.tool_name.as_str(); }
177 }
178 Self::Tool {
179 tool_call_id,
180 content,
181 } => {
182 validate_tool_call_id(tool_call_id, limits)?;
183 if !seen_tool_call_ids.iter().any(|id| id == tool_call_id) {
184 return Err(InputValidationError::UnknownToolCallId {
185 id: tool_call_id.clone(),
186 });
187 }
188 validate_text_parts(content, limits, true, aggregate_text)?;
189 }
190 }
191 Ok(())
192 }
193}
194
195fn validate_name(name: &Option<String>, limits: &InputLimits) -> Result<(), InputValidationError> {
196 if let Some(n) = name {
197 if n.is_empty() {
198 return Err(InputValidationError::EmptyName);
199 }
200 if n.len() > limits.max_name_bytes {
201 return Err(InputValidationError::NameTooLong {
202 bytes: n.len(),
203 max: limits.max_name_bytes,
204 });
205 }
206 if n.chars().any(|c| c.is_control()) {
207 return Err(InputValidationError::ControlCharacter);
208 }
209 }
210 Ok(())
211}
212
213fn validate_tool_call_id(id: &str, limits: &InputLimits) -> Result<(), InputValidationError> {
214 if id.is_empty() {
215 return Err(InputValidationError::EmptyToolCallId);
216 }
217 if id.len() > limits.max_tool_call_id_bytes {
218 return Err(InputValidationError::ToolCallIdTooLong {
219 bytes: id.len(),
220 max: limits.max_tool_call_id_bytes,
221 });
222 }
223 if id.chars().any(|c| c.is_control()) {
224 return Err(InputValidationError::ControlCharacter);
225 }
226 Ok(())
227}
228
229fn validate_text_parts(
230 parts: &[TextPart],
231 limits: &InputLimits,
232 require_non_empty: bool,
233 aggregate_text: &mut usize,
234) -> Result<(), InputValidationError> {
235 if require_non_empty && parts.is_empty() {
236 return Err(InputValidationError::EmptyTextParts);
237 }
238 if parts.len() > limits.max_content_parts {
239 return Err(InputValidationError::TooManyContentParts {
240 count: parts.len(),
241 max: limits.max_content_parts,
242 });
243 }
244 for p in parts {
245 if p.text.is_empty() {
246 return Err(InputValidationError::EmptyTextPart);
247 }
248 if p.text.len() > limits.max_text_part_bytes {
249 return Err(InputValidationError::TextPartTooLarge {
250 bytes: p.text.len(),
251 max: limits.max_text_part_bytes,
252 });
253 }
254 *aggregate_text = aggregate_text.saturating_add(p.text.len());
255 }
256 Ok(())
257}
258
259fn validate_json_value(
260 value: &serde_json::Value,
261 max_depth: u32,
262 max_bytes: usize,
263) -> Result<(), InputValidationError> {
264 let depth = json_depth(value);
265 if depth > max_depth {
266 return Err(InputValidationError::JsonTooDeep {
267 depth,
268 max: max_depth,
269 });
270 }
271 let encoded = serde_json::to_vec(value).map_err(|_| InputValidationError::JsonEncodeFailed)?;
272 if encoded.len() > max_bytes {
273 return Err(InputValidationError::ToolArgumentsTooLarge {
274 bytes: encoded.len(),
275 max: max_bytes,
276 });
277 }
278 Ok(())
279}
280
281fn json_depth(value: &serde_json::Value) -> u32 {
282 match value {
283 serde_json::Value::Array(items) => 1 + items.iter().map(json_depth).max().unwrap_or(0),
284 serde_json::Value::Object(map) => 1 + map.values().map(json_depth).max().unwrap_or(0),
285 _ => 1,
286 }
287}
288
289pub fn user_text_input(text: impl Into<String>) -> Result<CanonicalInput, InputValidationError> {
291 let limits = InputLimits::default();
292 let part = TextPart::try_new(text, limits.max_text_part_bytes)?;
293 CanonicalInput::try_new(
294 vec![CanonicalMessage::User {
295 content: vec![part],
296 name: None,
297 }],
298 &limits,
299 )
300}
301
302#[derive(Clone, Debug, Error, PartialEq, Eq)]
304pub enum InputValidationError {
305 #[error("canonical input requires at least one message")]
307 EmptyMessages,
308 #[error("message count {count} exceeds max {max}")]
310 TooManyMessages {
311 count: usize,
313 max: usize,
315 },
316 #[error("aggregate text bytes {bytes} exceeds max {max}")]
318 AggregateTextTooLarge {
319 bytes: usize,
321 max: usize,
323 },
324 #[error("message requires at least one text part")]
326 EmptyTextParts,
327 #[error("text part must be non-empty")]
329 EmptyTextPart,
330 #[error("text part bytes {bytes} exceeds max {max}")]
332 TextPartTooLarge {
333 bytes: usize,
335 max: usize,
337 },
338 #[error("content part count {count} exceeds max {max}")]
340 TooManyContentParts {
341 count: usize,
343 max: usize,
345 },
346 #[error("assistant message at index {index} is empty")]
348 EmptyAssistant {
349 index: usize,
351 },
352 #[error("tool call count {count} exceeds max {max}")]
354 TooManyToolCalls {
355 count: usize,
357 max: usize,
359 },
360 #[error("duplicate tool_call_id {id}")]
362 DuplicateToolCallId {
363 id: String,
365 },
366 #[error("tool message references unknown tool_call_id {id}")]
368 UnknownToolCallId {
369 id: String,
371 },
372 #[error("tool_call_id must be non-empty")]
374 EmptyToolCallId,
375 #[error("tool_call_id bytes {bytes} exceeds max {max}")]
377 ToolCallIdTooLong {
378 bytes: usize,
380 max: usize,
382 },
383 #[error("message name must be non-empty when present")]
385 EmptyName,
386 #[error("name bytes {bytes} exceeds max {max}")]
388 NameTooLong {
389 bytes: usize,
391 max: usize,
393 },
394 #[error("input string must not contain control characters")]
396 ControlCharacter,
397 #[error("JSON depth {depth} exceeds max {max}")]
399 JsonTooDeep {
400 depth: u32,
402 max: u32,
404 },
405 #[error("tool argument bytes {bytes} exceeds max {max}")]
407 ToolArgumentsTooLarge {
408 bytes: usize,
410 max: usize,
412 },
413 #[error("JSON encode failed")]
415 JsonEncodeFailed,
416 #[error(transparent)]
418 Identity(#[from] IdentityError),
419}
420
421const _: usize = MAX_IDENTITY_BYTES;
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427 use crate::id::ToolName;
428
429 #[test]
430 fn requires_messages_and_text() {
431 let limits = InputLimits::default();
432 assert!(CanonicalInput::try_new(vec![], &limits).is_err());
433 let empty_user = CanonicalMessage::User {
434 content: vec![],
435 name: None,
436 };
437 assert!(CanonicalInput::try_new(vec![empty_user], &limits).is_err());
438 }
439
440 #[test]
441 fn tool_must_reference_prior_assistant_call() {
442 let limits = InputLimits::default();
443 let part = TextPart::try_new("ok", limits.max_text_part_bytes).unwrap();
444 let bad = CanonicalMessage::Tool {
445 tool_call_id: "missing".into(),
446 content: vec![part],
447 };
448 assert!(matches!(
449 CanonicalInput::try_new(vec![bad], &limits),
450 Err(InputValidationError::UnknownToolCallId { .. })
451 ));
452 }
453
454 #[test]
455 fn historical_tool_round_trip_ok() {
456 let limits = InputLimits::default();
457 let call = CanonicalAssistantToolCall {
458 tool_call_id: "c1".into(),
459 tool_name: ToolName::try_new("search").unwrap(),
460 arguments: serde_json::json!({"q": "x"}),
461 };
462 let messages = vec![
463 CanonicalMessage::User {
464 content: vec![TextPart::try_new("hi", limits.max_text_part_bytes).unwrap()],
465 name: None,
466 },
467 CanonicalMessage::Assistant {
468 content: vec![],
469 tool_calls: vec![call],
470 },
471 CanonicalMessage::Tool {
472 tool_call_id: "c1".into(),
473 content: vec![TextPart::try_new("result", limits.max_text_part_bytes).unwrap()],
474 },
475 ];
476 let input = CanonicalInput::try_new(messages, &limits).unwrap();
477 let json = serde_json::to_string(&input).unwrap();
478 let back: CanonicalInput = serde_json::from_str(&json).unwrap();
479 assert_eq!(input, back);
480 }
481}