use std::future::Future;
use bytes::Bytes;
use serde::Deserialize;
use serde::Serialize;
use url::Url;
use crate::error::NoSuchModelError;
use crate::error::ProviderError;
use crate::json::JsonObject;
use crate::json::JsonValue;
use crate::shared::AudioFormat;
use crate::shared::ModelId;
use crate::shared::ProviderId;
use crate::shared::base64_bytes;
pub trait RealtimeModel: Send + Sync + 'static {
fn provider(&self) -> &ProviderId;
fn model_id(&self) -> &ModelId;
fn do_create_client_secret(
&self,
options: ClientSecretOptions,
) -> impl Future<Output = Result<ClientSecret, ProviderError>> + Send;
fn websocket_config(&self, token: &str, url: &Url) -> WebSocketConfig;
fn parse_server_event(&self, raw: JsonValue)
-> Result<Vec<RealtimeServerEvent>, ProviderError>;
fn serialize_client_event(
&self,
event: RealtimeClientEvent,
) -> impl Future<Output = Result<JsonValue, ProviderError>> + Send;
fn build_session_config(
&self,
config: &RealtimeSessionConfig,
) -> Result<JsonValue, ProviderError>;
fn health_check_response(&self, raw: &JsonValue) -> Option<JsonValue> {
let _ = raw;
None
}
}
pub trait RealtimeFactory: Send + Sync + 'static {
fn provider(&self) -> &ProviderId;
fn model(&self, model_id: &str) -> Result<crate::dynamic::RealtimeModelRef, NoSuchModelError>;
fn get_token(
&self,
options: GetTokenOptions,
) -> impl Future<Output = Result<ClientSecret, ProviderError>> + Send;
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ClientSecretOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_after_seconds: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_config: Option<RealtimeSessionConfig>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GetTokenOptions {
pub model: ModelId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_after_seconds: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_config: Option<RealtimeSessionConfig>,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClientSecret {
pub token: String,
pub url: Url,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<u64>,
}
impl std::fmt::Debug for ClientSecret {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClientSecret")
.field("token", &"***")
.field("url", &self.url)
.field("expires_at", &self.expires_at)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WebSocketConfig {
pub url: Url,
pub protocols: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum Modality {
Text,
Audio,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TranscriptionConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prompt: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum TurnDetectionKind {
ServerVad,
SemanticVad,
Disabled,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TurnDetection {
#[serde(rename = "type")]
pub kind: TurnDetectionKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub threshold: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub silence_duration_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prefix_padding_ms: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RealtimeToolDefinition {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub parameters: JsonValue,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct RealtimeSessionConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub voice: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_modalities: Option<Vec<Modality>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_audio_format: Option<AudioFormat>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub input_audio_transcription: Option<TranscriptionConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_audio_transcription: Option<TranscriptionConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_audio_format: Option<AudioFormat>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turn_detection: Option<TurnDetection>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tools: Vec<RealtimeToolDefinition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_options: Option<JsonObject>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum ConversationRole {
User,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
#[non_exhaustive]
pub enum ConversationItem {
TextMessage {
role: ConversationRole,
text: String,
},
AudioMessage {
role: ConversationRole,
#[serde(with = "base64_bytes")]
audio: Bytes,
},
FunctionCallOutput {
call_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
name: Option<String>,
output: String,
},
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ResponseCreateOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub modalities: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub instructions: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<JsonObject>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
#[non_exhaustive]
pub enum RealtimeClientEvent {
SessionUpdate {
config: Box<RealtimeSessionConfig>,
},
InputAudioAppend {
#[serde(with = "base64_bytes")]
audio: Bytes,
},
InputAudioCommit,
InputAudioClear,
ConversationItemCreate {
item: ConversationItem,
},
ConversationItemTruncate {
item_id: String,
content_index: u32,
audio_end_ms: u64,
},
ResponseCreate {
#[serde(default, skip_serializing_if = "Option::is_none")]
options: Option<ResponseCreateOptions>,
},
ResponseCancel,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
#[non_exhaustive]
pub enum RealtimeServerEvent {
SessionCreated {
#[serde(default, skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
raw: JsonValue,
},
SessionUpdated {
raw: JsonValue,
},
SpeechStarted {
#[serde(default, skip_serializing_if = "Option::is_none")]
item_id: Option<String>,
raw: JsonValue,
},
SpeechStopped {
#[serde(default, skip_serializing_if = "Option::is_none")]
item_id: Option<String>,
raw: JsonValue,
},
AudioCommitted {
#[serde(default, skip_serializing_if = "Option::is_none")]
item_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
previous_item_id: Option<String>,
raw: JsonValue,
},
ConversationItemAdded {
item_id: String,
item: JsonValue,
raw: JsonValue,
},
InputTranscriptionCompleted {
item_id: String,
transcript: String,
raw: JsonValue,
},
ResponseCreated {
response_id: String,
raw: JsonValue,
},
ResponseDone {
response_id: String,
status: String,
raw: JsonValue,
},
OutputItemAdded {
response_id: String,
item_id: String,
raw: JsonValue,
},
OutputItemDone {
response_id: String,
item_id: String,
raw: JsonValue,
},
ContentPartAdded {
response_id: String,
item_id: String,
raw: JsonValue,
},
ContentPartDone {
response_id: String,
item_id: String,
raw: JsonValue,
},
AudioDelta {
response_id: String,
item_id: String,
#[serde(with = "base64_bytes")]
delta: Bytes,
raw: JsonValue,
},
AudioDone {
response_id: String,
item_id: String,
raw: JsonValue,
},
AudioTranscriptDelta {
response_id: String,
item_id: String,
delta: String,
raw: JsonValue,
},
AudioTranscriptDone {
response_id: String,
item_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
transcript: Option<String>,
raw: JsonValue,
},
TextDelta {
response_id: String,
item_id: String,
delta: String,
raw: JsonValue,
},
TextDone {
response_id: String,
item_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
text: Option<String>,
raw: JsonValue,
},
FunctionCallArgumentsDelta {
response_id: String,
item_id: String,
call_id: String,
delta: String,
raw: JsonValue,
},
FunctionCallArgumentsDone {
response_id: String,
item_id: String,
call_id: String,
name: String,
arguments: String,
raw: JsonValue,
},
Error {
message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
code: Option<String>,
raw: JsonValue,
},
Custom {
raw_type: String,
raw: JsonValue,
},
}