1use crate::error::{HeaderMap, LingerError};
2use crate::transport::HttpRequest;
3use crate::RequestId;
4use bytes::Bytes;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::BTreeMap;
8use std::fmt;
9
10#[derive(Clone, Debug, PartialEq)]
13#[non_exhaustive]
14pub struct CreateRealtimeCallRequest {
15 pub sdp: String,
18 pub body_format: RealtimeCallBodyFormat,
21 pub session: Option<RealtimeSessionConfig>,
24}
25
26impl CreateRealtimeCallRequest {
27 pub fn builder() -> CreateRealtimeCallRequestBuilder {
30 CreateRealtimeCallRequestBuilder::default()
31 }
32
33 pub(crate) fn apply_body(&self, request: &mut HttpRequest) -> Result<(), LingerError> {
34 match self.body_format {
35 RealtimeCallBodyFormat::Sdp => {
36 request.insert_header("content-type", "application/sdp");
37 request.set_body(Bytes::from(self.sdp.clone()));
38 }
39 RealtimeCallBodyFormat::Multipart => {
40 let session = self
41 .session
42 .as_ref()
43 .map(serde_json::to_string)
44 .transpose()?;
45 let boundary = realtime_multipart_boundary(&self.sdp, session.as_deref());
46 request.insert_header(
47 "content-type",
48 format!("multipart/form-data; boundary={boundary}"),
49 );
50 request.set_body(self.multipart_body(&boundary, session.as_deref()));
51 }
52 }
53 Ok(())
54 }
55
56 fn multipart_body(&self, boundary: &str, session: Option<&str>) -> Bytes {
57 let mut body = Vec::new();
58 push_typed_multipart_field(
59 &mut body,
60 boundary,
61 "sdp",
62 "application/sdp",
63 self.sdp.as_bytes(),
64 );
65 if let Some(session) = session {
66 push_typed_multipart_field(
67 &mut body,
68 boundary,
69 "session",
70 "application/json",
71 session.as_bytes(),
72 );
73 }
74 body.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
75 Bytes::from(body)
76 }
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
82#[non_exhaustive]
83pub enum RealtimeCallBodyFormat {
84 Sdp,
87 Multipart,
90}
91
92#[derive(Clone, Debug, Default)]
95#[non_exhaustive]
96pub struct CreateRealtimeCallRequestBuilder {
97 sdp: Option<String>,
98 body_format: Option<RealtimeCallBodyFormat>,
99 session: Option<RealtimeSessionConfig>,
100}
101
102impl CreateRealtimeCallRequestBuilder {
103 pub fn sdp(mut self, sdp: impl Into<String>) -> Self {
106 self.sdp = Some(sdp.into());
107 self
108 }
109
110 pub fn body_format(mut self, body_format: RealtimeCallBodyFormat) -> Self {
113 self.body_format = Some(body_format);
114 self
115 }
116
117 pub fn session(mut self, session: RealtimeSessionConfig) -> Self {
120 self.session = Some(session);
121 self
122 }
123
124 pub fn build(self) -> Result<CreateRealtimeCallRequest, LingerError> {
127 let body_format = self
128 .body_format
129 .unwrap_or(RealtimeCallBodyFormat::Multipart);
130 if body_format == RealtimeCallBodyFormat::Sdp && self.session.is_some() {
131 return Err(LingerError::invalid_config(
132 "session requires multipart body format",
133 ));
134 }
135 Ok(CreateRealtimeCallRequest {
136 sdp: required_string("sdp", self.sdp)?,
137 body_format,
138 session: self.session,
139 })
140 }
141}
142
143#[derive(Clone, Debug, PartialEq, Eq)]
146#[non_exhaustive]
147pub struct RealtimeCallSdpAnswer {
148 pub sdp: String,
151 pub location: Option<String>,
154 request_id: Option<RequestId>,
157}
158
159impl RealtimeCallSdpAnswer {
160 pub(crate) fn from_parts(
161 headers: &HeaderMap,
162 request_id: Option<RequestId>,
163 body: Bytes,
164 ) -> Result<Self, LingerError> {
165 let sdp = String::from_utf8(body.to_vec())
166 .map_err(|error| LingerError::serialization(error.to_string()))?;
167 Ok(Self {
168 sdp,
169 location: headers.get("location").map(str::to_owned),
170 request_id,
171 })
172 }
173
174 pub fn request_id(&self) -> Option<&RequestId> {
177 self.request_id.as_ref()
178 }
179}
180
181#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
184#[non_exhaustive]
185pub struct CreateRealtimeCallReferRequest {
186 pub target_uri: String,
189}
190
191impl CreateRealtimeCallReferRequest {
192 pub fn builder() -> CreateRealtimeCallReferRequestBuilder {
195 CreateRealtimeCallReferRequestBuilder::default()
196 }
197}
198
199#[derive(Clone, Debug, Default)]
202#[non_exhaustive]
203pub struct CreateRealtimeCallReferRequestBuilder {
204 target_uri: Option<String>,
205}
206
207impl CreateRealtimeCallReferRequestBuilder {
208 pub fn target_uri(mut self, target_uri: impl Into<String>) -> Self {
211 self.target_uri = Some(target_uri.into());
212 self
213 }
214
215 pub fn build(self) -> Result<CreateRealtimeCallReferRequest, LingerError> {
218 Ok(CreateRealtimeCallReferRequest {
219 target_uri: required_string("target_uri", self.target_uri)?,
220 })
221 }
222}
223
224#[derive(Clone, Debug, Default, Serialize, PartialEq, Eq)]
227#[non_exhaustive]
228pub struct RejectRealtimeCallRequest {
229 #[serde(skip_serializing_if = "Option::is_none")]
232 pub status_code: Option<u16>,
233}
234
235impl RejectRealtimeCallRequest {
236 pub fn builder() -> RejectRealtimeCallRequestBuilder {
239 RejectRealtimeCallRequestBuilder::default()
240 }
241}
242
243#[derive(Clone, Debug, Default)]
246#[non_exhaustive]
247pub struct RejectRealtimeCallRequestBuilder {
248 status_code: Option<u16>,
249}
250
251impl RejectRealtimeCallRequestBuilder {
252 pub fn status_code(mut self, status_code: u16) -> Self {
255 self.status_code = Some(status_code);
256 self
257 }
258
259 pub fn build(self) -> Result<RejectRealtimeCallRequest, LingerError> {
262 Ok(RejectRealtimeCallRequest {
263 status_code: self.status_code,
264 })
265 }
266}
267
268#[derive(Clone, Debug, Serialize, PartialEq)]
271#[non_exhaustive]
272pub struct CreateRealtimeSessionRequest {
273 pub model: String,
276 #[serde(flatten)]
279 pub extra: BTreeMap<String, Value>,
280}
281
282impl CreateRealtimeSessionRequest {
283 pub fn builder() -> CreateRealtimeSessionRequestBuilder {
286 CreateRealtimeSessionRequestBuilder::default()
287 }
288}
289
290#[derive(Clone, Debug, Default)]
293#[non_exhaustive]
294pub struct CreateRealtimeSessionRequestBuilder {
295 model: Option<String>,
296 extra: BTreeMap<String, Value>,
297}
298
299impl CreateRealtimeSessionRequestBuilder {
300 pub fn model(mut self, model: impl Into<String>) -> Self {
303 self.model = Some(model.into());
304 self
305 }
306
307 pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
310 self.extra.insert(name.into(), value);
311 self
312 }
313
314 pub fn build(self) -> Result<CreateRealtimeSessionRequest, LingerError> {
317 validate_extra_fields(&self.extra)?;
318 Ok(CreateRealtimeSessionRequest {
319 model: required_string("model", self.model)?,
320 extra: self.extra,
321 })
322 }
323}
324
325#[derive(Clone, Debug, Default, Serialize, PartialEq)]
328#[non_exhaustive]
329pub struct CreateRealtimeTranscriptionSessionRequest {
330 #[serde(flatten)]
333 pub extra: BTreeMap<String, Value>,
334}
335
336impl CreateRealtimeTranscriptionSessionRequest {
337 pub fn builder() -> CreateRealtimeTranscriptionSessionRequestBuilder {
340 CreateRealtimeTranscriptionSessionRequestBuilder::default()
341 }
342}
343
344#[derive(Clone, Debug, Default)]
347#[non_exhaustive]
348pub struct CreateRealtimeTranscriptionSessionRequestBuilder {
349 extra: BTreeMap<String, Value>,
350}
351
352impl CreateRealtimeTranscriptionSessionRequestBuilder {
353 pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
356 self.extra.insert(name.into(), value);
357 self
358 }
359
360 pub fn build(self) -> Result<CreateRealtimeTranscriptionSessionRequest, LingerError> {
363 validate_extra_fields(&self.extra)?;
364 Ok(CreateRealtimeTranscriptionSessionRequest { extra: self.extra })
365 }
366}
367
368#[derive(Clone, Debug, Serialize, PartialEq)]
371#[non_exhaustive]
372pub struct CreateRealtimeTranslationSessionRequest {
373 pub model: String,
376 #[serde(flatten)]
379 pub extra: BTreeMap<String, Value>,
380}
381
382impl CreateRealtimeTranslationSessionRequest {
383 pub fn builder() -> CreateRealtimeTranslationSessionRequestBuilder {
386 CreateRealtimeTranslationSessionRequestBuilder::default()
387 }
388}
389
390#[derive(Clone, Debug, Default)]
393#[non_exhaustive]
394pub struct CreateRealtimeTranslationSessionRequestBuilder {
395 model: Option<String>,
396 extra: BTreeMap<String, Value>,
397}
398
399impl CreateRealtimeTranslationSessionRequestBuilder {
400 pub fn model(mut self, model: impl Into<String>) -> Self {
403 self.model = Some(model.into());
404 self
405 }
406
407 pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
410 self.extra.insert(name.into(), value);
411 self
412 }
413
414 pub fn build(self) -> Result<CreateRealtimeTranslationSessionRequest, LingerError> {
417 validate_extra_fields(&self.extra)?;
418 Ok(CreateRealtimeTranslationSessionRequest {
419 model: required_string("model", self.model)?,
420 extra: self.extra,
421 })
422 }
423}
424
425#[derive(Clone, Debug, Serialize, PartialEq)]
428#[non_exhaustive]
429pub struct CreateRealtimeTranslationClientSecretRequest {
430 pub session: CreateRealtimeTranslationSessionRequest,
433 #[serde(flatten)]
436 pub extra: BTreeMap<String, Value>,
437}
438
439impl CreateRealtimeTranslationClientSecretRequest {
440 pub fn builder() -> CreateRealtimeTranslationClientSecretRequestBuilder {
443 CreateRealtimeTranslationClientSecretRequestBuilder::default()
444 }
445}
446
447#[derive(Clone, Debug, Default)]
450#[non_exhaustive]
451pub struct CreateRealtimeTranslationClientSecretRequestBuilder {
452 session: Option<CreateRealtimeTranslationSessionRequest>,
453 extra: BTreeMap<String, Value>,
454}
455
456impl CreateRealtimeTranslationClientSecretRequestBuilder {
457 pub fn session(mut self, session: CreateRealtimeTranslationSessionRequest) -> Self {
460 self.session = Some(session);
461 self
462 }
463
464 pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
467 self.extra.insert(name.into(), value);
468 self
469 }
470
471 pub fn build(self) -> Result<CreateRealtimeTranslationClientSecretRequest, LingerError> {
474 validate_extra_fields(&self.extra)?;
475 Ok(CreateRealtimeTranslationClientSecretRequest {
476 session: self
477 .session
478 .ok_or_else(|| LingerError::invalid_config("session is required"))?,
479 extra: self.extra,
480 })
481 }
482}
483
484#[derive(Clone, Debug, Serialize, PartialEq)]
487#[non_exhaustive]
488pub struct CreateRealtimeClientSecretRequest {
489 pub session: RealtimeSessionConfig,
492}
493
494impl CreateRealtimeClientSecretRequest {
495 pub fn builder() -> CreateRealtimeClientSecretRequestBuilder {
498 CreateRealtimeClientSecretRequestBuilder::default()
499 }
500}
501
502#[derive(Clone, Debug, Default)]
505#[non_exhaustive]
506pub struct CreateRealtimeClientSecretRequestBuilder {
507 session: Option<RealtimeSessionConfig>,
508}
509
510impl CreateRealtimeClientSecretRequestBuilder {
511 pub fn session(mut self, session: RealtimeSessionConfig) -> Self {
514 self.session = Some(session);
515 self
516 }
517
518 pub fn build(self) -> Result<CreateRealtimeClientSecretRequest, LingerError> {
521 Ok(CreateRealtimeClientSecretRequest {
522 session: self
523 .session
524 .ok_or_else(|| LingerError::invalid_config("session is required"))?,
525 })
526 }
527}
528
529#[derive(Clone, Debug, Serialize, PartialEq)]
532#[non_exhaustive]
533pub struct RealtimeSessionConfig {
534 #[serde(rename = "type")]
537 pub kind: String,
538 #[serde(skip_serializing_if = "Option::is_none")]
541 pub model: Option<String>,
542 #[serde(flatten)]
545 pub extra: BTreeMap<String, Value>,
546}
547
548impl RealtimeSessionConfig {
549 pub fn builder(kind: impl Into<String>) -> RealtimeSessionConfigBuilder {
552 RealtimeSessionConfigBuilder {
553 kind: Some(kind.into()),
554 model: None,
555 extra: BTreeMap::new(),
556 }
557 }
558}
559
560#[derive(Clone, Debug, Default)]
563#[non_exhaustive]
564pub struct RealtimeSessionConfigBuilder {
565 kind: Option<String>,
566 model: Option<String>,
567 extra: BTreeMap<String, Value>,
568}
569
570impl RealtimeSessionConfigBuilder {
571 pub fn model(mut self, model: impl Into<String>) -> Self {
574 self.model = Some(model.into());
575 self
576 }
577
578 pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
581 self.extra.insert(name.into(), value);
582 self
583 }
584
585 pub fn build(self) -> Result<RealtimeSessionConfig, LingerError> {
588 validate_optional_string("model", self.model.as_deref())?;
589 validate_extra_fields(&self.extra)?;
590 Ok(RealtimeSessionConfig {
591 kind: required_string("type", self.kind)?,
592 model: self.model,
593 extra: self.extra,
594 })
595 }
596}
597
598#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
601#[non_exhaustive]
602pub struct RealtimeClientSecret {
603 pub value: RealtimeClientSecretValue,
606 #[serde(default)]
609 pub expires_at: Option<u64>,
610 #[serde(default)]
613 pub session: Value,
614 #[serde(flatten)]
617 pub extra: BTreeMap<String, Value>,
618 #[serde(skip)]
621 request_id: Option<RequestId>,
622}
623
624impl RealtimeClientSecret {
625 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
626 self.request_id = request_id;
627 self
628 }
629
630 pub fn request_id(&self) -> Option<&RequestId> {
633 self.request_id.as_ref()
634 }
635}
636
637#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
640#[non_exhaustive]
641pub struct RealtimeSession {
642 pub id: String,
645 pub object: String,
648 pub model: String,
651 pub client_secret: RealtimeClientSecret,
654 #[serde(flatten)]
657 pub extra: BTreeMap<String, Value>,
658 #[serde(skip)]
661 request_id: Option<RequestId>,
662}
663
664impl RealtimeSession {
665 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
666 self.request_id = request_id;
667 self
668 }
669
670 pub fn request_id(&self) -> Option<&RequestId> {
673 self.request_id.as_ref()
674 }
675}
676
677#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
680#[non_exhaustive]
681pub struct RealtimeTranscriptionSession {
682 #[serde(default)]
685 pub id: Option<String>,
686 #[serde(default)]
689 pub object: Option<String>,
690 #[serde(default)]
693 pub client_secret: Option<RealtimeClientSecret>,
694 #[serde(flatten)]
697 pub extra: BTreeMap<String, Value>,
698 #[serde(skip)]
701 request_id: Option<RequestId>,
702}
703
704impl RealtimeTranscriptionSession {
705 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
706 self.request_id = request_id;
707 self
708 }
709
710 pub fn request_id(&self) -> Option<&RequestId> {
713 self.request_id.as_ref()
714 }
715}
716
717#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
720#[non_exhaustive]
721pub struct RealtimeTranslationClientSecret {
722 pub value: RealtimeClientSecretValue,
725 pub expires_at: u64,
728 pub session: Value,
731 #[serde(flatten)]
734 pub extra: BTreeMap<String, Value>,
735 #[serde(skip)]
738 request_id: Option<RequestId>,
739}
740
741impl RealtimeTranslationClientSecret {
742 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
743 self.request_id = request_id;
744 self
745 }
746
747 pub fn request_id(&self) -> Option<&RequestId> {
750 self.request_id.as_ref()
751 }
752}
753
754#[derive(Clone, Deserialize, Serialize, PartialEq, Eq)]
757#[serde(transparent)]
758#[non_exhaustive]
759pub struct RealtimeClientSecretValue(String);
760
761impl RealtimeClientSecretValue {
762 pub fn as_str(&self) -> &str {
765 &self.0
766 }
767}
768
769impl fmt::Debug for RealtimeClientSecretValue {
770 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
771 f.write_str("\"<redacted>\"")
772 }
773}
774
775fn required_string(name: &str, value: Option<String>) -> Result<String, LingerError> {
776 value
777 .filter(|value| !value.trim().is_empty())
778 .ok_or_else(|| LingerError::invalid_config(format!("{name} is required")))
779}
780
781fn validate_optional_string(name: &str, value: Option<&str>) -> Result<(), LingerError> {
782 if value.is_some_and(|value| value.trim().is_empty()) {
783 return Err(LingerError::invalid_config(format!(
784 "{name} must not be empty"
785 )));
786 }
787 Ok(())
788}
789
790fn validate_extra_fields(extra: &BTreeMap<String, Value>) -> Result<(), LingerError> {
791 for (key, value) in extra {
792 if key.trim().is_empty() {
793 return Err(LingerError::invalid_config(
794 "extra field names must not be empty",
795 ));
796 }
797 if value.is_null() {
798 return Err(LingerError::invalid_config(format!(
799 "extra field {key} must not be null"
800 )));
801 }
802 }
803 Ok(())
804}
805
806fn push_typed_multipart_field(
807 body: &mut Vec<u8>,
808 boundary: &str,
809 name: &str,
810 content_type: &str,
811 value: &[u8],
812) {
813 body.extend_from_slice(
814 format!(
815 "--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"\r\nContent-Type: {content_type}\r\n\r\n"
816 )
817 .as_bytes(),
818 );
819 body.extend_from_slice(value);
820 body.extend_from_slice(b"\r\n");
821}
822
823fn realtime_multipart_boundary(sdp: &str, session: Option<&str>) -> String {
824 for counter in 0.. {
825 let boundary = format!("linger-openai-sdk-realtime-boundary-{counter}");
826 let boundary_bytes = boundary.as_bytes();
827 let conflicts_with_sdp = contains_bytes(sdp.as_bytes(), boundary_bytes);
828 let conflicts_with_session =
829 session.is_some_and(|session| contains_bytes(session.as_bytes(), boundary_bytes));
830 if !conflicts_with_sdp && !conflicts_with_session {
831 return boundary;
832 }
833 }
834 unreachable!("unbounded boundary counter")
835}
836
837fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
838 if needle.is_empty() {
839 return true;
840 }
841 haystack
842 .windows(needle.len())
843 .any(|window| window == needle)
844}