Skip to main content

openai_protocol/builders/realtime/
server_event.rs

1//! Constructors and streaming context builders for ServerEvent
2//!
3//! # One-shot events
4//!
5//! Simple events are constructed via associated functions on `ServerEvent`:
6//!
7//! ```ignore
8//! ServerEvent::session_created("evt_1", config)
9//! ServerEvent::error_event("evt_2", error)
10//! ```
11//!
12//! # Streaming events (hierarchical context builders)
13//!
14//! Response-streaming events share stable fields (`response_id`, `item_id`,
15//! `output_index`, `content_index`). Context builders capture this shared
16//! state and can be reused across many events — only `event_id` varies per call:
17//!
18//! ```ignore
19//! // One-shot chaining:
20//! ResponseEventBuilder::new("resp_1")
21//!     .for_item("item_1", 0)
22//!     .for_content(0)
23//!     .output_text_delta("evt_1", "Hello")
24//!
25//! // Streaming hot path — reuse the context, no rebuild:
26//! let ctx = ResponseEventBuilder::new("resp_1")
27//!     .for_item("item_1", 0)
28//!     .for_content(0);
29//! for chunk in chunks {
30//!     send(ctx.output_text_delta(next_event_id(), chunk));
31//! }
32//! ```
33
34use crate::{
35    realtime_conversation::RealtimeConversationItem,
36    realtime_events::{
37        Conversation, LogProbProperties, RealtimeError, RealtimeRateLimit, ResponseContentPart,
38        ServerEvent, SessionConfig, TranscriptionError, TranscriptionUsage,
39    },
40    realtime_response::RealtimeResponse,
41};
42
43// ============================================================================
44// ServerEvent associated constructors (one-shot events)
45// ============================================================================
46
47impl ServerEvent {
48    // ---- Session events ----
49
50    /// Build a `session.created` event.
51    pub fn session_created(event_id: impl Into<String>, session: SessionConfig) -> Self {
52        Self::SessionCreated {
53            event_id: event_id.into(),
54            session: Box::new(session),
55        }
56    }
57
58    /// Build a `session.updated` event.
59    pub fn session_updated(event_id: impl Into<String>, session: SessionConfig) -> Self {
60        Self::SessionUpdated {
61            event_id: event_id.into(),
62            session: Box::new(session),
63        }
64    }
65
66    // ---- Conversation events ----
67
68    /// Build a `conversation.created` event.
69    pub fn conversation_created(event_id: impl Into<String>, conversation: Conversation) -> Self {
70        Self::ConversationCreated {
71            event_id: event_id.into(),
72            conversation,
73        }
74    }
75
76    /// Build a `conversation.item.created` event.
77    pub fn conversation_item_created(
78        event_id: impl Into<String>,
79        previous_item_id: Option<String>,
80        item: RealtimeConversationItem,
81    ) -> Self {
82        Self::ConversationItemCreated {
83            event_id: event_id.into(),
84            previous_item_id,
85            item,
86        }
87    }
88
89    /// Build a `conversation.item.added` event.
90    pub fn conversation_item_added(
91        event_id: impl Into<String>,
92        previous_item_id: Option<String>,
93        item: RealtimeConversationItem,
94    ) -> Self {
95        Self::ConversationItemAdded {
96            event_id: event_id.into(),
97            previous_item_id,
98            item,
99        }
100    }
101
102    /// Build a `conversation.item.done` event.
103    pub fn conversation_item_done(
104        event_id: impl Into<String>,
105        previous_item_id: Option<String>,
106        item: RealtimeConversationItem,
107    ) -> Self {
108        Self::ConversationItemDone {
109            event_id: event_id.into(),
110            previous_item_id,
111            item,
112        }
113    }
114
115    /// Build a `conversation.item.deleted` event.
116    pub fn conversation_item_deleted(
117        event_id: impl Into<String>,
118        item_id: impl Into<String>,
119    ) -> Self {
120        Self::ConversationItemDeleted {
121            event_id: event_id.into(),
122            item_id: item_id.into(),
123        }
124    }
125
126    /// Build a `conversation.item.retrieved` event.
127    pub fn conversation_item_retrieved(
128        event_id: impl Into<String>,
129        item: RealtimeConversationItem,
130    ) -> Self {
131        Self::ConversationItemRetrieved {
132            event_id: event_id.into(),
133            item,
134        }
135    }
136
137    /// Build a `conversation.item.truncated` event.
138    pub fn conversation_item_truncated(
139        event_id: impl Into<String>,
140        item_id: impl Into<String>,
141        content_index: u32,
142        audio_end_ms: u32,
143    ) -> Self {
144        Self::ConversationItemTruncated {
145            event_id: event_id.into(),
146            item_id: item_id.into(),
147            content_index,
148            audio_end_ms,
149        }
150    }
151
152    // ---- Input audio transcription events ----
153
154    /// Build a `conversation.item.input_audio_transcription.completed` event.
155    pub fn input_audio_transcription_completed(
156        event_id: impl Into<String>,
157        item_id: impl Into<String>,
158        content_index: u32,
159        transcript: impl Into<String>,
160        usage: TranscriptionUsage,
161    ) -> Self {
162        Self::InputAudioTranscriptionCompleted {
163            event_id: event_id.into(),
164            item_id: item_id.into(),
165            content_index,
166            transcript: transcript.into(),
167            logprobs: None,
168            usage,
169        }
170    }
171
172    /// Build a `conversation.item.input_audio_transcription.completed` event with logprobs.
173    pub fn input_audio_transcription_completed_with_logprobs(
174        event_id: impl Into<String>,
175        item_id: impl Into<String>,
176        content_index: u32,
177        transcript: impl Into<String>,
178        usage: TranscriptionUsage,
179        logprobs: Vec<LogProbProperties>,
180    ) -> Self {
181        Self::InputAudioTranscriptionCompleted {
182            event_id: event_id.into(),
183            item_id: item_id.into(),
184            content_index,
185            transcript: transcript.into(),
186            logprobs: Some(logprobs),
187            usage,
188        }
189    }
190
191    /// Build a `conversation.item.input_audio_transcription.delta` event.
192    pub fn input_audio_transcription_delta(
193        event_id: impl Into<String>,
194        item_id: impl Into<String>,
195        content_index: Option<u32>,
196        delta: Option<String>,
197        logprobs: Option<Vec<LogProbProperties>>,
198    ) -> Self {
199        Self::InputAudioTranscriptionDelta {
200            event_id: event_id.into(),
201            item_id: item_id.into(),
202            content_index,
203            delta,
204            logprobs,
205        }
206    }
207
208    /// Build a `conversation.item.input_audio_transcription.failed` event.
209    pub fn input_audio_transcription_failed(
210        event_id: impl Into<String>,
211        item_id: impl Into<String>,
212        content_index: u32,
213        error: TranscriptionError,
214    ) -> Self {
215        Self::InputAudioTranscriptionFailed {
216            event_id: event_id.into(),
217            item_id: item_id.into(),
218            content_index,
219            error,
220        }
221    }
222
223    /// Build a `conversation.item.input_audio_transcription.segment` event.
224    #[expect(clippy::too_many_arguments)]
225    pub fn input_audio_transcription_segment(
226        event_id: impl Into<String>,
227        item_id: impl Into<String>,
228        content_index: u32,
229        text: impl Into<String>,
230        id: impl Into<String>,
231        speaker: impl Into<String>,
232        start: f32,
233        end: f32,
234    ) -> Self {
235        Self::InputAudioTranscriptionSegment {
236            event_id: event_id.into(),
237            item_id: item_id.into(),
238            content_index,
239            text: text.into(),
240            id: id.into(),
241            speaker: speaker.into(),
242            start,
243            end,
244        }
245    }
246
247    // ---- Input audio buffer events ----
248
249    /// Build an `input_audio_buffer.cleared` event.
250    pub fn input_audio_buffer_cleared(event_id: impl Into<String>) -> Self {
251        Self::InputAudioBufferCleared {
252            event_id: event_id.into(),
253        }
254    }
255
256    /// Build an `input_audio_buffer.committed` event.
257    pub fn input_audio_buffer_committed(
258        event_id: impl Into<String>,
259        item_id: impl Into<String>,
260        previous_item_id: Option<String>,
261    ) -> Self {
262        Self::InputAudioBufferCommitted {
263            event_id: event_id.into(),
264            previous_item_id,
265            item_id: item_id.into(),
266        }
267    }
268
269    /// Build an `input_audio_buffer.speech_started` event.
270    pub fn input_audio_buffer_speech_started(
271        event_id: impl Into<String>,
272        audio_start_ms: u32,
273        item_id: impl Into<String>,
274    ) -> Self {
275        Self::InputAudioBufferSpeechStarted {
276            event_id: event_id.into(),
277            audio_start_ms,
278            item_id: item_id.into(),
279        }
280    }
281
282    /// Build an `input_audio_buffer.speech_stopped` event.
283    pub fn input_audio_buffer_speech_stopped(
284        event_id: impl Into<String>,
285        audio_end_ms: u32,
286        item_id: impl Into<String>,
287    ) -> Self {
288        Self::InputAudioBufferSpeechStopped {
289            event_id: event_id.into(),
290            audio_end_ms,
291            item_id: item_id.into(),
292        }
293    }
294
295    /// Build an `input_audio_buffer.timeout_triggered` event.
296    pub fn input_audio_buffer_timeout_triggered(
297        event_id: impl Into<String>,
298        audio_start_ms: u32,
299        audio_end_ms: u32,
300        item_id: impl Into<String>,
301    ) -> Self {
302        Self::InputAudioBufferTimeoutTriggered {
303            event_id: event_id.into(),
304            audio_start_ms,
305            audio_end_ms,
306            item_id: item_id.into(),
307        }
308    }
309
310    /// Build an `input_audio_buffer.dtmf_event_received` event.
311    ///
312    /// DTMF events have no `event_id` per the OpenAI spec.
313    pub fn dtmf_event_received(event: impl Into<String>, received_at: i64) -> Self {
314        Self::InputAudioBufferDtmfEventReceived {
315            event: event.into(),
316            received_at,
317        }
318    }
319
320    // ---- MCP list tools events ----
321
322    /// Build an `mcp_list_tools.in_progress` event.
323    pub fn mcp_list_tools_in_progress(
324        event_id: impl Into<String>,
325        item_id: impl Into<String>,
326    ) -> Self {
327        Self::McpListToolsInProgress {
328            event_id: event_id.into(),
329            item_id: item_id.into(),
330        }
331    }
332
333    /// Build an `mcp_list_tools.completed` event.
334    pub fn mcp_list_tools_completed(
335        event_id: impl Into<String>,
336        item_id: impl Into<String>,
337    ) -> Self {
338        Self::McpListToolsCompleted {
339            event_id: event_id.into(),
340            item_id: item_id.into(),
341        }
342    }
343
344    /// Build an `mcp_list_tools.failed` event.
345    pub fn mcp_list_tools_failed(event_id: impl Into<String>, item_id: impl Into<String>) -> Self {
346        Self::McpListToolsFailed {
347            event_id: event_id.into(),
348            item_id: item_id.into(),
349        }
350    }
351
352    // ---- MCP call lifecycle events ----
353
354    /// Build a `response.mcp_call.in_progress` event.
355    pub fn mcp_call_in_progress(
356        event_id: impl Into<String>,
357        item_id: impl Into<String>,
358        output_index: u32,
359    ) -> Self {
360        Self::ResponseMcpCallInProgress {
361            event_id: event_id.into(),
362            output_index,
363            item_id: item_id.into(),
364        }
365    }
366
367    /// Build a `response.mcp_call.completed` event.
368    pub fn mcp_call_completed(
369        event_id: impl Into<String>,
370        item_id: impl Into<String>,
371        output_index: u32,
372    ) -> Self {
373        Self::ResponseMcpCallCompleted {
374            event_id: event_id.into(),
375            output_index,
376            item_id: item_id.into(),
377        }
378    }
379
380    /// Build a `response.mcp_call.failed` event.
381    pub fn mcp_call_failed(
382        event_id: impl Into<String>,
383        item_id: impl Into<String>,
384        output_index: u32,
385    ) -> Self {
386        Self::ResponseMcpCallFailed {
387            event_id: event_id.into(),
388            output_index,
389            item_id: item_id.into(),
390        }
391    }
392
393    // ---- Rate limits ----
394
395    /// Build a `rate_limits.updated` event.
396    pub fn rate_limits_updated(
397        event_id: impl Into<String>,
398        rate_limits: Vec<RealtimeRateLimit>,
399    ) -> Self {
400        Self::RateLimitsUpdated {
401            event_id: event_id.into(),
402            rate_limits,
403        }
404    }
405
406    // ---- Response lifecycle events ----
407
408    /// Build a `response.created` event.
409    pub fn response_created(event_id: impl Into<String>, response: RealtimeResponse) -> Self {
410        Self::ResponseCreated {
411            event_id: event_id.into(),
412            response: Box::new(response),
413        }
414    }
415
416    /// Build a `response.done` event.
417    pub fn response_done(event_id: impl Into<String>, response: RealtimeResponse) -> Self {
418        Self::ResponseDone {
419            event_id: event_id.into(),
420            response: Box::new(response),
421        }
422    }
423
424    // ---- Error ----
425
426    /// Build an `error` event.
427    pub fn error_event(event_id: impl Into<String>, error: RealtimeError) -> Self {
428        Self::Error {
429            event_id: event_id.into(),
430            error,
431        }
432    }
433}
434
435// ============================================================================
436// Level 2: ResponseEventBuilder (response_id)
437// ============================================================================
438
439/// Reusable context for response-scoped server events.
440///
441/// Holds `response_id`. Terminal methods take `event_id` per call, so the
442/// builder can be reused across many events in a streaming loop.
443/// Call `.for_item()` to descend into item-scoped events.
444#[derive(Clone, Debug)]
445pub struct ResponseEventBuilder {
446    response_id: String,
447}
448
449impl ResponseEventBuilder {
450    /// Create a new response-scoped context.
451    pub fn new(response_id: impl Into<String>) -> Self {
452        Self {
453            response_id: response_id.into(),
454        }
455    }
456
457    // ---- Transition ----
458
459    /// Descend into item-scoped events.
460    pub fn for_item(&self, item_id: impl Into<String>, output_index: u32) -> ItemEventBuilder {
461        ItemEventBuilder {
462            response_id: self.response_id.clone(),
463            item_id: item_id.into(),
464            output_index,
465        }
466    }
467
468    // ---- Output audio buffer events (WebRTC/SIP only) ----
469
470    /// Build an `output_audio_buffer.started` event.
471    pub fn output_audio_buffer_started(&self, event_id: impl Into<String>) -> ServerEvent {
472        ServerEvent::OutputAudioBufferStarted {
473            event_id: event_id.into(),
474            response_id: self.response_id.clone(),
475        }
476    }
477
478    /// Build an `output_audio_buffer.stopped` event.
479    pub fn output_audio_buffer_stopped(&self, event_id: impl Into<String>) -> ServerEvent {
480        ServerEvent::OutputAudioBufferStopped {
481            event_id: event_id.into(),
482            response_id: self.response_id.clone(),
483        }
484    }
485
486    /// Build an `output_audio_buffer.cleared` event.
487    pub fn output_audio_buffer_cleared(&self, event_id: impl Into<String>) -> ServerEvent {
488        ServerEvent::OutputAudioBufferCleared {
489            event_id: event_id.into(),
490            response_id: self.response_id.clone(),
491        }
492    }
493}
494
495// ============================================================================
496// Level 3: ItemEventBuilder (response_id + item_id + output_index)
497// ============================================================================
498
499/// Reusable context for item-scoped server events.
500///
501/// Holds `response_id`, `item_id`, and `output_index`. Terminal methods take
502/// `event_id` per call. Call `.for_content()` to descend into content-scoped events.
503#[derive(Clone, Debug)]
504pub struct ItemEventBuilder {
505    response_id: String,
506    item_id: String,
507    output_index: u32,
508}
509
510impl ItemEventBuilder {
511    /// Create a new item-scoped context directly.
512    pub fn new(
513        response_id: impl Into<String>,
514        item_id: impl Into<String>,
515        output_index: u32,
516    ) -> Self {
517        Self {
518            response_id: response_id.into(),
519            item_id: item_id.into(),
520            output_index,
521        }
522    }
523
524    // ---- Transition ----
525
526    /// Descend into content-scoped events.
527    pub fn for_content(&self, content_index: u32) -> ContentEventBuilder {
528        ContentEventBuilder {
529            response_id: self.response_id.clone(),
530            item_id: self.item_id.clone(),
531            output_index: self.output_index,
532            content_index,
533        }
534    }
535
536    // ---- Response output item events ----
537
538    /// Build a `response.output_item.added` event.
539    pub fn output_item_added(
540        &self,
541        event_id: impl Into<String>,
542        item: RealtimeConversationItem,
543    ) -> ServerEvent {
544        ServerEvent::ResponseOutputItemAdded {
545            event_id: event_id.into(),
546            response_id: self.response_id.clone(),
547            output_index: self.output_index,
548            item,
549        }
550    }
551
552    /// Build a `response.output_item.done` event.
553    pub fn output_item_done(
554        &self,
555        event_id: impl Into<String>,
556        item: RealtimeConversationItem,
557    ) -> ServerEvent {
558        ServerEvent::ResponseOutputItemDone {
559            event_id: event_id.into(),
560            response_id: self.response_id.clone(),
561            output_index: self.output_index,
562            item,
563        }
564    }
565
566    // ---- Response function call events ----
567
568    /// Build a `response.function_call_arguments.delta` event.
569    pub fn function_call_arguments_delta(
570        &self,
571        event_id: impl Into<String>,
572        call_id: impl Into<String>,
573        delta: impl Into<String>,
574    ) -> ServerEvent {
575        ServerEvent::ResponseFunctionCallArgumentsDelta {
576            event_id: event_id.into(),
577            response_id: self.response_id.clone(),
578            item_id: self.item_id.clone(),
579            output_index: self.output_index,
580            call_id: call_id.into(),
581            delta: delta.into(),
582        }
583    }
584
585    /// Build a `response.function_call_arguments.done` event.
586    pub fn function_call_arguments_done(
587        &self,
588        event_id: impl Into<String>,
589        call_id: impl Into<String>,
590        name: impl Into<String>,
591        arguments: impl Into<String>,
592    ) -> ServerEvent {
593        ServerEvent::ResponseFunctionCallArgumentsDone {
594            event_id: event_id.into(),
595            response_id: self.response_id.clone(),
596            item_id: self.item_id.clone(),
597            output_index: self.output_index,
598            call_id: call_id.into(),
599            name: name.into(),
600            arguments: arguments.into(),
601        }
602    }
603
604    // ---- Response MCP call events ----
605
606    /// Build a `response.mcp_call_arguments.delta` event.
607    pub fn mcp_call_arguments_delta(
608        &self,
609        event_id: impl Into<String>,
610        delta: impl Into<String>,
611        obfuscation: Option<String>,
612    ) -> ServerEvent {
613        ServerEvent::ResponseMcpCallArgumentsDelta {
614            event_id: event_id.into(),
615            response_id: self.response_id.clone(),
616            item_id: self.item_id.clone(),
617            output_index: self.output_index,
618            delta: delta.into(),
619            obfuscation,
620        }
621    }
622
623    /// Build a `response.mcp_call_arguments.done` event.
624    pub fn mcp_call_arguments_done(
625        &self,
626        event_id: impl Into<String>,
627        arguments: impl Into<String>,
628    ) -> ServerEvent {
629        ServerEvent::ResponseMcpCallArgumentsDone {
630            event_id: event_id.into(),
631            response_id: self.response_id.clone(),
632            item_id: self.item_id.clone(),
633            output_index: self.output_index,
634            arguments: arguments.into(),
635        }
636    }
637}
638
639// ============================================================================
640// Level 4: ContentEventBuilder
641//   (response_id + item_id + output_index + content_index)
642// ============================================================================
643
644/// Reusable context for content-scoped server events.
645///
646/// Holds `response_id`, `item_id`, `output_index`, and `content_index`.
647/// Terminal methods take `event_id` per call, enabling reuse in streaming loops.
648#[derive(Clone, Debug)]
649pub struct ContentEventBuilder {
650    response_id: String,
651    item_id: String,
652    output_index: u32,
653    content_index: u32,
654}
655
656impl ContentEventBuilder {
657    /// Create a new content-scoped context directly.
658    pub fn new(
659        response_id: impl Into<String>,
660        item_id: impl Into<String>,
661        output_index: u32,
662        content_index: u32,
663    ) -> Self {
664        Self {
665            response_id: response_id.into(),
666            item_id: item_id.into(),
667            output_index,
668            content_index,
669        }
670    }
671
672    // ---- Response content part events ----
673
674    /// Build a `response.content_part.added` event.
675    pub fn content_part_added(
676        &self,
677        event_id: impl Into<String>,
678        part: ResponseContentPart,
679    ) -> ServerEvent {
680        ServerEvent::ResponseContentPartAdded {
681            event_id: event_id.into(),
682            response_id: self.response_id.clone(),
683            item_id: self.item_id.clone(),
684            output_index: self.output_index,
685            content_index: self.content_index,
686            part,
687        }
688    }
689
690    /// Build a `response.content_part.done` event.
691    pub fn content_part_done(
692        &self,
693        event_id: impl Into<String>,
694        part: ResponseContentPart,
695    ) -> ServerEvent {
696        ServerEvent::ResponseContentPartDone {
697            event_id: event_id.into(),
698            response_id: self.response_id.clone(),
699            item_id: self.item_id.clone(),
700            output_index: self.output_index,
701            content_index: self.content_index,
702            part,
703        }
704    }
705
706    // ---- Response text events ----
707
708    /// Build a `response.output_text.delta` event.
709    pub fn output_text_delta(
710        &self,
711        event_id: impl Into<String>,
712        delta: impl Into<String>,
713    ) -> ServerEvent {
714        ServerEvent::ResponseOutputTextDelta {
715            event_id: event_id.into(),
716            response_id: self.response_id.clone(),
717            item_id: self.item_id.clone(),
718            output_index: self.output_index,
719            content_index: self.content_index,
720            delta: delta.into(),
721        }
722    }
723
724    /// Build a `response.output_text.done` event.
725    pub fn output_text_done(
726        &self,
727        event_id: impl Into<String>,
728        text: impl Into<String>,
729    ) -> ServerEvent {
730        ServerEvent::ResponseOutputTextDone {
731            event_id: event_id.into(),
732            response_id: self.response_id.clone(),
733            item_id: self.item_id.clone(),
734            output_index: self.output_index,
735            content_index: self.content_index,
736            text: text.into(),
737        }
738    }
739
740    // ---- Response audio events ----
741
742    /// Build a `response.output_audio.delta` event.
743    pub fn output_audio_delta(
744        &self,
745        event_id: impl Into<String>,
746        delta: impl Into<String>,
747    ) -> ServerEvent {
748        ServerEvent::ResponseOutputAudioDelta {
749            event_id: event_id.into(),
750            response_id: self.response_id.clone(),
751            item_id: self.item_id.clone(),
752            output_index: self.output_index,
753            content_index: self.content_index,
754            delta: delta.into(),
755        }
756    }
757
758    /// Build a `response.output_audio.done` event.
759    pub fn output_audio_done(&self, event_id: impl Into<String>) -> ServerEvent {
760        ServerEvent::ResponseOutputAudioDone {
761            event_id: event_id.into(),
762            response_id: self.response_id.clone(),
763            item_id: self.item_id.clone(),
764            output_index: self.output_index,
765            content_index: self.content_index,
766        }
767    }
768
769    // ---- Response audio transcript events ----
770
771    /// Build a `response.output_audio_transcript.delta` event.
772    pub fn output_audio_transcript_delta(
773        &self,
774        event_id: impl Into<String>,
775        delta: impl Into<String>,
776    ) -> ServerEvent {
777        ServerEvent::ResponseOutputAudioTranscriptDelta {
778            event_id: event_id.into(),
779            response_id: self.response_id.clone(),
780            item_id: self.item_id.clone(),
781            output_index: self.output_index,
782            content_index: self.content_index,
783            delta: delta.into(),
784        }
785    }
786
787    /// Build a `response.output_audio_transcript.done` event.
788    pub fn output_audio_transcript_done(
789        &self,
790        event_id: impl Into<String>,
791        transcript: impl Into<String>,
792    ) -> ServerEvent {
793        ServerEvent::ResponseOutputAudioTranscriptDone {
794            event_id: event_id.into(),
795            response_id: self.response_id.clone(),
796            item_id: self.item_id.clone(),
797            output_index: self.output_index,
798            content_index: self.content_index,
799            transcript: transcript.into(),
800        }
801    }
802}
803
804// ============================================================================
805// Tests
806// ============================================================================
807
808#[cfg(test)]
809mod tests {
810    use super::*;
811    use crate::realtime_session::{
812        OutputModality, RealtimeSessionCreateRequest, RealtimeSessionType,
813    };
814
815    // One-shot events via ServerEvent associated functions
816    #[test]
817    fn test_session_created() {
818        let config = SessionConfig::Realtime(Box::new(RealtimeSessionCreateRequest {
819            r#type: RealtimeSessionType::Realtime,
820            output_modalities: Some(vec![OutputModality::Audio]),
821            model: None,
822            instructions: None,
823            audio: None,
824            include: None,
825            tracing: None,
826            tools: None,
827            tool_choice: None,
828            max_output_tokens: None,
829            truncation: None,
830            prompt: None,
831        }));
832
833        let event = ServerEvent::session_created("evt_1", config);
834        assert_eq!(event.event_type(), "session.created");
835        let json = serde_json::to_string(&event).expect("serialization failed");
836        assert!(json.contains("\"type\":\"session.created\""));
837    }
838
839    // Full hierarchy — verifies context propagation at every level
840    #[test]
841    fn test_full_hierarchy() {
842        let l2 = ResponseEventBuilder::new("resp_1");
843
844        // Level 2 terminal — only needs response_id
845        let event = l2.output_audio_buffer_started("evt_1");
846        assert_eq!(event.event_type(), "output_audio_buffer.started");
847        if let ServerEvent::OutputAudioBufferStarted { response_id, .. } = &event {
848            assert_eq!(response_id, "resp_1");
849        } else {
850            panic!("Expected OutputAudioBufferStarted");
851        }
852
853        // Level 3 terminal — needs response_id + item context
854        let l3 = l2.for_item("item_1", 0);
855        let item = RealtimeConversationItem::FunctionCallOutput {
856            call_id: "call_1".into(),
857            output: "result".into(),
858            id: None,
859            object: None,
860            status: None,
861        };
862        let event = l3.output_item_done("evt_2", item);
863        assert_eq!(event.event_type(), "response.output_item.done");
864        if let ServerEvent::ResponseOutputItemDone {
865            response_id,
866            output_index,
867            ..
868        } = &event
869        {
870            assert_eq!(response_id, "resp_1");
871            assert_eq!(*output_index, 0);
872        } else {
873            panic!("Expected ResponseOutputItemDone");
874        }
875
876        // Level 4 terminal — needs response_id + item + content context
877        let l4 = l3.for_content(0);
878        let event = l4.output_text_delta("evt_3", "Hello");
879        assert_eq!(event.event_type(), "response.output_text.delta");
880        if let ServerEvent::ResponseOutputTextDelta {
881            response_id,
882            item_id,
883            output_index,
884            content_index,
885            delta,
886            ..
887        } = &event
888        {
889            assert_eq!(response_id, "resp_1");
890            assert_eq!(item_id, "item_1");
891            assert_eq!(*output_index, 0);
892            assert_eq!(*content_index, 0);
893            assert_eq!(delta, "Hello");
894        } else {
895            panic!("Expected ResponseOutputTextDelta");
896        }
897    }
898
899    // Streaming reuse test — the main motivation for this refactoring
900    #[test]
901    fn test_streaming_reuse() {
902        let ctx = ResponseEventBuilder::new("resp_1")
903            .for_item("item_1", 0)
904            .for_content(0);
905
906        let chunks = ["Hello", " ", "world"];
907        let events: Vec<_> = chunks
908            .iter()
909            .enumerate()
910            .map(|(i, chunk)| ctx.output_text_delta(format!("evt_{i}"), *chunk))
911            .collect();
912
913        assert_eq!(events.len(), 3);
914        for (i, event) in events.iter().enumerate() {
915            assert_eq!(event.event_type(), "response.output_text.delta");
916            if let ServerEvent::ResponseOutputTextDelta {
917                event_id, delta, ..
918            } = event
919            {
920                assert_eq!(event_id, &format!("evt_{i}"));
921                assert_eq!(delta, chunks[i]);
922            }
923        }
924    }
925}