1use std::collections::HashMap;
2
3use serde_json::Value;
4
5use crate::events::{EventFrame, EventPayload, SSEEventType, SSEItemType};
6use crate::executor::accumulator::AccumulatedFunctionCall;
7use crate::executor::error::{ExecutorError, ExecutorResult};
8use crate::executor::gateway_accumulator::synthetic_event;
9use crate::tool::ToolType;
10use crate::utils::common::serialize_to_string;
11
12const MAX_PENDING_FUNCTION_BYTES: usize = 256 * 1024;
13
14#[derive(Debug)]
15enum FunctionCallShape {
16 PublicFunction,
17 GatewayOwned,
18 Custom(CustomCallState),
19}
20
21#[derive(Debug)]
22struct CustomCallState {
23 public_item_id: String,
24 output_index: u32,
25 emitted_input: String,
26 input_start: Option<usize>,
27 input_cursor: usize,
28 input_done: bool,
29}
30
31#[derive(Debug, Default)]
32struct PendingFunctionCall {
33 output_index: u32,
34 frames: Vec<EventFrame>,
35 bytes: usize,
36}
37
38#[derive(Debug, Default)]
39pub(super) struct FunctionSseTranslation {
40 pub(super) frames: Vec<EventFrame>,
41 pub(super) defer_from_output_index: Option<u32>,
42}
43
44#[derive(Debug, Default)]
48pub(super) struct FunctionSseTranslator {
49 tool_types: HashMap<String, ToolType>,
50 active: HashMap<u32, FunctionCallShape>,
51 pending_unnamed: HashMap<u32, PendingFunctionCall>,
52 pending_bytes: usize,
53 first_gateway_output_index: Option<u32>,
54}
55
56impl FunctionSseTranslator {
57 pub(super) fn new(tool_types: HashMap<String, ToolType>) -> Self {
58 Self {
59 tool_types,
60 ..Self::default()
61 }
62 }
63
64 pub(super) fn translate(
65 &mut self,
66 frame: EventFrame,
67 call: Option<AccumulatedFunctionCall<'_>>,
68 ) -> ExecutorResult<FunctionSseTranslation> {
69 let mut translated = match &frame.payload {
70 EventPayload::OutputItemAdded {
71 item_id,
72 item_type: SSEItemType::FunctionCall,
73 output_index,
74 name: Some(name),
75 ..
76 } => self.start_call(item_id, name, *output_index, Some(frame.clone()), call),
77 EventPayload::OutputItemAdded {
78 item_id: _,
79 item_type: SSEItemType::FunctionCall,
80 output_index,
81 name: None,
82 ..
83 } => self.buffer_unnamed(*output_index, frame),
84 EventPayload::FunctionCallArgsDelta {
85 item_id, output_index, ..
86 } => self.translate_delta(item_id, *output_index, frame.clone(), call),
87 EventPayload::FunctionCallArgsDone {
88 item_id,
89 name,
90 output_index,
91 ..
92 } => self.finish_arguments(item_id, name, *output_index, frame.clone(), call),
93 EventPayload::OutputItemDone {
94 item_id,
95 item_type: SSEItemType::FunctionCall,
96 output_index,
97 item,
98 } => {
99 let name = item.get("name").and_then(Value::as_str).unwrap_or_default();
100 self.finish_call(item_id, name, *output_index, frame.clone(), call)
101 }
102 _ => Ok(FunctionSseTranslation {
103 frames: vec![frame],
104 defer_from_output_index: None,
105 }),
106 }?;
107 translated.defer_from_output_index = self.defer_from_output_index();
108 Ok(translated)
109 }
110
111 fn start_call(
112 &mut self,
113 item_id: &str,
114 name: &str,
115 output_index: u32,
116 original: Option<EventFrame>,
117 call: Option<AccumulatedFunctionCall<'_>>,
118 ) -> ExecutorResult<FunctionSseTranslation> {
119 match self.tool_type(name) {
120 ToolType::Custom => {
121 let public_item_id = call.as_ref().map_or_else(
122 || crate::tool::custom::public_item_id(item_id),
123 |call| crate::tool::custom::public_item_id(&call.item.id),
124 );
125 self.active.insert(
126 output_index,
127 FunctionCallShape::Custom(CustomCallState {
128 public_item_id,
129 output_index,
130 emitted_input: String::new(),
131 input_start: None,
132 input_cursor: 0,
133 input_done: false,
134 }),
135 );
136 Ok(FunctionSseTranslation {
137 frames: call
138 .map(|call| custom_added_frame(&call))
139 .transpose()?
140 .into_iter()
141 .collect(),
142 defer_from_output_index: None,
143 })
144 }
145 ToolType::Mcp | ToolType::WebSearch | ToolType::FileSearch | ToolType::CodeInterpreter => {
146 if self.first_gateway_output_index.is_none_or(|first| output_index < first) {
147 self.first_gateway_output_index = Some(output_index);
148 }
149 self.active.insert(output_index, FunctionCallShape::GatewayOwned);
150 Ok(FunctionSseTranslation::default())
151 }
152 ToolType::Function | ToolType::CodexNamespace => {
153 self.active.insert(output_index, FunctionCallShape::PublicFunction);
154 Ok(FunctionSseTranslation {
155 frames: original.into_iter().collect(),
156 defer_from_output_index: None,
157 })
158 }
159 }
160 }
161
162 fn translate_delta(
163 &mut self,
164 _item_id: &str,
165 output_index: u32,
166 original: EventFrame,
167 call: Option<AccumulatedFunctionCall<'_>>,
168 ) -> ExecutorResult<FunctionSseTranslation> {
169 match self.active.get_mut(&output_index) {
170 Some(FunctionCallShape::PublicFunction) => Ok(FunctionSseTranslation {
171 frames: vec![original],
172 defer_from_output_index: None,
173 }),
174 Some(FunctionCallShape::GatewayOwned) => Ok(FunctionSseTranslation::default()),
175 Some(FunctionCallShape::Custom(state)) => {
176 let frame = match call {
177 Some(call) => incremental_custom_delta(state, call.arguments())?,
178 None => None,
179 };
180 Ok(FunctionSseTranslation {
181 frames: frame.into_iter().collect(),
182 defer_from_output_index: None,
183 })
184 }
185 None => self.buffer_unnamed(output_index, original),
186 }
187 }
188
189 fn finish_arguments(
190 &mut self,
191 item_id: &str,
192 name: &str,
193 output_index: u32,
194 original: EventFrame,
195 call: Option<AccumulatedFunctionCall<'_>>,
196 ) -> ExecutorResult<FunctionSseTranslation> {
197 let mut translated = self.resolve_pending(item_id, name, output_index, call)?;
198 match self.active.get_mut(&output_index) {
199 Some(FunctionCallShape::PublicFunction) | None => translated.frames.push(original),
200 Some(FunctionCallShape::GatewayOwned) => {}
201 Some(FunctionCallShape::Custom(state)) => {
202 if let Some(call) = call {
203 translated.frames.extend(finish_custom_input(state, call.arguments())?);
204 }
205 }
206 }
207 Ok(translated)
208 }
209
210 fn finish_call(
211 &mut self,
212 item_id: &str,
213 name: &str,
214 output_index: u32,
215 original: EventFrame,
216 call: Option<AccumulatedFunctionCall<'_>>,
217 ) -> ExecutorResult<FunctionSseTranslation> {
218 let mut translated = self.resolve_pending(item_id, name, output_index, call)?;
219 match self.active.remove(&output_index) {
220 Some(FunctionCallShape::PublicFunction) | None => translated.frames.push(original),
221 Some(FunctionCallShape::GatewayOwned) => {}
222 Some(FunctionCallShape::Custom(mut state)) => {
223 if let Some(call) = call {
224 translated
225 .frames
226 .extend(finish_custom_input(&mut state, call.arguments())?);
227 translated.frames.push(custom_done_frame(&state, &call)?);
228 }
229 }
230 }
231 Ok(translated)
232 }
233
234 fn resolve_pending(
235 &mut self,
236 item_id: &str,
237 name: &str,
238 output_index: u32,
239 call: Option<AccumulatedFunctionCall<'_>>,
240 ) -> ExecutorResult<FunctionSseTranslation> {
241 if self.active.contains_key(&output_index) {
242 return Ok(FunctionSseTranslation::default());
243 }
244
245 let pending = self.take_pending(output_index);
246 let original_added = pending.iter().find(|frame| {
247 matches!(
248 frame.payload,
249 EventPayload::OutputItemAdded {
250 item_type: SSEItemType::FunctionCall,
251 ..
252 }
253 )
254 });
255 let mut translated = self.start_call(item_id, name, output_index, original_added.cloned(), call)?;
256
257 for frame in pending {
258 if let EventPayload::FunctionCallArgsDelta { output_index, .. } = &frame.payload {
259 let delta = self.translate_delta(item_id, *output_index, frame.clone(), call)?;
260 translated.frames.extend(delta.frames);
261 }
262 }
263 Ok(translated)
264 }
265
266 fn tool_type(&self, name: &str) -> ToolType {
267 self.tool_types.get(name).copied().unwrap_or(ToolType::Function)
268 }
269
270 fn defer_from_output_index(&self) -> Option<u32> {
271 self.first_gateway_output_index
272 .into_iter()
273 .chain(self.pending_unnamed.values().map(|pending| pending.output_index))
274 .min()
275 }
276
277 fn buffer_unnamed(&mut self, output_index: u32, frame: EventFrame) -> ExecutorResult<FunctionSseTranslation> {
278 let bytes = serialize_to_string(&frame.wire)
279 .map_err(ExecutorError::JsonError)?
280 .len();
281 if self.pending_bytes.saturating_add(bytes) > MAX_PENDING_FUNCTION_BYTES {
282 return Err(ExecutorError::StreamError(format!(
283 "unnamed function-call SSE exceeded {MAX_PENDING_FUNCTION_BYTES} buffered bytes"
284 )));
285 }
286 let pending = self
287 .pending_unnamed
288 .entry(output_index)
289 .or_insert_with(|| PendingFunctionCall {
290 output_index,
291 ..PendingFunctionCall::default()
292 });
293 pending.frames.push(frame);
294 pending.bytes = pending.bytes.saturating_add(bytes);
295 self.pending_bytes = self.pending_bytes.saturating_add(bytes);
296 Ok(FunctionSseTranslation::default())
297 }
298
299 fn take_pending(&mut self, output_index: u32) -> Vec<EventFrame> {
300 let Some(pending) = self.pending_unnamed.remove(&output_index) else {
301 return Vec::new();
302 };
303 self.pending_bytes = self.pending_bytes.saturating_sub(pending.bytes);
304 pending.frames
305 }
306}
307
308fn custom_added_frame(call: &AccumulatedFunctionCall<'_>) -> ExecutorResult<EventFrame> {
309 custom_frame(
310 SSEEventType::OutputItemAdded,
311 call.output_index,
312 [(
313 "item".to_owned(),
314 serde_json::json!({
315 "id": crate::tool::custom::public_item_id(&call.item.id),
316 "type": "custom_tool_call",
317 "status": "in_progress",
318 "call_id": call.item.call_id,
319 "input": "",
320 "name": call.item.name,
321 }),
322 )],
323 )
324}
325
326fn incremental_custom_delta(state: &mut CustomCallState, arguments: &str) -> ExecutorResult<Option<EventFrame>> {
327 ensure_function_call_size(arguments)?;
328 let Some(delta) = partial_custom_input(state, arguments)? else {
329 return Ok(None);
330 };
331 state.emitted_input.push_str(&delta);
332 custom_frame(
333 SSEEventType::CustomToolCallInputDelta,
334 state.output_index,
335 [
336 ("delta".to_owned(), Value::String(delta)),
337 ("item_id".to_owned(), Value::String(state.public_item_id.clone())),
338 ],
339 )
340 .map(Some)
341}
342
343fn finish_custom_input(state: &mut CustomCallState, arguments: &str) -> ExecutorResult<Vec<EventFrame>> {
344 if state.input_done {
345 return Ok(Vec::new());
346 }
347 ensure_function_call_size(arguments)?;
348 let input = crate::tool::custom::input_from_arguments(arguments);
349 let Some(remaining) = input.strip_prefix(&state.emitted_input) else {
350 return Err(ExecutorError::StreamError(
351 "authoritative custom tool input contradicts streamed custom tool input".to_owned(),
352 ));
353 };
354 let remaining = (!remaining.is_empty()).then(|| remaining.to_owned());
355 state.emitted_input.clone_from(&input);
356 state.input_done = true;
357
358 let mut frames = Vec::with_capacity(2);
359 if let Some(delta) = remaining {
360 frames.push(custom_frame(
361 SSEEventType::CustomToolCallInputDelta,
362 state.output_index,
363 [
364 ("delta".to_owned(), Value::String(delta)),
365 ("item_id".to_owned(), Value::String(state.public_item_id.clone())),
366 ],
367 )?);
368 }
369 frames.push(custom_frame(
370 SSEEventType::CustomToolCallInputDone,
371 state.output_index,
372 [
373 ("input".to_owned(), Value::String(input)),
374 ("item_id".to_owned(), Value::String(state.public_item_id.clone())),
375 ],
376 )?);
377 Ok(frames)
378}
379
380fn custom_done_frame(state: &CustomCallState, call: &AccumulatedFunctionCall<'_>) -> ExecutorResult<EventFrame> {
381 custom_frame(
382 SSEEventType::OutputItemDone,
383 state.output_index,
384 [(
385 "item".to_owned(),
386 serde_json::json!({
387 "id": state.public_item_id,
388 "type": "custom_tool_call",
389 "status": "completed",
390 "call_id": call.item.call_id,
391 "input": state.emitted_input,
392 "name": call.item.name,
393 }),
394 )],
395 )
396}
397
398fn custom_frame(
399 event_type: SSEEventType,
400 output_index: u32,
401 fields: impl IntoIterator<Item = (String, Value)>,
402) -> ExecutorResult<EventFrame> {
403 let mut frame = synthetic_event(event_type, fields)?;
404 frame.wire.output_index = Some(u64::from(output_index));
405 Ok(frame)
406}
407
408fn ensure_function_call_size(arguments: &str) -> ExecutorResult<()> {
409 if arguments.len() > MAX_PENDING_FUNCTION_BYTES {
410 return Err(ExecutorError::StreamError(format!(
411 "function-call SSE exceeded {MAX_PENDING_FUNCTION_BYTES} buffered bytes"
412 )));
413 }
414 Ok(())
415}
416
417fn partial_custom_input(state: &mut CustomCallState, arguments: &str) -> ExecutorResult<Option<String>> {
418 let input_start = if let Some(input_start) = state.input_start {
419 input_start
420 } else {
421 let Some(input_start) = custom_input_start(arguments) else {
422 return Ok(None);
423 };
424 state.input_start = Some(input_start);
425 state.input_cursor = input_start;
426 input_start
427 };
428 if state.input_cursor < input_start || state.input_cursor > arguments.len() {
429 return Ok(None);
430 }
431 let encoded = &arguments[state.input_cursor..];
432 let end = complete_json_string_prefix(encoded);
433 if end == 0 {
434 return Ok(None);
435 }
436 let candidate = format!("\"{}\"", &encoded[..end]);
437 let delta = serde_json::from_str::<String>(&candidate)
438 .map_err(|error| ExecutorError::StreamError(format!("invalid custom tool input string: {error}")))?;
439 state.input_cursor = state.input_cursor.saturating_add(end);
440 Ok((!delta.is_empty()).then_some(delta))
441}
442
443fn complete_json_string_prefix(value: &str) -> usize {
444 let bytes = value.as_bytes();
445 let mut index = 0;
446 while index < bytes.len() {
447 match bytes[index] {
448 b'"' => return index,
449 b'\\' => {
450 let Some(escape) = bytes.get(index + 1) else {
451 return index;
452 };
453 if *escape == b'u' {
454 let unicode_end = index.saturating_add(6);
455 if unicode_end > bytes.len() {
456 return index;
457 }
458 let Some(code_unit) = json_hex_quad(&bytes[index + 2..unicode_end]) else {
459 index = unicode_end;
460 continue;
461 };
462 if (0xD800..=0xDBFF).contains(&code_unit) {
463 let pair_end = index.saturating_add(12);
464 if pair_end > bytes.len() {
465 return index;
466 }
467 index = pair_end;
468 } else {
469 index = unicode_end;
470 }
471 } else {
472 index = index.saturating_add(2);
473 }
474 }
475 _ => index = index.saturating_add(1),
476 }
477 }
478 index
479}
480
481fn json_hex_quad(bytes: &[u8]) -> Option<u16> {
482 if bytes.len() != 4 {
483 return None;
484 }
485 bytes.iter().try_fold(0_u16, |value, byte| {
486 let digit = byte.to_ascii_lowercase();
487 let digit = match digit {
488 b'0'..=b'9' => u16::from(digit - b'0'),
489 b'a'..=b'f' => u16::from(digit - b'a' + 10),
490 _ => return None,
491 };
492 value.checked_mul(16)?.checked_add(digit)
493 })
494}
495
496fn custom_input_start(arguments: &str) -> Option<usize> {
497 let original_len = arguments.len();
498 let arguments = arguments.trim_start();
499 let arguments = arguments.strip_prefix("{}").unwrap_or(arguments).trim_start();
500 let encoded = arguments
501 .strip_prefix('{')?
502 .trim_start()
503 .strip_prefix("\"input\"")?
504 .trim_start()
505 .strip_prefix(':')?
506 .trim_start()
507 .strip_prefix('"')?;
508 Some(original_len.saturating_sub(encoded.len()))
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514 use crate::executor::accumulator::ResponseAccumulator;
515
516 fn sse(value: &Value) -> String {
517 format!("data: {value}")
518 }
519
520 fn translate(
521 accumulator: &mut ResponseAccumulator,
522 translator: &mut FunctionSseTranslator,
523 value: &Value,
524 ) -> FunctionSseTranslation {
525 accumulator
526 .process_sse_line_with_translator(&sse(value), translator)
527 .expect("translation succeeds")
528 .expect("SSE event")
529 }
530
531 #[test]
532 fn custom_function_arguments_are_emitted_incrementally() {
533 let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None);
534 let mut translator = FunctionSseTranslator::new(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)]));
535 let mut frames = Vec::new();
536
537 for event in [
538 serde_json::json!({
539 "type": "response.output_item.added",
540 "output_index": 0,
541 "item": {
542 "id": "fc_custom",
543 "type": "function_call",
544 "status": "in_progress",
545 "call_id": "call_custom",
546 "name": "raw_echo",
547 "arguments": ""
548 }
549 }),
550 serde_json::json!({
551 "type": "response.function_call_arguments.delta",
552 "output_index": 0,
553 "item_id": "fc_custom",
554 "call_id": "call_custom",
555 "delta": "{\"in"
556 }),
557 serde_json::json!({
558 "type": "response.function_call_arguments.delta",
559 "output_index": 0,
560 "item_id": "fc_custom",
561 "call_id": "call_custom",
562 "delta": "put\":\"hello "
563 }),
564 serde_json::json!({
565 "type": "response.function_call_arguments.delta",
566 "output_index": 0,
567 "item_id": "fc_custom",
568 "call_id": "call_custom",
569 "delta": "world\"}"
570 }),
571 serde_json::json!({
572 "type": "response.function_call_arguments.done",
573 "output_index": 0,
574 "item_id": "fc_custom",
575 "call_id": "call_custom",
576 "name": "raw_echo",
577 "arguments": "{\"input\":\"hello world\"}"
578 }),
579 serde_json::json!({
580 "type": "response.output_item.done",
581 "output_index": 0,
582 "item": {
583 "id": "fc_custom",
584 "type": "function_call",
585 "status": "completed",
586 "call_id": "call_custom",
587 "name": "raw_echo",
588 "arguments": "{\"input\":\"hello world\"}"
589 }
590 }),
591 ] {
592 frames.extend(translate(&mut accumulator, &mut translator, &event).frames);
593 }
594
595 assert_eq!(
596 frames.iter().map(|frame| frame.event_type).collect::<Vec<_>>(),
597 [
598 SSEEventType::OutputItemAdded,
599 SSEEventType::CustomToolCallInputDelta,
600 SSEEventType::CustomToolCallInputDelta,
601 SSEEventType::CustomToolCallInputDone,
602 SSEEventType::OutputItemDone,
603 ]
604 );
605 assert_eq!(frames[0].wire.rest["item"]["type"], "custom_tool_call");
606 assert_eq!(frames[1].wire.rest["delta"], "hello ");
607 assert_eq!(frames[2].wire.rest["delta"], "world");
608 assert_eq!(frames[3].wire.rest["input"], "hello world");
609 assert_eq!(frames[4].wire.rest["item"]["input"], "hello world");
610 }
611
612 #[test]
613 fn custom_input_deltas_match_authoritative_done_input() {
614 let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None);
615 let mut translator = FunctionSseTranslator::new(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)]));
616 let events = [
617 serde_json::json!({
618 "type": "response.output_item.added", "output_index": 0,
619 "item": {"id": "fc_1", "type": "function_call", "call_id": "call_1",
620 "name": "raw_echo", "arguments": "", "status": "in_progress"}
621 }),
622 serde_json::json!({
623 "type": "response.function_call_arguments.delta", "output_index": 0,
624 "item_id": "fc_1", "call_id": "call_1", "delta": "{\"input\":\"hello\""
625 }),
626 serde_json::json!({
627 "type": "response.function_call_arguments.done", "output_index": 0,
628 "item_id": "fc_1", "call_id": "call_1", "name": "raw_echo",
629 "arguments": "{\"input\":\"hello\",\"extra\":true}"
630 }),
631 serde_json::json!({
632 "type": "response.output_item.done", "output_index": 0,
633 "item": {"id": "fc_1", "type": "function_call", "call_id": "call_1",
634 "name": "raw_echo", "arguments": "{\"input\":\"hello\",\"extra\":true}", "status": "completed"}
635 }),
636 ];
637
638 let mut frames = Vec::new();
639 for event in events {
640 frames.extend(translate(&mut accumulator, &mut translator, &event).frames);
641 }
642 let deltas = frames
643 .iter()
644 .filter(|frame| frame.event_type == SSEEventType::CustomToolCallInputDelta)
645 .filter_map(|frame| frame.wire.rest["delta"].as_str())
646 .collect::<String>();
647 let done = frames
648 .iter()
649 .find(|frame| frame.event_type == SSEEventType::CustomToolCallInputDone)
650 .and_then(|frame| frame.wire.rest["input"].as_str())
651 .expect("input.done");
652
653 assert_eq!(deltas, done);
654 }
655
656 #[test]
657 fn custom_input_rejects_authoritative_value_that_contradicts_deltas() {
658 let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None);
659 let mut translator = FunctionSseTranslator::new(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)]));
660 let events = [
661 serde_json::json!({
662 "type": "response.output_item.added", "output_index": 0,
663 "item": {"id": "fc_1", "type": "function_call", "call_id": "call_1",
664 "name": "raw_echo", "arguments": "", "status": "in_progress"}
665 }),
666 serde_json::json!({
667 "type": "response.function_call_arguments.delta", "output_index": 0,
668 "item_id": "fc_1", "call_id": "call_1", "delta": "{\"input\":\"hello\"}"
669 }),
670 ];
671 for event in events {
672 translate(&mut accumulator, &mut translator, &event);
673 }
674 let done = serde_json::json!({
675 "type": "response.function_call_arguments.done", "output_index": 0,
676 "item_id": "fc_1", "call_id": "call_1", "name": "raw_echo",
677 "arguments": "{\"input\":\"bye\"}"
678 });
679
680 let error = accumulator
681 .process_sse_line_with_translator(&sse(&done), &mut translator)
682 .expect_err("contradictory final input must fail");
683 assert!(error.to_string().contains("contradicts streamed custom tool input"));
684 }
685
686 #[test]
687 fn malformed_custom_input_escape_is_rejected() {
688 let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None);
689 let mut translator = FunctionSseTranslator::new(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)]));
690 let added = serde_json::json!({
691 "type": "response.output_item.added", "output_index": 0,
692 "item": {"id": "fc_1", "type": "function_call", "call_id": "call_1",
693 "name": "raw_echo", "arguments": "", "status": "in_progress"}
694 });
695 translate(&mut accumulator, &mut translator, &added);
696 let delta = serde_json::json!({
697 "type": "response.function_call_arguments.delta", "output_index": 0,
698 "item_id": "fc_1", "call_id": "call_1", "delta": r#"{"input":"\q"#
699 });
700
701 let error = accumulator
702 .process_sse_line_with_translator(&sse(&delta), &mut translator)
703 .expect_err("invalid JSON string escape must fail");
704 assert!(error.to_string().contains("invalid custom tool input"));
705 }
706
707 #[test]
708 fn custom_input_waits_for_split_unicode_surrogate_pair() {
709 let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None);
710 let mut translator = FunctionSseTranslator::new(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)]));
711 let events = [
712 serde_json::json!({
713 "type": "response.output_item.added", "output_index": 0,
714 "item": {"id": "fc_1", "type": "function_call", "call_id": "call_1",
715 "name": "raw_echo", "arguments": "", "status": "in_progress"}
716 }),
717 serde_json::json!({
718 "type": "response.function_call_arguments.delta", "output_index": 0,
719 "item_id": "fc_1", "call_id": "call_1", "delta": r#"{"input":"hi \uD83D"#
720 }),
721 serde_json::json!({
722 "type": "response.function_call_arguments.delta", "output_index": 0,
723 "item_id": "fc_1", "call_id": "call_1", "delta": r#"\uDE00"}"#
724 }),
725 ];
726
727 let frames = events
728 .iter()
729 .flat_map(|event| translate(&mut accumulator, &mut translator, event).frames)
730 .collect::<Vec<_>>();
731 let input = frames
732 .iter()
733 .filter(|frame| frame.event_type == SSEEventType::CustomToolCallInputDelta)
734 .filter_map(|frame| frame.wire.rest["delta"].as_str())
735 .collect::<String>();
736
737 assert_eq!(input, "hi 😀");
738 }
739
740 #[test]
741 fn custom_input_over_limit_is_rejected() {
742 let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None);
743 let mut translator = FunctionSseTranslator::new(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)]));
744 let added = serde_json::json!({
745 "type": "response.output_item.added", "output_index": 0,
746 "item": {"id": "fc_1", "type": "function_call", "call_id": "call_1",
747 "name": "raw_echo", "arguments": "", "status": "in_progress"}
748 });
749 translate(&mut accumulator, &mut translator, &added);
750 let oversized = serde_json::json!({
751 "type": "response.function_call_arguments.delta", "output_index": 0,
752 "item_id": "fc_1", "call_id": "call_1",
753 "delta": format!("{{\"input\":\"{}", "x".repeat(MAX_PENDING_FUNCTION_BYTES + 1))
754 });
755
756 let error = accumulator
757 .process_sse_line_with_translator(&sse(&oversized), &mut translator)
758 .expect_err("oversized custom input must fail");
759 assert!(error.to_string().contains("function-call SSE exceeded"));
760 }
761
762 #[test]
763 fn ordinary_functions_pass_through_unchanged() {
764 let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None);
765 let mut translator = FunctionSseTranslator::new(HashMap::from([("echo".to_owned(), ToolType::Function)]));
766 let event = serde_json::json!({
767 "type": "response.output_item.added",
768 "output_index": 3,
769 "item": {
770 "id": "fc_echo",
771 "type": "function_call",
772 "call_id": "call_echo",
773 "name": "echo",
774 "arguments": ""
775 }
776 });
777
778 let translated = translate(&mut accumulator, &mut translator, &event);
779
780 assert_eq!(translated.frames.len(), 1);
781 assert_eq!(translated.frames[0].event_type, SSEEventType::OutputItemAdded);
782 assert_eq!(translated.frames[0].wire.rest["item"]["type"], "function_call");
783 assert_eq!(translated.defer_from_output_index, None);
784 }
785
786 #[test]
787 fn unnamed_function_frames_are_recovered_by_output_index_when_done_changes_id() {
788 let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None);
789 let mut translator = FunctionSseTranslator::new(HashMap::from([("echo".to_owned(), ToolType::Function)]));
790 let mut frames = Vec::new();
791 let mut defer_boundaries = Vec::new();
792
793 for event in [
794 serde_json::json!({
795 "type": "response.output_item.added",
796 "output_index": 1,
797 "item": {
798 "id": "fc_transient",
799 "type": "function_call",
800 "status": "in_progress",
801 "call_id": "call_echo",
802 "arguments": ""
803 }
804 }),
805 serde_json::json!({
806 "type": "response.function_call_arguments.delta",
807 "output_index": 1,
808 "item_id": "fc_transient",
809 "call_id": "call_echo",
810 "delta": "{\"value\":1}"
811 }),
812 serde_json::json!({
813 "type": "response.output_item.done",
814 "output_index": 1,
815 "item": {
816 "id": "fc_stable",
817 "type": "function_call",
818 "status": "completed",
819 "call_id": "call_echo",
820 "name": "echo",
821 "arguments": "{\"value\":1}"
822 }
823 }),
824 ] {
825 let translated = translate(&mut accumulator, &mut translator, &event);
826 defer_boundaries.push(translated.defer_from_output_index);
827 frames.extend(translated.frames);
828 }
829
830 assert_eq!(
831 frames.iter().map(|frame| frame.event_type).collect::<Vec<_>>(),
832 [
833 SSEEventType::OutputItemAdded,
834 SSEEventType::FunctionCallArgumentsDelta,
835 SSEEventType::OutputItemDone,
836 ]
837 );
838 assert_eq!(frames[0].wire.rest["item"]["id"], "fc_transient");
839 assert_eq!(frames[1].wire.rest["item_id"], "fc_transient");
840 assert_eq!(frames[2].wire.rest["item"]["id"], "fc_stable");
841 assert_eq!(defer_boundaries, [Some(1), Some(1), None]);
842 }
843
844 #[test]
845 fn parallel_unnamed_functions_with_empty_ids_remain_distinct() {
846 let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None);
847 let mut translator = FunctionSseTranslator::new(HashMap::from([
848 ("first".to_owned(), ToolType::Function),
849 ("second".to_owned(), ToolType::Function),
850 ]));
851 let events = [
852 serde_json::json!({
853 "type": "response.output_item.added", "output_index": 0,
854 "item": {"id": "", "type": "function_call", "arguments": ""}
855 }),
856 serde_json::json!({
857 "type": "response.output_item.added", "output_index": 1,
858 "item": {"id": "", "type": "function_call", "arguments": ""}
859 }),
860 serde_json::json!({
861 "type": "response.function_call_arguments.delta", "output_index": 0,
862 "item_id": "", "delta": "{\"value\":\"a\"}"
863 }),
864 serde_json::json!({
865 "type": "response.function_call_arguments.delta", "output_index": 1,
866 "item_id": "", "delta": "{\"value\":\"b\"}"
867 }),
868 serde_json::json!({
869 "type": "response.output_item.done", "output_index": 0,
870 "item": {"id": "fc_first", "type": "function_call", "call_id": "call_first",
871 "name": "first", "arguments": "{\"value\":\"a\"}", "status": "completed"}
872 }),
873 serde_json::json!({
874 "type": "response.output_item.done", "output_index": 1,
875 "item": {"id": "fc_second", "type": "function_call", "call_id": "call_second",
876 "name": "second", "arguments": "{\"value\":\"b\"}", "status": "completed"}
877 }),
878 ];
879
880 let mut frames = Vec::new();
881 for event in events {
882 frames.extend(translate(&mut accumulator, &mut translator, &event).frames);
883 }
884
885 assert_eq!(
886 frames
887 .iter()
888 .filter(|frame| frame.event_type == SSEEventType::OutputItemAdded)
889 .count(),
890 2
891 );
892 assert_eq!(
893 frames
894 .iter()
895 .filter(|frame| frame.event_type == SSEEventType::OutputItemDone)
896 .count(),
897 2
898 );
899 }
900
901 #[test]
902 fn parallel_named_custom_functions_with_empty_ids_remain_distinct() {
903 let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None);
904 let mut translator = FunctionSseTranslator::new(HashMap::from([
905 ("first".to_owned(), ToolType::Custom),
906 ("second".to_owned(), ToolType::Custom),
907 ]));
908 let events = [
909 serde_json::json!({
910 "type": "response.output_item.added", "output_index": 0,
911 "item": {"id": "", "type": "function_call", "call_id": "call_first",
912 "name": "first", "arguments": "", "status": "in_progress"}
913 }),
914 serde_json::json!({
915 "type": "response.output_item.added", "output_index": 1,
916 "item": {"id": "", "type": "function_call", "call_id": "call_second",
917 "name": "second", "arguments": "", "status": "in_progress"}
918 }),
919 serde_json::json!({
920 "type": "response.function_call_arguments.delta", "output_index": 0,
921 "item_id": "", "call_id": "call_first", "delta": "{\"input\":\"a\"}"
922 }),
923 serde_json::json!({
924 "type": "response.function_call_arguments.delta", "output_index": 1,
925 "item_id": "", "call_id": "call_second", "delta": "{\"input\":\"b\"}"
926 }),
927 ];
928
929 let frames = events
930 .iter()
931 .flat_map(|event| translate(&mut accumulator, &mut translator, event).frames)
932 .collect::<Vec<_>>();
933 let deltas = frames
934 .iter()
935 .filter(|frame| frame.event_type == SSEEventType::CustomToolCallInputDelta)
936 .map(|frame| (frame.wire.output_index, frame.wire.rest["delta"].as_str()))
937 .collect::<Vec<_>>();
938
939 assert_eq!(deltas, [(Some(0), Some("a")), (Some(1), Some("b"))]);
940 }
941
942 #[test]
943 fn unnamed_custom_function_with_empty_id_uses_one_public_id() {
944 let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None);
945 let mut translator = FunctionSseTranslator::new(HashMap::from([("raw_echo".to_owned(), ToolType::Custom)]));
946 let events = [
947 serde_json::json!({
948 "type": "response.output_item.added", "output_index": 0,
949 "item": {"id": "", "type": "function_call", "call_id": "call_1", "arguments": ""}
950 }),
951 serde_json::json!({
952 "type": "response.function_call_arguments.delta", "output_index": 0,
953 "item_id": "", "call_id": "call_1", "delta": "{\"input\":\"hello\"}"
954 }),
955 serde_json::json!({
956 "type": "response.function_call_arguments.done", "output_index": 0,
957 "item_id": "", "call_id": "call_1", "name": "raw_echo",
958 "arguments": "{\"input\":\"hello\"}"
959 }),
960 ];
961
962 let frames = events
963 .iter()
964 .flat_map(|event| translate(&mut accumulator, &mut translator, event).frames)
965 .collect::<Vec<_>>();
966 let added_id = frames
967 .iter()
968 .find(|frame| frame.event_type == SSEEventType::OutputItemAdded)
969 .and_then(|frame| frame.wire.rest["item"]["id"].as_str())
970 .expect("custom item id");
971 let lifecycle_ids = frames.iter().filter_map(|frame| {
972 matches!(
973 frame.event_type,
974 SSEEventType::CustomToolCallInputDelta | SSEEventType::CustomToolCallInputDone
975 )
976 .then(|| frame.wire.rest["item_id"].as_str())
977 .flatten()
978 });
979
980 assert!(lifecycle_ids.eq(std::iter::repeat_n(added_id, 2)));
981 }
982
983 #[test]
984 fn gateway_owned_functions_are_suppressed_and_mark_the_defer_boundary() {
985 let mut accumulator = ResponseAccumulator::new("resp_1".to_owned(), None);
986 let mut translator =
987 FunctionSseTranslator::new(HashMap::from([("web_search".to_owned(), ToolType::WebSearch)]));
988 let added = serde_json::json!({
989 "type": "response.output_item.added",
990 "output_index": 2,
991 "item": {
992 "id": "fc_search",
993 "type": "function_call",
994 "call_id": "call_search",
995 "name": "web_search",
996 "arguments": ""
997 }
998 });
999 let delta = serde_json::json!({
1000 "type": "response.function_call_arguments.delta",
1001 "output_index": 2,
1002 "item_id": "fc_search",
1003 "call_id": "call_search",
1004 "delta": "{}"
1005 });
1006
1007 let added = translate(&mut accumulator, &mut translator, &added);
1008 let delta = translate(&mut accumulator, &mut translator, &delta);
1009
1010 assert!(added.frames.is_empty());
1011 assert_eq!(added.defer_from_output_index, Some(2));
1012 assert!(delta.frames.is_empty());
1013 assert_eq!(delta.defer_from_output_index, Some(2));
1014 }
1015}