Skip to main content

agentic_core/executor/
function_sse.rs

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/// Restores normalized upstream function-call SSE to the public call shape.
45/// Tool routing remains outside this type; it receives only the request's
46/// model-visible name-to-type mapping.
47#[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}