use serde::{Deserialize, Serialize};
pub mod openai;
pub use openai::OpenAiRealtimeTranslator;
use crate::error::Result;
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ContentPart {
Text {
text: String,
},
Audio {
base64: String,
},
ImageRef {
url: String,
},
}
impl ContentPart {
pub fn text(content: impl Into<String>) -> Self {
Self::Text { text: content.into() }
}
pub fn audio(base64: impl Into<String>) -> Self {
Self::Audio { base64: base64.into() }
}
pub fn image_ref(url: impl Into<String>) -> Self {
Self::ImageRef { url: url.into() }
}
}
#[cfg_attr(alef, alef(skip))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResponseStatus {
Completed,
Cancelled,
Failed,
Incomplete,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RealtimeEvent {
SessionCreated {
session_id: String,
model: String,
},
SessionUpdated {
session_id: String,
instructions: Option<String>,
},
ConversationItemCreated {
item_id: String,
role: String,
content: Vec<ContentPart>,
},
ConversationItemDeleted {
item_id: String,
},
ResponseCreated {
response_id: String,
},
ResponseDone {
response_id: String,
status: ResponseStatus,
},
ResponseTextDelta {
response_id: String,
delta: String,
},
ResponseTextDone {
response_id: String,
text: String,
},
ResponseAudioDelta {
response_id: String,
delta_base64: String,
},
ResponseAudioDone {
response_id: String,
},
ResponseAudioTranscriptDelta {
response_id: String,
delta: String,
},
ResponseAudioTranscriptDone {
response_id: String,
transcript: String,
},
ResponseFunctionCallArgumentsDelta {
response_id: String,
call_id: String,
delta: String,
},
ResponseFunctionCallArgumentsDone {
response_id: String,
call_id: String,
name: String,
arguments: String,
},
InputAudioBufferAppend {
audio_base64: String,
},
InputAudioBufferCommit,
InputAudioBufferClear,
InputAudioBufferSpeechStarted {
item_id: String,
},
InputAudioBufferSpeechStopped {
item_id: String,
audio_end_ms: u32,
},
RateLimitsUpdated {
remaining_requests: Option<u32>,
remaining_tokens: Option<u32>,
reset_at_unix_ms: i64,
},
Error {
code: String,
message: String,
event_id: Option<String>,
},
Raw {
event_type: String,
payload: serde_json::Value,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RealtimeEnvelope {
pub event_id: Option<String>,
pub event: RealtimeEvent,
}
impl RealtimeEnvelope {
pub fn new(event: RealtimeEvent) -> Self {
Self { event_id: None, event }
}
pub fn with_id(event_id: impl Into<String>, event: RealtimeEvent) -> Self {
Self {
event_id: Some(event_id.into()),
event,
}
}
}
#[cfg_attr(alef, alef(skip))]
pub trait RealtimeTranslator: Send + Sync + 'static {
fn translate_inbound(&self, raw: serde_json::Value) -> Result<RealtimeEvent>;
fn translate_outbound(&self, event: &RealtimeEvent) -> Result<serde_json::Value>;
fn provider(&self) -> &'static str;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn content_part_text_serialises_and_deserialises() {
let part = ContentPart::text("hello");
let json = serde_json::to_string(&part).unwrap();
let back: ContentPart = serde_json::from_str(&json).unwrap();
assert_eq!(back, part);
}
#[test]
fn response_status_all_variants_round_trip() {
for status in [
ResponseStatus::Completed,
ResponseStatus::Cancelled,
ResponseStatus::Failed,
ResponseStatus::Incomplete,
] {
let json = serde_json::to_string(&status).unwrap();
let back: ResponseStatus = serde_json::from_str(&json).unwrap();
assert_eq!(back, status);
}
}
#[test]
fn realtime_envelope_with_id_sets_event_id() {
let env = RealtimeEnvelope::with_id("evt_1", RealtimeEvent::InputAudioBufferCommit);
assert_eq!(env.event_id.as_deref(), Some("evt_1"));
}
#[test]
fn realtime_envelope_new_has_no_event_id() {
let env = RealtimeEnvelope::new(RealtimeEvent::InputAudioBufferCommit);
assert!(env.event_id.is_none());
}
#[test]
fn realtime_event_raw_round_trips() {
let payload = serde_json::json!({"foo": "bar"});
let event = RealtimeEvent::Raw {
event_type: "some.new.event".into(),
payload: payload.clone(),
};
let json = serde_json::to_string(&event).unwrap();
let back: RealtimeEvent = serde_json::from_str(&json).unwrap();
assert_eq!(back, event);
}
}