use crate::{
realtime_conversation::RealtimeConversationItem,
realtime_events::{
Conversation, LogProbProperties, RealtimeError, RealtimeRateLimit, ResponseContentPart,
ServerEvent, SessionConfig, TranscriptionError, TranscriptionUsage,
},
realtime_response::RealtimeResponse,
};
impl ServerEvent {
pub fn session_created(event_id: impl Into<String>, session: SessionConfig) -> Self {
Self::SessionCreated {
event_id: event_id.into(),
session: Box::new(session),
}
}
pub fn session_updated(event_id: impl Into<String>, session: SessionConfig) -> Self {
Self::SessionUpdated {
event_id: event_id.into(),
session: Box::new(session),
}
}
pub fn conversation_created(event_id: impl Into<String>, conversation: Conversation) -> Self {
Self::ConversationCreated {
event_id: event_id.into(),
conversation,
}
}
pub fn conversation_item_created(
event_id: impl Into<String>,
previous_item_id: Option<String>,
item: RealtimeConversationItem,
) -> Self {
Self::ConversationItemCreated {
event_id: event_id.into(),
previous_item_id,
item,
}
}
pub fn conversation_item_added(
event_id: impl Into<String>,
previous_item_id: Option<String>,
item: RealtimeConversationItem,
) -> Self {
Self::ConversationItemAdded {
event_id: event_id.into(),
previous_item_id,
item,
}
}
pub fn conversation_item_done(
event_id: impl Into<String>,
previous_item_id: Option<String>,
item: RealtimeConversationItem,
) -> Self {
Self::ConversationItemDone {
event_id: event_id.into(),
previous_item_id,
item,
}
}
pub fn conversation_item_deleted(
event_id: impl Into<String>,
item_id: impl Into<String>,
) -> Self {
Self::ConversationItemDeleted {
event_id: event_id.into(),
item_id: item_id.into(),
}
}
pub fn conversation_item_retrieved(
event_id: impl Into<String>,
item: RealtimeConversationItem,
) -> Self {
Self::ConversationItemRetrieved {
event_id: event_id.into(),
item,
}
}
pub fn conversation_item_truncated(
event_id: impl Into<String>,
item_id: impl Into<String>,
content_index: u32,
audio_end_ms: u32,
) -> Self {
Self::ConversationItemTruncated {
event_id: event_id.into(),
item_id: item_id.into(),
content_index,
audio_end_ms,
}
}
pub fn input_audio_transcription_completed(
event_id: impl Into<String>,
item_id: impl Into<String>,
content_index: u32,
transcript: impl Into<String>,
usage: TranscriptionUsage,
) -> Self {
Self::InputAudioTranscriptionCompleted {
event_id: event_id.into(),
item_id: item_id.into(),
content_index,
transcript: transcript.into(),
logprobs: None,
usage,
}
}
pub fn input_audio_transcription_completed_with_logprobs(
event_id: impl Into<String>,
item_id: impl Into<String>,
content_index: u32,
transcript: impl Into<String>,
usage: TranscriptionUsage,
logprobs: Vec<LogProbProperties>,
) -> Self {
Self::InputAudioTranscriptionCompleted {
event_id: event_id.into(),
item_id: item_id.into(),
content_index,
transcript: transcript.into(),
logprobs: Some(logprobs),
usage,
}
}
pub fn input_audio_transcription_delta(
event_id: impl Into<String>,
item_id: impl Into<String>,
content_index: Option<u32>,
delta: Option<String>,
logprobs: Option<Vec<LogProbProperties>>,
) -> Self {
Self::InputAudioTranscriptionDelta {
event_id: event_id.into(),
item_id: item_id.into(),
content_index,
delta,
logprobs,
}
}
pub fn input_audio_transcription_failed(
event_id: impl Into<String>,
item_id: impl Into<String>,
content_index: u32,
error: TranscriptionError,
) -> Self {
Self::InputAudioTranscriptionFailed {
event_id: event_id.into(),
item_id: item_id.into(),
content_index,
error,
}
}
#[expect(clippy::too_many_arguments)]
pub fn input_audio_transcription_segment(
event_id: impl Into<String>,
item_id: impl Into<String>,
content_index: u32,
text: impl Into<String>,
id: impl Into<String>,
speaker: impl Into<String>,
start: f32,
end: f32,
) -> Self {
Self::InputAudioTranscriptionSegment {
event_id: event_id.into(),
item_id: item_id.into(),
content_index,
text: text.into(),
id: id.into(),
speaker: speaker.into(),
start,
end,
}
}
pub fn input_audio_buffer_cleared(event_id: impl Into<String>) -> Self {
Self::InputAudioBufferCleared {
event_id: event_id.into(),
}
}
pub fn input_audio_buffer_committed(
event_id: impl Into<String>,
item_id: impl Into<String>,
previous_item_id: Option<String>,
) -> Self {
Self::InputAudioBufferCommitted {
event_id: event_id.into(),
previous_item_id,
item_id: item_id.into(),
}
}
pub fn input_audio_buffer_speech_started(
event_id: impl Into<String>,
audio_start_ms: u32,
item_id: impl Into<String>,
) -> Self {
Self::InputAudioBufferSpeechStarted {
event_id: event_id.into(),
audio_start_ms,
item_id: item_id.into(),
}
}
pub fn input_audio_buffer_speech_stopped(
event_id: impl Into<String>,
audio_end_ms: u32,
item_id: impl Into<String>,
) -> Self {
Self::InputAudioBufferSpeechStopped {
event_id: event_id.into(),
audio_end_ms,
item_id: item_id.into(),
}
}
pub fn input_audio_buffer_timeout_triggered(
event_id: impl Into<String>,
audio_start_ms: u32,
audio_end_ms: u32,
item_id: impl Into<String>,
) -> Self {
Self::InputAudioBufferTimeoutTriggered {
event_id: event_id.into(),
audio_start_ms,
audio_end_ms,
item_id: item_id.into(),
}
}
pub fn dtmf_event_received(event: impl Into<String>, received_at: i64) -> Self {
Self::InputAudioBufferDtmfEventReceived {
event: event.into(),
received_at,
}
}
pub fn mcp_list_tools_in_progress(
event_id: impl Into<String>,
item_id: impl Into<String>,
) -> Self {
Self::McpListToolsInProgress {
event_id: event_id.into(),
item_id: item_id.into(),
}
}
pub fn mcp_list_tools_completed(
event_id: impl Into<String>,
item_id: impl Into<String>,
) -> Self {
Self::McpListToolsCompleted {
event_id: event_id.into(),
item_id: item_id.into(),
}
}
pub fn mcp_list_tools_failed(event_id: impl Into<String>, item_id: impl Into<String>) -> Self {
Self::McpListToolsFailed {
event_id: event_id.into(),
item_id: item_id.into(),
}
}
pub fn mcp_call_in_progress(
event_id: impl Into<String>,
item_id: impl Into<String>,
output_index: u32,
) -> Self {
Self::ResponseMcpCallInProgress {
event_id: event_id.into(),
output_index,
item_id: item_id.into(),
}
}
pub fn mcp_call_completed(
event_id: impl Into<String>,
item_id: impl Into<String>,
output_index: u32,
) -> Self {
Self::ResponseMcpCallCompleted {
event_id: event_id.into(),
output_index,
item_id: item_id.into(),
}
}
pub fn mcp_call_failed(
event_id: impl Into<String>,
item_id: impl Into<String>,
output_index: u32,
) -> Self {
Self::ResponseMcpCallFailed {
event_id: event_id.into(),
output_index,
item_id: item_id.into(),
}
}
pub fn rate_limits_updated(
event_id: impl Into<String>,
rate_limits: Vec<RealtimeRateLimit>,
) -> Self {
Self::RateLimitsUpdated {
event_id: event_id.into(),
rate_limits,
}
}
pub fn response_created(event_id: impl Into<String>, response: RealtimeResponse) -> Self {
Self::ResponseCreated {
event_id: event_id.into(),
response: Box::new(response),
}
}
pub fn response_done(event_id: impl Into<String>, response: RealtimeResponse) -> Self {
Self::ResponseDone {
event_id: event_id.into(),
response: Box::new(response),
}
}
pub fn error_event(event_id: impl Into<String>, error: RealtimeError) -> Self {
Self::Error {
event_id: event_id.into(),
error,
}
}
}
#[derive(Clone, Debug)]
pub struct ResponseEventBuilder {
response_id: String,
}
impl ResponseEventBuilder {
pub fn new(response_id: impl Into<String>) -> Self {
Self {
response_id: response_id.into(),
}
}
pub fn for_item(&self, item_id: impl Into<String>, output_index: u32) -> ItemEventBuilder {
ItemEventBuilder {
response_id: self.response_id.clone(),
item_id: item_id.into(),
output_index,
}
}
pub fn output_audio_buffer_started(&self, event_id: impl Into<String>) -> ServerEvent {
ServerEvent::OutputAudioBufferStarted {
event_id: event_id.into(),
response_id: self.response_id.clone(),
}
}
pub fn output_audio_buffer_stopped(&self, event_id: impl Into<String>) -> ServerEvent {
ServerEvent::OutputAudioBufferStopped {
event_id: event_id.into(),
response_id: self.response_id.clone(),
}
}
pub fn output_audio_buffer_cleared(&self, event_id: impl Into<String>) -> ServerEvent {
ServerEvent::OutputAudioBufferCleared {
event_id: event_id.into(),
response_id: self.response_id.clone(),
}
}
}
#[derive(Clone, Debug)]
pub struct ItemEventBuilder {
response_id: String,
item_id: String,
output_index: u32,
}
impl ItemEventBuilder {
pub fn new(
response_id: impl Into<String>,
item_id: impl Into<String>,
output_index: u32,
) -> Self {
Self {
response_id: response_id.into(),
item_id: item_id.into(),
output_index,
}
}
pub fn for_content(&self, content_index: u32) -> ContentEventBuilder {
ContentEventBuilder {
response_id: self.response_id.clone(),
item_id: self.item_id.clone(),
output_index: self.output_index,
content_index,
}
}
pub fn output_item_added(
&self,
event_id: impl Into<String>,
item: RealtimeConversationItem,
) -> ServerEvent {
ServerEvent::ResponseOutputItemAdded {
event_id: event_id.into(),
response_id: self.response_id.clone(),
output_index: self.output_index,
item,
}
}
pub fn output_item_done(
&self,
event_id: impl Into<String>,
item: RealtimeConversationItem,
) -> ServerEvent {
ServerEvent::ResponseOutputItemDone {
event_id: event_id.into(),
response_id: self.response_id.clone(),
output_index: self.output_index,
item,
}
}
pub fn function_call_arguments_delta(
&self,
event_id: impl Into<String>,
call_id: impl Into<String>,
delta: impl Into<String>,
) -> ServerEvent {
ServerEvent::ResponseFunctionCallArgumentsDelta {
event_id: event_id.into(),
response_id: self.response_id.clone(),
item_id: self.item_id.clone(),
output_index: self.output_index,
call_id: call_id.into(),
delta: delta.into(),
}
}
pub fn function_call_arguments_done(
&self,
event_id: impl Into<String>,
call_id: impl Into<String>,
name: impl Into<String>,
arguments: impl Into<String>,
) -> ServerEvent {
ServerEvent::ResponseFunctionCallArgumentsDone {
event_id: event_id.into(),
response_id: self.response_id.clone(),
item_id: self.item_id.clone(),
output_index: self.output_index,
call_id: call_id.into(),
name: name.into(),
arguments: arguments.into(),
}
}
pub fn mcp_call_arguments_delta(
&self,
event_id: impl Into<String>,
delta: impl Into<String>,
obfuscation: Option<String>,
) -> ServerEvent {
ServerEvent::ResponseMcpCallArgumentsDelta {
event_id: event_id.into(),
response_id: self.response_id.clone(),
item_id: self.item_id.clone(),
output_index: self.output_index,
delta: delta.into(),
obfuscation,
}
}
pub fn mcp_call_arguments_done(
&self,
event_id: impl Into<String>,
arguments: impl Into<String>,
) -> ServerEvent {
ServerEvent::ResponseMcpCallArgumentsDone {
event_id: event_id.into(),
response_id: self.response_id.clone(),
item_id: self.item_id.clone(),
output_index: self.output_index,
arguments: arguments.into(),
}
}
}
#[derive(Clone, Debug)]
pub struct ContentEventBuilder {
response_id: String,
item_id: String,
output_index: u32,
content_index: u32,
}
impl ContentEventBuilder {
pub fn new(
response_id: impl Into<String>,
item_id: impl Into<String>,
output_index: u32,
content_index: u32,
) -> Self {
Self {
response_id: response_id.into(),
item_id: item_id.into(),
output_index,
content_index,
}
}
pub fn content_part_added(
&self,
event_id: impl Into<String>,
part: ResponseContentPart,
) -> ServerEvent {
ServerEvent::ResponseContentPartAdded {
event_id: event_id.into(),
response_id: self.response_id.clone(),
item_id: self.item_id.clone(),
output_index: self.output_index,
content_index: self.content_index,
part,
}
}
pub fn content_part_done(
&self,
event_id: impl Into<String>,
part: ResponseContentPart,
) -> ServerEvent {
ServerEvent::ResponseContentPartDone {
event_id: event_id.into(),
response_id: self.response_id.clone(),
item_id: self.item_id.clone(),
output_index: self.output_index,
content_index: self.content_index,
part,
}
}
pub fn output_text_delta(
&self,
event_id: impl Into<String>,
delta: impl Into<String>,
) -> ServerEvent {
ServerEvent::ResponseOutputTextDelta {
event_id: event_id.into(),
response_id: self.response_id.clone(),
item_id: self.item_id.clone(),
output_index: self.output_index,
content_index: self.content_index,
delta: delta.into(),
}
}
pub fn output_text_done(
&self,
event_id: impl Into<String>,
text: impl Into<String>,
) -> ServerEvent {
ServerEvent::ResponseOutputTextDone {
event_id: event_id.into(),
response_id: self.response_id.clone(),
item_id: self.item_id.clone(),
output_index: self.output_index,
content_index: self.content_index,
text: text.into(),
}
}
pub fn output_audio_delta(
&self,
event_id: impl Into<String>,
delta: impl Into<String>,
) -> ServerEvent {
ServerEvent::ResponseOutputAudioDelta {
event_id: event_id.into(),
response_id: self.response_id.clone(),
item_id: self.item_id.clone(),
output_index: self.output_index,
content_index: self.content_index,
delta: delta.into(),
}
}
pub fn output_audio_done(&self, event_id: impl Into<String>) -> ServerEvent {
ServerEvent::ResponseOutputAudioDone {
event_id: event_id.into(),
response_id: self.response_id.clone(),
item_id: self.item_id.clone(),
output_index: self.output_index,
content_index: self.content_index,
}
}
pub fn output_audio_transcript_delta(
&self,
event_id: impl Into<String>,
delta: impl Into<String>,
) -> ServerEvent {
ServerEvent::ResponseOutputAudioTranscriptDelta {
event_id: event_id.into(),
response_id: self.response_id.clone(),
item_id: self.item_id.clone(),
output_index: self.output_index,
content_index: self.content_index,
delta: delta.into(),
}
}
pub fn output_audio_transcript_done(
&self,
event_id: impl Into<String>,
transcript: impl Into<String>,
) -> ServerEvent {
ServerEvent::ResponseOutputAudioTranscriptDone {
event_id: event_id.into(),
response_id: self.response_id.clone(),
item_id: self.item_id.clone(),
output_index: self.output_index,
content_index: self.content_index,
transcript: transcript.into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::realtime_session::{
OutputModality, RealtimeSessionCreateRequest, RealtimeSessionType,
};
#[test]
fn test_session_created() {
let config = SessionConfig::Realtime(Box::new(RealtimeSessionCreateRequest {
r#type: RealtimeSessionType::Realtime,
output_modalities: Some(vec![OutputModality::Audio]),
model: None,
instructions: None,
audio: None,
include: None,
tracing: None,
tools: None,
tool_choice: None,
max_output_tokens: None,
truncation: None,
prompt: None,
}));
let event = ServerEvent::session_created("evt_1", config);
assert_eq!(event.event_type(), "session.created");
let json = serde_json::to_string(&event).expect("serialization failed");
assert!(json.contains("\"type\":\"session.created\""));
}
#[test]
fn test_full_hierarchy() {
let l2 = ResponseEventBuilder::new("resp_1");
let event = l2.output_audio_buffer_started("evt_1");
assert_eq!(event.event_type(), "output_audio_buffer.started");
if let ServerEvent::OutputAudioBufferStarted { response_id, .. } = &event {
assert_eq!(response_id, "resp_1");
} else {
panic!("Expected OutputAudioBufferStarted");
}
let l3 = l2.for_item("item_1", 0);
let item = RealtimeConversationItem::FunctionCallOutput {
call_id: "call_1".into(),
output: "result".into(),
id: None,
object: None,
status: None,
};
let event = l3.output_item_done("evt_2", item);
assert_eq!(event.event_type(), "response.output_item.done");
if let ServerEvent::ResponseOutputItemDone {
response_id,
output_index,
..
} = &event
{
assert_eq!(response_id, "resp_1");
assert_eq!(*output_index, 0);
} else {
panic!("Expected ResponseOutputItemDone");
}
let l4 = l3.for_content(0);
let event = l4.output_text_delta("evt_3", "Hello");
assert_eq!(event.event_type(), "response.output_text.delta");
if let ServerEvent::ResponseOutputTextDelta {
response_id,
item_id,
output_index,
content_index,
delta,
..
} = &event
{
assert_eq!(response_id, "resp_1");
assert_eq!(item_id, "item_1");
assert_eq!(*output_index, 0);
assert_eq!(*content_index, 0);
assert_eq!(delta, "Hello");
} else {
panic!("Expected ResponseOutputTextDelta");
}
}
#[test]
fn test_streaming_reuse() {
let ctx = ResponseEventBuilder::new("resp_1")
.for_item("item_1", 0)
.for_content(0);
let chunks = ["Hello", " ", "world"];
let events: Vec<_> = chunks
.iter()
.enumerate()
.map(|(i, chunk)| ctx.output_text_delta(format!("evt_{i}"), *chunk))
.collect();
assert_eq!(events.len(), 3);
for (i, event) in events.iter().enumerate() {
assert_eq!(event.event_type(), "response.output_text.delta");
if let ServerEvent::ResponseOutputTextDelta {
event_id, delta, ..
} = event
{
assert_eq!(event_id, &format!("evt_{i}"));
assert_eq!(delta, chunks[i]);
}
}
}
}