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)]
17pub struct CanonicalInput {
18 messages: Vec<CanonicalMessage>,
19}
20
21impl CanonicalInput {
22 pub fn try_new(
24 messages: Vec<CanonicalMessage>,
25 limits: &InputLimits,
26 ) -> Result<Self, InputValidationError> {
27 if messages.is_empty() {
28 return Err(InputValidationError::EmptyMessages);
29 }
30 if messages.len() > limits.max_messages {
31 return Err(InputValidationError::TooManyMessages {
32 count: messages.len(),
33 max: limits.max_messages,
34 });
35 }
36
37 let mut aggregate_text = 0usize;
38 let mut seen_tool_call_ids: Vec<String> = Vec::new();
39
40 for (index, msg) in messages.iter().enumerate() {
41 msg.validate(limits, index, &mut aggregate_text, &mut seen_tool_call_ids)?;
42 }
43
44 if aggregate_text > limits.max_aggregate_text_bytes {
45 return Err(InputValidationError::AggregateTextTooLarge {
46 bytes: aggregate_text,
47 max: limits.max_aggregate_text_bytes,
48 });
49 }
50
51 Ok(Self { messages })
52 }
53
54 pub fn messages(&self) -> &[CanonicalMessage] {
56 &self.messages
57 }
58
59 pub fn into_messages(self) -> Vec<CanonicalMessage> {
61 self.messages
62 }
63}
64
65pub fn estimate_canonical_input_bytes(
72 input: &CanonicalInput,
73) -> Result<usize, InputValidationError> {
74 let mut total = 0usize;
75 for msg in input.messages() {
76 match msg {
77 CanonicalMessage::System { content, name }
78 | CanonicalMessage::User { content, name } => {
79 if let Some(n) = name {
80 total = total.saturating_add(n.len());
81 }
82 for part in content {
83 total = total.saturating_add(part.text().len());
84 }
85 }
86 CanonicalMessage::Assistant {
87 content,
88 tool_calls,
89 } => {
90 for part in content {
91 total = total.saturating_add(part.text().len());
92 }
93 for call in tool_calls {
94 total = total.saturating_add(call.tool_call_id.len());
95 total = total.saturating_add(call.tool_name.as_str().len());
96 let encoded = serde_json::to_vec(&call.arguments)
97 .map_err(|_| InputValidationError::JsonEncodeFailed)?;
98 total = total.saturating_add(encoded.len());
99 }
100 }
101 CanonicalMessage::Tool {
102 tool_call_id,
103 content,
104 } => {
105 total = total.saturating_add(tool_call_id.len());
106 for part in content {
107 total = total.saturating_add(part.text().len());
108 }
109 }
110 }
111 }
112 Ok(total)
113}
114
115#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
117pub enum CanonicalMessage {
118 System {
120 content: Vec<TextPart>,
122 name: Option<String>,
124 },
125 User {
127 content: Vec<TextPart>,
129 name: Option<String>,
131 },
132 Assistant {
134 content: Vec<TextPart>,
136 tool_calls: Vec<CanonicalAssistantToolCall>,
138 },
139 Tool {
141 tool_call_id: String,
143 content: Vec<TextPart>,
145 },
146}
147
148#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
150pub struct TextPart {
151 text: String,
152}
153
154impl TextPart {
155 pub fn try_new(
157 text: impl Into<String>,
158 max_bytes: usize,
159 ) -> Result<Self, InputValidationError> {
160 let text = text.into();
161 if text.is_empty() {
162 return Err(InputValidationError::EmptyTextPart);
163 }
164 if text.len() > max_bytes {
165 return Err(InputValidationError::TextPartTooLarge {
166 bytes: text.len(),
167 max: max_bytes,
168 });
169 }
170 Ok(Self { text })
171 }
172
173 pub fn text(&self) -> &str {
175 &self.text
176 }
177}
178
179#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
181pub struct CanonicalAssistantToolCall {
182 pub tool_call_id: String,
184 pub tool_name: ToolName,
186 pub arguments: serde_json::Value,
188}
189
190impl CanonicalMessage {
191 fn validate(
192 &self,
193 limits: &InputLimits,
194 index: usize,
195 aggregate_text: &mut usize,
196 seen_tool_call_ids: &mut Vec<String>,
197 ) -> Result<(), InputValidationError> {
198 match self {
199 Self::System { content, name } | Self::User { content, name } => {
200 validate_name(name, limits)?;
201 validate_text_parts(content, limits, true, aggregate_text)?;
202 }
203 Self::Assistant {
204 content,
205 tool_calls,
206 } => {
207 if content.is_empty() && tool_calls.is_empty() {
208 return Err(InputValidationError::EmptyAssistant { index });
209 }
210 validate_text_parts(content, limits, false, aggregate_text)?;
211 if tool_calls.len() > limits.max_tool_calls {
212 return Err(InputValidationError::TooManyToolCalls {
213 count: tool_calls.len(),
214 max: limits.max_tool_calls,
215 });
216 }
217 for call in tool_calls {
218 validate_tool_call_id(&call.tool_call_id, limits)?;
219 if seen_tool_call_ids.iter().any(|id| id == &call.tool_call_id) {
220 return Err(InputValidationError::DuplicateToolCallId {
221 id: call.tool_call_id.clone(),
222 });
223 }
224 seen_tool_call_ids.push(call.tool_call_id.clone());
225 validate_json_value(
226 &call.arguments,
227 limits.max_json_depth,
228 limits.max_tool_argument_bytes,
229 )?;
230 let _ = call.tool_name.as_str(); }
232 }
233 Self::Tool {
234 tool_call_id,
235 content,
236 } => {
237 validate_tool_call_id(tool_call_id, limits)?;
238 if !seen_tool_call_ids.iter().any(|id| id == tool_call_id) {
239 return Err(InputValidationError::UnknownToolCallId {
240 id: tool_call_id.clone(),
241 });
242 }
243 validate_text_parts(content, limits, true, aggregate_text)?;
244 }
245 }
246 Ok(())
247 }
248}
249
250fn validate_name(name: &Option<String>, limits: &InputLimits) -> Result<(), InputValidationError> {
251 if let Some(n) = name {
252 if n.is_empty() {
253 return Err(InputValidationError::EmptyName);
254 }
255 if n.len() > limits.max_name_bytes {
256 return Err(InputValidationError::NameTooLong {
257 bytes: n.len(),
258 max: limits.max_name_bytes,
259 });
260 }
261 if n.chars().any(|c| c.is_control()) {
262 return Err(InputValidationError::ControlCharacter);
263 }
264 }
265 Ok(())
266}
267
268fn validate_tool_call_id(id: &str, limits: &InputLimits) -> Result<(), InputValidationError> {
269 if id.is_empty() {
270 return Err(InputValidationError::EmptyToolCallId);
271 }
272 if id.len() > limits.max_tool_call_id_bytes {
273 return Err(InputValidationError::ToolCallIdTooLong {
274 bytes: id.len(),
275 max: limits.max_tool_call_id_bytes,
276 });
277 }
278 if id.chars().any(|c| c.is_control()) {
279 return Err(InputValidationError::ControlCharacter);
280 }
281 Ok(())
282}
283
284fn validate_text_parts(
285 parts: &[TextPart],
286 limits: &InputLimits,
287 require_non_empty: bool,
288 aggregate_text: &mut usize,
289) -> Result<(), InputValidationError> {
290 if require_non_empty && parts.is_empty() {
291 return Err(InputValidationError::EmptyTextParts);
292 }
293 if parts.len() > limits.max_content_parts {
294 return Err(InputValidationError::TooManyContentParts {
295 count: parts.len(),
296 max: limits.max_content_parts,
297 });
298 }
299 for p in parts {
300 if p.text.is_empty() {
301 return Err(InputValidationError::EmptyTextPart);
302 }
303 if p.text.len() > limits.max_text_part_bytes {
304 return Err(InputValidationError::TextPartTooLarge {
305 bytes: p.text.len(),
306 max: limits.max_text_part_bytes,
307 });
308 }
309 *aggregate_text = aggregate_text.saturating_add(p.text.len());
310 }
311 Ok(())
312}
313
314fn validate_json_value(
315 value: &serde_json::Value,
316 max_depth: u32,
317 max_bytes: usize,
318) -> Result<(), InputValidationError> {
319 let depth = json_depth(value);
320 if depth > max_depth {
321 return Err(InputValidationError::JsonTooDeep {
322 depth,
323 max: max_depth,
324 });
325 }
326 let encoded = serde_json::to_vec(value).map_err(|_| InputValidationError::JsonEncodeFailed)?;
327 if encoded.len() > max_bytes {
328 return Err(InputValidationError::ToolArgumentsTooLarge {
329 bytes: encoded.len(),
330 max: max_bytes,
331 });
332 }
333 Ok(())
334}
335
336fn json_depth(value: &serde_json::Value) -> u32 {
337 match value {
338 serde_json::Value::Array(items) => 1 + items.iter().map(json_depth).max().unwrap_or(0),
339 serde_json::Value::Object(map) => 1 + map.values().map(json_depth).max().unwrap_or(0),
340 _ => 1,
341 }
342}
343
344pub fn user_text_input(text: impl Into<String>) -> Result<CanonicalInput, InputValidationError> {
346 let limits = InputLimits::default();
347 let part = TextPart::try_new(text, limits.max_text_part_bytes)?;
348 CanonicalInput::try_new(
349 vec![CanonicalMessage::User {
350 content: vec![part],
351 name: None,
352 }],
353 &limits,
354 )
355}
356
357#[derive(Clone, Debug, Error, PartialEq, Eq)]
359pub enum InputValidationError {
360 #[error("canonical input requires at least one message")]
362 EmptyMessages,
363 #[error("message count {count} exceeds max {max}")]
365 TooManyMessages {
366 count: usize,
368 max: usize,
370 },
371 #[error("aggregate text bytes {bytes} exceeds max {max}")]
373 AggregateTextTooLarge {
374 bytes: usize,
376 max: usize,
378 },
379 #[error("message requires at least one text part")]
381 EmptyTextParts,
382 #[error("text part must be non-empty")]
384 EmptyTextPart,
385 #[error("text part bytes {bytes} exceeds max {max}")]
387 TextPartTooLarge {
388 bytes: usize,
390 max: usize,
392 },
393 #[error("content part count {count} exceeds max {max}")]
395 TooManyContentParts {
396 count: usize,
398 max: usize,
400 },
401 #[error("assistant message at index {index} is empty")]
403 EmptyAssistant {
404 index: usize,
406 },
407 #[error("tool call count {count} exceeds max {max}")]
409 TooManyToolCalls {
410 count: usize,
412 max: usize,
414 },
415 #[error("duplicate tool_call_id {id}")]
417 DuplicateToolCallId {
418 id: String,
420 },
421 #[error("tool message references unknown tool_call_id {id}")]
423 UnknownToolCallId {
424 id: String,
426 },
427 #[error("tool_call_id must be non-empty")]
429 EmptyToolCallId,
430 #[error("tool_call_id bytes {bytes} exceeds max {max}")]
432 ToolCallIdTooLong {
433 bytes: usize,
435 max: usize,
437 },
438 #[error("message name must be non-empty when present")]
440 EmptyName,
441 #[error("name bytes {bytes} exceeds max {max}")]
443 NameTooLong {
444 bytes: usize,
446 max: usize,
448 },
449 #[error("input string must not contain control characters")]
451 ControlCharacter,
452 #[error("JSON depth {depth} exceeds max {max}")]
454 JsonTooDeep {
455 depth: u32,
457 max: u32,
459 },
460 #[error("tool argument bytes {bytes} exceeds max {max}")]
462 ToolArgumentsTooLarge {
463 bytes: usize,
465 max: usize,
467 },
468 #[error("JSON encode failed")]
470 JsonEncodeFailed,
471 #[error(transparent)]
473 Identity(#[from] IdentityError),
474}
475
476const _: usize = MAX_IDENTITY_BYTES;
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482 use crate::id::ToolName;
483
484 #[test]
485 fn requires_messages_and_text() {
486 let limits = InputLimits::default();
487 assert!(CanonicalInput::try_new(vec![], &limits).is_err());
488 let empty_user = CanonicalMessage::User {
489 content: vec![],
490 name: None,
491 };
492 assert!(CanonicalInput::try_new(vec![empty_user], &limits).is_err());
493 }
494
495 #[test]
496 fn tool_must_reference_prior_assistant_call() {
497 let limits = InputLimits::default();
498 let part = TextPart::try_new("ok", limits.max_text_part_bytes).unwrap();
499 let bad = CanonicalMessage::Tool {
500 tool_call_id: "missing".into(),
501 content: vec![part],
502 };
503 assert!(matches!(
504 CanonicalInput::try_new(vec![bad], &limits),
505 Err(InputValidationError::UnknownToolCallId { .. })
506 ));
507 }
508
509 #[test]
510 fn historical_tool_round_trip_ok() {
511 let limits = InputLimits::default();
512 let call = CanonicalAssistantToolCall {
513 tool_call_id: "c1".into(),
514 tool_name: ToolName::try_new("search").unwrap(),
515 arguments: serde_json::json!({"q": "x"}),
516 };
517 let messages = vec![
518 CanonicalMessage::User {
519 content: vec![TextPart::try_new("hi", limits.max_text_part_bytes).unwrap()],
520 name: None,
521 },
522 CanonicalMessage::Assistant {
523 content: vec![],
524 tool_calls: vec![call],
525 },
526 CanonicalMessage::Tool {
527 tool_call_id: "c1".into(),
528 content: vec![TextPart::try_new("result", limits.max_text_part_bytes).unwrap()],
529 },
530 ];
531 let input = CanonicalInput::try_new(messages, &limits).unwrap();
532 let json = serde_json::to_string(&input).unwrap();
533 let back: CanonicalInput = serde_json::from_str(&json).unwrap();
534 assert_eq!(input, back);
535 }
536
537 #[test]
538 fn estimate_counts_names_ids_and_tool_arguments() {
539 let limits = InputLimits::default();
540 let args = serde_json::json!({"q": "abcdefghij"}); let encoded_args = serde_json::to_vec(&args).unwrap().len();
542 let call = CanonicalAssistantToolCall {
543 tool_call_id: "call-id-123".into(),
544 tool_name: ToolName::try_new("search").unwrap(),
545 arguments: args,
546 };
547 let input = CanonicalInput::try_new(
548 vec![
549 CanonicalMessage::User {
550 content: vec![TextPart::try_new("hi", limits.max_text_part_bytes).unwrap()],
551 name: Some("alice".into()),
552 },
553 CanonicalMessage::Assistant {
554 content: vec![],
555 tool_calls: vec![call],
556 },
557 CanonicalMessage::Tool {
558 tool_call_id: "call-id-123".into(),
559 content: vec![TextPart::try_new("ok", limits.max_text_part_bytes).unwrap()],
560 },
561 ],
562 &limits,
563 )
564 .unwrap();
565
566 let bytes = estimate_canonical_input_bytes(&input).unwrap();
567 let expected =
569 2 + 5 + "call-id-123".len() + "search".len() + encoded_args + "call-id-123".len() + 2;
570 assert_eq!(bytes, expected);
571 assert!(
572 bytes > 2 + 2,
573 "tool args/ids/names must increase estimate beyond text-only"
574 );
575 }
576}