1use 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
43impl ServerEvent {
48 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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#[derive(Clone, Debug)]
445pub struct ResponseEventBuilder {
446 response_id: String,
447}
448
449impl ResponseEventBuilder {
450 pub fn new(response_id: impl Into<String>) -> Self {
452 Self {
453 response_id: response_id.into(),
454 }
455 }
456
457 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 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 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 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#[derive(Clone, Debug)]
504pub struct ItemEventBuilder {
505 response_id: String,
506 item_id: String,
507 output_index: u32,
508}
509
510impl ItemEventBuilder {
511 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 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 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 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 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 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 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 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#[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 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 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 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 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 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 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 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 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 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#[cfg(test)]
809mod tests {
810 use super::*;
811 use crate::realtime_session::{
812 OutputModality, RealtimeSessionCreateRequest, RealtimeSessionType,
813 };
814
815 #[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 #[test]
841 fn test_full_hierarchy() {
842 let l2 = ResponseEventBuilder::new("resp_1");
843
844 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 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 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 #[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}