use crate::error::{HeaderMap, LingerError};
use crate::transport::HttpRequest;
use crate::RequestId;
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
use std::fmt;
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct CreateRealtimeCallRequest {
pub sdp: String,
pub body_format: RealtimeCallBodyFormat,
pub session: Option<RealtimeSessionConfig>,
}
impl CreateRealtimeCallRequest {
pub fn builder() -> CreateRealtimeCallRequestBuilder {
CreateRealtimeCallRequestBuilder::default()
}
pub(crate) fn apply_body(&self, request: &mut HttpRequest) -> Result<(), LingerError> {
match self.body_format {
RealtimeCallBodyFormat::Sdp => {
request.insert_header("content-type", "application/sdp");
request.set_body(Bytes::from(self.sdp.clone()));
}
RealtimeCallBodyFormat::Multipart => {
let session = self
.session
.as_ref()
.map(serde_json::to_string)
.transpose()?;
let boundary = realtime_multipart_boundary(&self.sdp, session.as_deref());
request.insert_header(
"content-type",
format!("multipart/form-data; boundary={boundary}"),
);
request.set_body(self.multipart_body(&boundary, session.as_deref()));
}
}
Ok(())
}
fn multipart_body(&self, boundary: &str, session: Option<&str>) -> Bytes {
let mut body = Vec::new();
push_typed_multipart_field(
&mut body,
boundary,
"sdp",
"application/sdp",
self.sdp.as_bytes(),
);
if let Some(session) = session {
push_typed_multipart_field(
&mut body,
boundary,
"session",
"application/json",
session.as_bytes(),
);
}
body.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
Bytes::from(body)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum RealtimeCallBodyFormat {
Sdp,
Multipart,
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateRealtimeCallRequestBuilder {
sdp: Option<String>,
body_format: Option<RealtimeCallBodyFormat>,
session: Option<RealtimeSessionConfig>,
}
impl CreateRealtimeCallRequestBuilder {
pub fn sdp(mut self, sdp: impl Into<String>) -> Self {
self.sdp = Some(sdp.into());
self
}
pub fn body_format(mut self, body_format: RealtimeCallBodyFormat) -> Self {
self.body_format = Some(body_format);
self
}
pub fn session(mut self, session: RealtimeSessionConfig) -> Self {
self.session = Some(session);
self
}
pub fn build(self) -> Result<CreateRealtimeCallRequest, LingerError> {
let body_format = self
.body_format
.unwrap_or(RealtimeCallBodyFormat::Multipart);
if body_format == RealtimeCallBodyFormat::Sdp && self.session.is_some() {
return Err(LingerError::invalid_config(
"session requires multipart body format",
));
}
Ok(CreateRealtimeCallRequest {
sdp: required_string("sdp", self.sdp)?,
body_format,
session: self.session,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct RealtimeCallSdpAnswer {
pub sdp: String,
pub location: Option<String>,
request_id: Option<RequestId>,
}
impl RealtimeCallSdpAnswer {
pub(crate) fn from_parts(
headers: &HeaderMap,
request_id: Option<RequestId>,
body: Bytes,
) -> Result<Self, LingerError> {
let sdp = String::from_utf8(body.to_vec())
.map_err(|error| LingerError::serialization(error.to_string()))?;
Ok(Self {
sdp,
location: headers.get("location").map(str::to_owned),
request_id,
})
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct CreateRealtimeCallReferRequest {
pub target_uri: String,
}
impl CreateRealtimeCallReferRequest {
pub fn builder() -> CreateRealtimeCallReferRequestBuilder {
CreateRealtimeCallReferRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateRealtimeCallReferRequestBuilder {
target_uri: Option<String>,
}
impl CreateRealtimeCallReferRequestBuilder {
pub fn target_uri(mut self, target_uri: impl Into<String>) -> Self {
self.target_uri = Some(target_uri.into());
self
}
pub fn build(self) -> Result<CreateRealtimeCallReferRequest, LingerError> {
Ok(CreateRealtimeCallReferRequest {
target_uri: required_string("target_uri", self.target_uri)?,
})
}
}
#[derive(Clone, Debug, Default, Serialize, PartialEq, Eq)]
#[non_exhaustive]
pub struct RejectRealtimeCallRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub status_code: Option<u16>,
}
impl RejectRealtimeCallRequest {
pub fn builder() -> RejectRealtimeCallRequestBuilder {
RejectRealtimeCallRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct RejectRealtimeCallRequestBuilder {
status_code: Option<u16>,
}
impl RejectRealtimeCallRequestBuilder {
pub fn status_code(mut self, status_code: u16) -> Self {
self.status_code = Some(status_code);
self
}
pub fn build(self) -> Result<RejectRealtimeCallRequest, LingerError> {
Ok(RejectRealtimeCallRequest {
status_code: self.status_code,
})
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct CreateRealtimeSessionRequest {
pub model: String,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl CreateRealtimeSessionRequest {
pub fn builder() -> CreateRealtimeSessionRequestBuilder {
CreateRealtimeSessionRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateRealtimeSessionRequestBuilder {
model: Option<String>,
extra: BTreeMap<String, Value>,
}
impl CreateRealtimeSessionRequestBuilder {
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
self.extra.insert(name.into(), value);
self
}
pub fn build(self) -> Result<CreateRealtimeSessionRequest, LingerError> {
validate_extra_fields(&self.extra)?;
Ok(CreateRealtimeSessionRequest {
model: required_string("model", self.model)?,
extra: self.extra,
})
}
}
#[derive(Clone, Debug, Default, Serialize, PartialEq)]
#[non_exhaustive]
pub struct CreateRealtimeTranscriptionSessionRequest {
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl CreateRealtimeTranscriptionSessionRequest {
pub fn builder() -> CreateRealtimeTranscriptionSessionRequestBuilder {
CreateRealtimeTranscriptionSessionRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateRealtimeTranscriptionSessionRequestBuilder {
extra: BTreeMap<String, Value>,
}
impl CreateRealtimeTranscriptionSessionRequestBuilder {
pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
self.extra.insert(name.into(), value);
self
}
pub fn build(self) -> Result<CreateRealtimeTranscriptionSessionRequest, LingerError> {
validate_extra_fields(&self.extra)?;
Ok(CreateRealtimeTranscriptionSessionRequest { extra: self.extra })
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct CreateRealtimeTranslationSessionRequest {
pub model: String,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl CreateRealtimeTranslationSessionRequest {
pub fn builder() -> CreateRealtimeTranslationSessionRequestBuilder {
CreateRealtimeTranslationSessionRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateRealtimeTranslationSessionRequestBuilder {
model: Option<String>,
extra: BTreeMap<String, Value>,
}
impl CreateRealtimeTranslationSessionRequestBuilder {
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
self.extra.insert(name.into(), value);
self
}
pub fn build(self) -> Result<CreateRealtimeTranslationSessionRequest, LingerError> {
validate_extra_fields(&self.extra)?;
Ok(CreateRealtimeTranslationSessionRequest {
model: required_string("model", self.model)?,
extra: self.extra,
})
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct CreateRealtimeTranslationClientSecretRequest {
pub session: CreateRealtimeTranslationSessionRequest,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl CreateRealtimeTranslationClientSecretRequest {
pub fn builder() -> CreateRealtimeTranslationClientSecretRequestBuilder {
CreateRealtimeTranslationClientSecretRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateRealtimeTranslationClientSecretRequestBuilder {
session: Option<CreateRealtimeTranslationSessionRequest>,
extra: BTreeMap<String, Value>,
}
impl CreateRealtimeTranslationClientSecretRequestBuilder {
pub fn session(mut self, session: CreateRealtimeTranslationSessionRequest) -> Self {
self.session = Some(session);
self
}
pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
self.extra.insert(name.into(), value);
self
}
pub fn build(self) -> Result<CreateRealtimeTranslationClientSecretRequest, LingerError> {
validate_extra_fields(&self.extra)?;
Ok(CreateRealtimeTranslationClientSecretRequest {
session: self
.session
.ok_or_else(|| LingerError::invalid_config("session is required"))?,
extra: self.extra,
})
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct CreateRealtimeClientSecretRequest {
pub session: RealtimeSessionConfig,
}
impl CreateRealtimeClientSecretRequest {
pub fn builder() -> CreateRealtimeClientSecretRequestBuilder {
CreateRealtimeClientSecretRequestBuilder::default()
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct CreateRealtimeClientSecretRequestBuilder {
session: Option<RealtimeSessionConfig>,
}
impl CreateRealtimeClientSecretRequestBuilder {
pub fn session(mut self, session: RealtimeSessionConfig) -> Self {
self.session = Some(session);
self
}
pub fn build(self) -> Result<CreateRealtimeClientSecretRequest, LingerError> {
Ok(CreateRealtimeClientSecretRequest {
session: self
.session
.ok_or_else(|| LingerError::invalid_config("session is required"))?,
})
}
}
#[derive(Clone, Debug, Serialize, PartialEq)]
#[non_exhaustive]
pub struct RealtimeSessionConfig {
#[serde(rename = "type")]
pub kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
}
impl RealtimeSessionConfig {
pub fn builder(kind: impl Into<String>) -> RealtimeSessionConfigBuilder {
RealtimeSessionConfigBuilder {
kind: Some(kind.into()),
model: None,
extra: BTreeMap::new(),
}
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct RealtimeSessionConfigBuilder {
kind: Option<String>,
model: Option<String>,
extra: BTreeMap<String, Value>,
}
impl RealtimeSessionConfigBuilder {
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
self.extra.insert(name.into(), value);
self
}
pub fn build(self) -> Result<RealtimeSessionConfig, LingerError> {
validate_optional_string("model", self.model.as_deref())?;
validate_extra_fields(&self.extra)?;
Ok(RealtimeSessionConfig {
kind: required_string("type", self.kind)?,
model: self.model,
extra: self.extra,
})
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct RealtimeClientSecret {
pub value: RealtimeClientSecretValue,
#[serde(default)]
pub expires_at: Option<u64>,
#[serde(default)]
pub session: Value,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl RealtimeClientSecret {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct RealtimeSession {
pub id: String,
pub object: String,
pub model: String,
pub client_secret: RealtimeClientSecret,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl RealtimeSession {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct RealtimeTranscriptionSession {
#[serde(default)]
pub id: Option<String>,
#[serde(default)]
pub object: Option<String>,
#[serde(default)]
pub client_secret: Option<RealtimeClientSecret>,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl RealtimeTranscriptionSession {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
#[non_exhaustive]
pub struct RealtimeTranslationClientSecret {
pub value: RealtimeClientSecretValue,
pub expires_at: u64,
pub session: Value,
#[serde(flatten)]
pub extra: BTreeMap<String, Value>,
#[serde(skip)]
request_id: Option<RequestId>,
}
impl RealtimeTranslationClientSecret {
pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
self.request_id = request_id;
self
}
pub fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
}
#[derive(Clone, Deserialize, Serialize, PartialEq, Eq)]
#[serde(transparent)]
#[non_exhaustive]
pub struct RealtimeClientSecretValue(String);
impl RealtimeClientSecretValue {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Debug for RealtimeClientSecretValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("\"<redacted>\"")
}
}
fn required_string(name: &str, value: Option<String>) -> Result<String, LingerError> {
value
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| LingerError::invalid_config(format!("{name} is required")))
}
fn validate_optional_string(name: &str, value: Option<&str>) -> Result<(), LingerError> {
if value.is_some_and(|value| value.trim().is_empty()) {
return Err(LingerError::invalid_config(format!(
"{name} must not be empty"
)));
}
Ok(())
}
fn validate_extra_fields(extra: &BTreeMap<String, Value>) -> Result<(), LingerError> {
for (key, value) in extra {
if key.trim().is_empty() {
return Err(LingerError::invalid_config(
"extra field names must not be empty",
));
}
if value.is_null() {
return Err(LingerError::invalid_config(format!(
"extra field {key} must not be null"
)));
}
}
Ok(())
}
fn push_typed_multipart_field(
body: &mut Vec<u8>,
boundary: &str,
name: &str,
content_type: &str,
value: &[u8],
) {
body.extend_from_slice(
format!(
"--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"\r\nContent-Type: {content_type}\r\n\r\n"
)
.as_bytes(),
);
body.extend_from_slice(value);
body.extend_from_slice(b"\r\n");
}
fn realtime_multipart_boundary(sdp: &str, session: Option<&str>) -> String {
for counter in 0.. {
let boundary = format!("linger-openai-sdk-realtime-boundary-{counter}");
let boundary_bytes = boundary.as_bytes();
let conflicts_with_sdp = contains_bytes(sdp.as_bytes(), boundary_bytes);
let conflicts_with_session =
session.is_some_and(|session| contains_bytes(session.as_bytes(), boundary_bytes));
if !conflicts_with_sdp && !conflicts_with_session {
return boundary;
}
}
unreachable!("unbounded boundary counter")
}
fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() {
return true;
}
haystack
.windows(needle.len())
.any(|window| window == needle)
}