pub use async_openai::types::realtime::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct RealtimeClientEventInputTextAppend {
#[serde(skip_serializing_if = "Option::is_none")]
pub event_id: Option<String>,
pub text: String,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct RealtimeClientEventInputTextCommit {
#[serde(skip_serializing_if = "Option::is_none")]
pub event_id: Option<String>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct RealtimeClientEventInputTextClear {
#[serde(skip_serializing_if = "Option::is_none")]
pub event_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum RealtimeClientEventExtension {
#[serde(rename = "input_text.append")]
InputTextAppend(RealtimeClientEventInputTextAppend),
#[serde(rename = "input_text.commit")]
InputTextCommit(RealtimeClientEventInputTextCommit),
#[serde(rename = "input_text.clear")]
InputTextClear(RealtimeClientEventInputTextClear),
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum DynamoRealtimeClientEvent {
OpenAI(RealtimeClientEvent),
Extension(RealtimeClientEventExtension),
}
impl From<RealtimeClientEvent> for DynamoRealtimeClientEvent {
fn from(event: RealtimeClientEvent) -> Self {
Self::OpenAI(event)
}
}
pub trait EventType {
fn event_type(&self) -> &'static str;
}
impl EventType for RealtimeClientEvent {
fn event_type(&self) -> &'static str {
match self {
RealtimeClientEvent::SessionUpdate(_) => "session.update",
RealtimeClientEvent::InputAudioBufferAppend(_) => "input_audio_buffer.append",
RealtimeClientEvent::InputAudioBufferCommit(_) => "input_audio_buffer.commit",
RealtimeClientEvent::InputAudioBufferClear(_) => "input_audio_buffer.clear",
RealtimeClientEvent::ConversationItemCreate(_) => "conversation.item.create",
RealtimeClientEvent::ConversationItemRetrieve(_) => "conversation.item.retrieve",
RealtimeClientEvent::ConversationItemTruncate(_) => "conversation.item.truncate",
RealtimeClientEvent::ConversationItemDelete(_) => "conversation.item.delete",
RealtimeClientEvent::ResponseCreate(_) => "response.create",
RealtimeClientEvent::ResponseCancel(_) => "response.cancel",
RealtimeClientEvent::OutputAudioBufferClear(_) => "output_audio_buffer.clear",
}
}
}
impl EventType for RealtimeClientEventExtension {
fn event_type(&self) -> &'static str {
match self {
RealtimeClientEventExtension::InputTextAppend(_) => "input_text.append",
RealtimeClientEventExtension::InputTextCommit(_) => "input_text.commit",
RealtimeClientEventExtension::InputTextClear(_) => "input_text.clear",
}
}
}
impl EventType for DynamoRealtimeClientEvent {
fn event_type(&self) -> &'static str {
match self {
DynamoRealtimeClientEvent::OpenAI(event) => event.event_type(),
DynamoRealtimeClientEvent::Extension(event) => event.event_type(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn session_update_accepts_null_turn_detection() {
let event: RealtimeClientEvent = serde_json::from_value(serde_json::json!({
"type": "session.update",
"session": {
"type": "transcription",
"audio": {
"input": {
"format": { "type": "audio/pcm", "rate": 24000 },
"transcription": { "model": "whisper-1" },
"turn_detection": null
}
}
}
}))
.expect("null turn_detection should be accepted");
let RealtimeClientEvent::SessionUpdate(update) = event else {
panic!("expected session.update");
};
let Session::RealtimeTranscriptionSession(session) = update.session else {
panic!("expected transcription session");
};
assert!(session.audio.input.turn_detection.is_none());
}
#[test]
fn text_events_round_trip() {
let values = [
(
serde_json::json!({
"type": "input_text.append",
"event_id": "append-1",
"text": "hello "
}),
"input_text.append",
),
(
serde_json::json!({
"type": "input_text.commit",
"event_id": "commit-1"
}),
"input_text.commit",
),
(
serde_json::json!({
"type": "input_text.clear",
"event_id": "clear-1"
}),
"input_text.clear",
),
];
for (value, event_type) in values {
let event: DynamoRealtimeClientEvent =
serde_json::from_value(value.clone()).expect("event should deserialize");
assert_eq!(event.event_type(), event_type);
assert_eq!(serde_json::to_value(event).unwrap(), value);
}
}
#[test]
fn text_append_requires_non_null_text() {
for value in [
serde_json::json!({"type": "input_text.append"}),
serde_json::json!({"type": "input_text.append", "text": null}),
] {
assert!(serde_json::from_value::<DynamoRealtimeClientEvent>(value).is_err());
}
}
}