1use crate::error::LingerError;
2use crate::transport::{BodyStream, HttpRequest};
3use crate::RequestId;
4use bytes::Bytes;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::BTreeMap;
8
9#[derive(Clone, Debug, Serialize, PartialEq)]
12#[non_exhaustive]
13pub struct CreateSpeechRequest {
14 pub model: String,
17 pub input: String,
20 pub voice: String,
23 #[serde(skip_serializing_if = "Option::is_none")]
26 pub response_format: Option<String>,
27 #[serde(skip_serializing_if = "Option::is_none")]
30 pub speed: Option<f32>,
31 #[serde(skip_serializing_if = "Option::is_none")]
34 pub instructions: Option<String>,
35 #[serde(skip_serializing_if = "Option::is_none")]
38 pub stream_format: Option<String>,
39 #[serde(flatten)]
42 pub extra: BTreeMap<String, Value>,
43}
44
45impl CreateSpeechRequest {
46 pub fn builder() -> CreateSpeechRequestBuilder {
49 CreateSpeechRequestBuilder::default()
50 }
51}
52
53#[derive(Clone, Debug, Default)]
56#[non_exhaustive]
57pub struct CreateSpeechRequestBuilder {
58 model: Option<String>,
59 input: Option<String>,
60 voice: Option<String>,
61 response_format: Option<String>,
62 speed: Option<f32>,
63 instructions: Option<String>,
64 stream_format: Option<String>,
65 extra: BTreeMap<String, Value>,
66}
67
68impl CreateSpeechRequestBuilder {
69 pub fn model(mut self, model: impl Into<String>) -> Self {
72 self.model = Some(model.into());
73 self
74 }
75
76 pub fn input(mut self, input: impl Into<String>) -> Self {
79 self.input = Some(input.into());
80 self
81 }
82
83 pub fn voice(mut self, voice: impl Into<String>) -> Self {
86 self.voice = Some(voice.into());
87 self
88 }
89
90 pub fn response_format(mut self, response_format: impl Into<String>) -> Self {
93 self.response_format = Some(response_format.into());
94 self
95 }
96
97 pub fn speed(mut self, speed: f32) -> Self {
100 self.speed = Some(speed);
101 self
102 }
103
104 pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
107 self.instructions = Some(instructions.into());
108 self
109 }
110
111 pub fn stream_format(mut self, stream_format: impl Into<String>) -> Self {
114 self.stream_format = Some(stream_format.into());
115 self
116 }
117
118 pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
121 self.extra.insert(name.into(), value);
122 self
123 }
124
125 pub fn build(self) -> Result<CreateSpeechRequest, LingerError> {
128 let model = required_string("model", self.model)?;
129 let input = required_string("input", self.input)?;
130 let voice = required_string("voice", self.voice)?;
131 validate_max_chars("input", &input, 4096)?;
132 validate_optional_string("response_format", self.response_format.as_deref())?;
133 validate_optional_string("instructions", self.instructions.as_deref())?;
134 if let Some(instructions) = self.instructions.as_deref() {
135 validate_max_chars("instructions", instructions, 4096)?;
136 }
137 validate_optional_string("stream_format", self.stream_format.as_deref())?;
138 if self
139 .speed
140 .is_some_and(|speed| !(0.25..=4.0).contains(&speed))
141 {
142 return Err(LingerError::invalid_config(
143 "speed must be between 0.25 and 4.0",
144 ));
145 }
146 Ok(CreateSpeechRequest {
147 model,
148 input,
149 voice,
150 response_format: self.response_format,
151 speed: self.speed,
152 instructions: self.instructions,
153 stream_format: self.stream_format,
154 extra: self.extra,
155 })
156 }
157}
158
159pub struct AudioSpeechResponse {
162 request_id: Option<RequestId>,
163 content_type: Option<String>,
164 body: BodyStream,
165}
166
167impl AudioSpeechResponse {
168 pub(crate) fn new(
169 request_id: Option<RequestId>,
170 content_type: Option<String>,
171 body: BodyStream,
172 ) -> Self {
173 Self {
174 request_id,
175 content_type,
176 body,
177 }
178 }
179
180 pub fn request_id(&self) -> Option<&RequestId> {
183 self.request_id.as_ref()
184 }
185
186 pub fn content_type(&self) -> Option<&str> {
189 self.content_type.as_deref()
190 }
191
192 pub fn into_stream(self) -> BodyStream {
195 self.body
196 }
197}
198
199#[derive(Clone, Debug, PartialEq, Eq)]
202#[non_exhaustive]
203pub struct AudioUpload {
204 pub filename: String,
207 pub content_type: String,
210 content: Bytes,
211}
212
213impl AudioUpload {
214 pub fn from_bytes(
217 filename: impl Into<String>,
218 content: impl Into<Bytes>,
219 ) -> Result<Self, LingerError> {
220 let filename = filename.into();
221 validate_header_param("filename", &filename)?;
222 Ok(Self {
223 filename,
224 content_type: "application/octet-stream".to_string(),
225 content: content.into(),
226 })
227 }
228
229 pub fn content_type(mut self, content_type: impl Into<String>) -> Result<Self, LingerError> {
232 let content_type = content_type.into();
233 validate_header_value("content_type", &content_type)?;
234 self.content_type = content_type;
235 Ok(self)
236 }
237}
238
239#[derive(Clone, Debug, PartialEq, Eq)]
242#[non_exhaustive]
243pub struct CreateVoiceConsentRequest {
244 pub name: String,
247 pub language: String,
250 pub recording: AudioUpload,
253}
254
255impl CreateVoiceConsentRequest {
256 pub fn builder() -> CreateVoiceConsentRequestBuilder {
259 CreateVoiceConsentRequestBuilder::default()
260 }
261
262 pub(crate) fn apply_multipart_body(&self, request: &mut HttpRequest) {
263 let fields = vec![
264 ("name".to_string(), self.name.clone()),
265 ("language".to_string(), self.language.clone()),
266 ];
267 apply_audio_multipart(request, "recording", &self.recording, fields);
268 }
269}
270
271#[derive(Clone, Debug, Default)]
274#[non_exhaustive]
275pub struct CreateVoiceConsentRequestBuilder {
276 name: Option<String>,
277 language: Option<String>,
278 recording: Option<AudioUpload>,
279}
280
281impl CreateVoiceConsentRequestBuilder {
282 pub fn name(mut self, name: impl Into<String>) -> Self {
285 self.name = Some(name.into());
286 self
287 }
288
289 pub fn language(mut self, language: impl Into<String>) -> Self {
292 self.language = Some(language.into());
293 self
294 }
295
296 pub fn recording(mut self, recording: AudioUpload) -> Self {
299 self.recording = Some(recording);
300 self
301 }
302
303 pub fn build(self) -> Result<CreateVoiceConsentRequest, LingerError> {
306 let name = required_string("name", self.name)?;
307 let language = required_string("language", self.language)?;
308 let recording = self
309 .recording
310 .ok_or_else(|| LingerError::invalid_config("recording is required"))?;
311 Ok(CreateVoiceConsentRequest {
312 name,
313 language,
314 recording,
315 })
316 }
317}
318
319#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
322#[non_exhaustive]
323pub struct UpdateVoiceConsentRequest {
324 pub name: String,
327}
328
329impl UpdateVoiceConsentRequest {
330 pub fn builder() -> UpdateVoiceConsentRequestBuilder {
333 UpdateVoiceConsentRequestBuilder::default()
334 }
335}
336
337#[derive(Clone, Debug, Default)]
340#[non_exhaustive]
341pub struct UpdateVoiceConsentRequestBuilder {
342 name: Option<String>,
343}
344
345impl UpdateVoiceConsentRequestBuilder {
346 pub fn name(mut self, name: impl Into<String>) -> Self {
349 self.name = Some(name.into());
350 self
351 }
352
353 pub fn build(self) -> Result<UpdateVoiceConsentRequest, LingerError> {
356 let name = required_string("name", self.name)?;
357 Ok(UpdateVoiceConsentRequest { name })
358 }
359}
360
361#[derive(Clone, Debug, Default, PartialEq, Eq)]
364#[non_exhaustive]
365pub struct AudioVoiceConsentListRequest {
366 pub limit: Option<u8>,
369 pub after: Option<String>,
372}
373
374impl AudioVoiceConsentListRequest {
375 pub fn builder() -> AudioVoiceConsentListRequestBuilder {
378 AudioVoiceConsentListRequestBuilder::default()
379 }
380
381 pub(crate) fn path(&self) -> String {
382 path_with_query(
383 "/v1/audio/voice_consents",
384 AudioListQuery {
385 limit: self.limit,
386 after: self.after.as_deref(),
387 },
388 )
389 }
390}
391
392#[derive(Clone, Debug, Default)]
395#[non_exhaustive]
396pub struct AudioVoiceConsentListRequestBuilder {
397 limit: Option<u8>,
398 after: Option<String>,
399}
400
401impl AudioVoiceConsentListRequestBuilder {
402 pub fn limit(mut self, limit: u8) -> Self {
405 self.limit = Some(limit);
406 self
407 }
408
409 pub fn after(mut self, after: impl Into<String>) -> Self {
412 self.after = Some(after.into());
413 self
414 }
415
416 pub fn build(self) -> Result<AudioVoiceConsentListRequest, LingerError> {
419 if let Some(limit) = self.limit {
420 if limit == 0 || limit > 100 {
421 return Err(LingerError::invalid_config(
422 "limit must be between 1 and 100",
423 ));
424 }
425 }
426 if let Some(after) = &self.after {
427 if after.trim().is_empty() {
428 return Err(LingerError::invalid_config("after must not be empty"));
429 }
430 }
431 Ok(AudioVoiceConsentListRequest {
432 limit: self.limit,
433 after: self.after,
434 })
435 }
436}
437
438#[derive(Clone, Debug, PartialEq, Eq)]
441#[non_exhaustive]
442pub struct CreateVoiceRequest {
443 pub name: String,
446 pub consent: String,
449 pub audio_sample: AudioUpload,
452}
453
454impl CreateVoiceRequest {
455 pub fn builder() -> CreateVoiceRequestBuilder {
458 CreateVoiceRequestBuilder::default()
459 }
460
461 pub(crate) fn apply_multipart_body(&self, request: &mut HttpRequest) {
462 let fields = vec![
463 ("name".to_string(), self.name.clone()),
464 ("consent".to_string(), self.consent.clone()),
465 ];
466 apply_audio_multipart(request, "audio_sample", &self.audio_sample, fields);
467 }
468}
469
470#[derive(Clone, Debug, Default)]
473#[non_exhaustive]
474pub struct CreateVoiceRequestBuilder {
475 name: Option<String>,
476 consent: Option<String>,
477 audio_sample: Option<AudioUpload>,
478}
479
480impl CreateVoiceRequestBuilder {
481 pub fn name(mut self, name: impl Into<String>) -> Self {
484 self.name = Some(name.into());
485 self
486 }
487
488 pub fn consent(mut self, consent: impl Into<String>) -> Self {
491 self.consent = Some(consent.into());
492 self
493 }
494
495 pub fn audio_sample(mut self, audio_sample: AudioUpload) -> Self {
498 self.audio_sample = Some(audio_sample);
499 self
500 }
501
502 pub fn build(self) -> Result<CreateVoiceRequest, LingerError> {
505 let name = required_string("name", self.name)?;
506 let consent = required_string("consent", self.consent)?;
507 let audio_sample = self
508 .audio_sample
509 .ok_or_else(|| LingerError::invalid_config("audio_sample is required"))?;
510 Ok(CreateVoiceRequest {
511 name,
512 consent,
513 audio_sample,
514 })
515 }
516}
517
518#[derive(Clone, Debug, PartialEq)]
521#[non_exhaustive]
522pub struct CreateTranscriptionRequest {
523 pub file: AudioUpload,
526 pub model: String,
529 pub language: Option<String>,
532 pub prompt: Option<String>,
535 pub response_format: Option<String>,
538 pub temperature: Option<f32>,
541 pub timestamp_granularities: Vec<String>,
544 pub chunking_strategy: Option<String>,
547 pub known_speaker_names: Vec<String>,
550 pub known_speaker_references: Vec<String>,
553 include_logprobs: bool,
554}
555
556impl CreateTranscriptionRequest {
557 pub fn builder() -> CreateTranscriptionRequestBuilder {
560 CreateTranscriptionRequestBuilder::default()
561 }
562
563 pub(crate) fn apply_multipart_body(&self, request: &mut HttpRequest) {
564 let mut fields = Vec::new();
565 fields.push(("model".to_string(), self.model.clone()));
566 push_optional_field(&mut fields, "language", self.language.as_deref());
567 push_optional_field(&mut fields, "prompt", self.prompt.as_deref());
568 push_optional_field(
569 &mut fields,
570 "response_format",
571 self.response_format.as_deref(),
572 );
573 if let Some(temperature) = self.temperature {
574 fields.push(("temperature".to_string(), temperature.to_string()));
575 }
576 for granularity in &self.timestamp_granularities {
577 fields.push(("timestamp_granularities[]".to_string(), granularity.clone()));
578 }
579 push_optional_field(
580 &mut fields,
581 "chunking_strategy",
582 self.chunking_strategy.as_deref(),
583 );
584 for name in &self.known_speaker_names {
585 fields.push(("known_speaker_names[]".to_string(), name.clone()));
586 }
587 for reference in &self.known_speaker_references {
588 fields.push(("known_speaker_references[]".to_string(), reference.clone()));
589 }
590 if self.include_logprobs {
591 fields.push(("include[]".to_string(), "logprobs".to_string()));
592 }
593 apply_audio_multipart(request, "file", &self.file, fields);
594 }
595}
596
597#[derive(Clone, Debug, Default)]
600#[non_exhaustive]
601pub struct CreateTranscriptionRequestBuilder {
602 file: Option<AudioUpload>,
603 model: Option<String>,
604 language: Option<String>,
605 prompt: Option<String>,
606 response_format: Option<String>,
607 temperature: Option<f32>,
608 timestamp_granularities: Vec<String>,
609 chunking_strategy: Option<String>,
610 known_speaker_names: Vec<String>,
611 known_speaker_references: Vec<String>,
612 include_logprobs: bool,
613}
614
615impl CreateTranscriptionRequestBuilder {
616 pub fn file(mut self, file: AudioUpload) -> Self {
619 self.file = Some(file);
620 self
621 }
622
623 pub fn model(mut self, model: impl Into<String>) -> Self {
626 self.model = Some(model.into());
627 self
628 }
629
630 pub fn language(mut self, language: impl Into<String>) -> Self {
633 self.language = Some(language.into());
634 self
635 }
636
637 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
640 self.prompt = Some(prompt.into());
641 self
642 }
643
644 pub fn response_format(mut self, response_format: impl Into<String>) -> Self {
647 self.response_format = Some(response_format.into());
648 self
649 }
650
651 pub fn temperature(mut self, temperature: f32) -> Self {
654 self.temperature = Some(temperature);
655 self
656 }
657
658 pub fn timestamp_granularity(mut self, granularity: impl Into<String>) -> Self {
661 self.timestamp_granularities.push(granularity.into());
662 self
663 }
664
665 pub fn timestamp_granularities(
668 mut self,
669 granularities: impl IntoIterator<Item = impl Into<String>>,
670 ) -> Self {
671 self.timestamp_granularities = granularities.into_iter().map(Into::into).collect();
672 self
673 }
674
675 pub fn chunking_strategy_auto(mut self) -> Self {
678 self.chunking_strategy = Some("auto".to_string());
679 self
680 }
681
682 pub fn known_speaker_name(mut self, name: impl Into<String>) -> Self {
685 self.known_speaker_names.push(name.into());
686 self
687 }
688
689 pub fn known_speaker_names(
692 mut self,
693 names: impl IntoIterator<Item = impl Into<String>>,
694 ) -> Self {
695 self.known_speaker_names = names.into_iter().map(Into::into).collect();
696 self
697 }
698
699 pub fn known_speaker_reference(mut self, reference: impl Into<String>) -> Self {
702 self.known_speaker_references.push(reference.into());
703 self
704 }
705
706 pub fn known_speaker_references(
709 mut self,
710 references: impl IntoIterator<Item = impl Into<String>>,
711 ) -> Self {
712 self.known_speaker_references = references.into_iter().map(Into::into).collect();
713 self
714 }
715
716 pub fn include_logprobs(mut self) -> Self {
719 self.include_logprobs = true;
720 self
721 }
722
723 pub fn build(self) -> Result<CreateTranscriptionRequest, LingerError> {
726 let file = self
727 .file
728 .ok_or_else(|| LingerError::invalid_config("file is required"))?;
729 let model = required_string("model", self.model)?;
730 validate_optional_string("language", self.language.as_deref())?;
731 validate_optional_string("prompt", self.prompt.as_deref())?;
732 validate_optional_string("response_format", self.response_format.as_deref())?;
733 validate_optional_string("chunking_strategy", self.chunking_strategy.as_deref())?;
734 validate_string_items("timestamp_granularities", &self.timestamp_granularities)?;
735 validate_limited_string_items("known_speaker_names", &self.known_speaker_names, 4)?;
736 validate_limited_string_items(
737 "known_speaker_references",
738 &self.known_speaker_references,
739 4,
740 )?;
741 Ok(CreateTranscriptionRequest {
742 file,
743 model,
744 language: self.language,
745 prompt: self.prompt,
746 response_format: self.response_format,
747 temperature: self.temperature,
748 timestamp_granularities: self.timestamp_granularities,
749 chunking_strategy: self.chunking_strategy,
750 known_speaker_names: self.known_speaker_names,
751 known_speaker_references: self.known_speaker_references,
752 include_logprobs: self.include_logprobs,
753 })
754 }
755}
756
757#[derive(Clone, Debug, PartialEq)]
760#[non_exhaustive]
761pub struct CreateTranslationRequest {
762 pub file: AudioUpload,
765 pub model: String,
768 pub prompt: Option<String>,
771 pub response_format: Option<String>,
774 pub temperature: Option<f32>,
777}
778
779impl CreateTranslationRequest {
780 pub fn builder() -> CreateTranslationRequestBuilder {
783 CreateTranslationRequestBuilder::default()
784 }
785
786 pub(crate) fn apply_multipart_body(&self, request: &mut HttpRequest) {
787 let mut fields = Vec::new();
788 fields.push(("model".to_string(), self.model.clone()));
789 push_optional_field(&mut fields, "prompt", self.prompt.as_deref());
790 push_optional_field(
791 &mut fields,
792 "response_format",
793 self.response_format.as_deref(),
794 );
795 if let Some(temperature) = self.temperature {
796 fields.push(("temperature".to_string(), temperature.to_string()));
797 }
798 apply_audio_multipart(request, "file", &self.file, fields);
799 }
800}
801
802#[derive(Clone, Debug, Default)]
805#[non_exhaustive]
806pub struct CreateTranslationRequestBuilder {
807 file: Option<AudioUpload>,
808 model: Option<String>,
809 prompt: Option<String>,
810 response_format: Option<String>,
811 temperature: Option<f32>,
812}
813
814impl CreateTranslationRequestBuilder {
815 pub fn file(mut self, file: AudioUpload) -> Self {
818 self.file = Some(file);
819 self
820 }
821
822 pub fn model(mut self, model: impl Into<String>) -> Self {
825 self.model = Some(model.into());
826 self
827 }
828
829 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
832 self.prompt = Some(prompt.into());
833 self
834 }
835
836 pub fn response_format(mut self, response_format: impl Into<String>) -> Self {
839 self.response_format = Some(response_format.into());
840 self
841 }
842
843 pub fn temperature(mut self, temperature: f32) -> Self {
846 self.temperature = Some(temperature);
847 self
848 }
849
850 pub fn build(self) -> Result<CreateTranslationRequest, LingerError> {
853 let file = self
854 .file
855 .ok_or_else(|| LingerError::invalid_config("file is required"))?;
856 let model = required_string("model", self.model)?;
857 validate_optional_string("prompt", self.prompt.as_deref())?;
858 validate_optional_string("response_format", self.response_format.as_deref())?;
859 Ok(CreateTranslationRequest {
860 file,
861 model,
862 prompt: self.prompt,
863 response_format: self.response_format,
864 temperature: self.temperature,
865 })
866 }
867}
868
869#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
872#[non_exhaustive]
873pub struct AudioTranscription {
874 pub text: String,
877 #[serde(flatten)]
880 pub extra: BTreeMap<String, Value>,
881 #[serde(skip)]
884 request_id: Option<RequestId>,
885}
886
887impl AudioTranscription {
888 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
889 self.request_id = request_id;
890 self
891 }
892
893 pub fn request_id(&self) -> Option<&RequestId> {
896 self.request_id.as_ref()
897 }
898}
899
900#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
903#[non_exhaustive]
904pub struct AudioTranslation {
905 pub text: String,
908 #[serde(flatten)]
911 pub extra: BTreeMap<String, Value>,
912 #[serde(skip)]
915 request_id: Option<RequestId>,
916}
917
918impl AudioTranslation {
919 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
920 self.request_id = request_id;
921 self
922 }
923
924 pub fn request_id(&self) -> Option<&RequestId> {
927 self.request_id.as_ref()
928 }
929}
930
931#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
934#[non_exhaustive]
935pub struct AudioVoiceConsent {
936 pub object: String,
939 pub id: String,
942 pub name: String,
945 pub language: String,
948 pub created_at: u64,
951 #[serde(skip)]
954 request_id: Option<RequestId>,
955}
956
957impl AudioVoiceConsent {
958 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
959 self.request_id = request_id;
960 self
961 }
962
963 pub fn request_id(&self) -> Option<&RequestId> {
966 self.request_id.as_ref()
967 }
968}
969
970#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
973#[non_exhaustive]
974pub struct AudioVoiceConsentPage {
975 pub object: String,
978 #[serde(default)]
981 pub data: Vec<AudioVoiceConsent>,
982 #[serde(default)]
985 pub first_id: Option<String>,
986 #[serde(default)]
989 pub last_id: Option<String>,
990 pub has_more: bool,
993 #[serde(skip)]
996 request_id: Option<RequestId>,
997}
998
999impl AudioVoiceConsentPage {
1000 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
1001 self.request_id = request_id;
1002 self
1003 }
1004
1005 pub fn request_id(&self) -> Option<&RequestId> {
1008 self.request_id.as_ref()
1009 }
1010}
1011
1012#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
1015#[non_exhaustive]
1016pub struct AudioVoiceConsentDeletion {
1017 pub id: String,
1020 pub object: String,
1023 pub deleted: bool,
1026 #[serde(skip)]
1029 request_id: Option<RequestId>,
1030}
1031
1032impl AudioVoiceConsentDeletion {
1033 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
1034 self.request_id = request_id;
1035 self
1036 }
1037
1038 pub fn request_id(&self) -> Option<&RequestId> {
1041 self.request_id.as_ref()
1042 }
1043}
1044
1045#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
1048#[non_exhaustive]
1049pub struct AudioVoice {
1050 pub object: String,
1053 pub id: String,
1056 pub name: String,
1059 pub created_at: u64,
1062 #[serde(skip)]
1065 request_id: Option<RequestId>,
1066}
1067
1068impl AudioVoice {
1069 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
1070 self.request_id = request_id;
1071 self
1072 }
1073
1074 pub fn request_id(&self) -> Option<&RequestId> {
1077 self.request_id.as_ref()
1078 }
1079}
1080
1081fn apply_audio_multipart(
1082 request: &mut HttpRequest,
1083 file_field_name: &str,
1084 file: &AudioUpload,
1085 fields: Vec<(String, String)>,
1086) {
1087 let boundary = multipart_boundary(&file.content);
1088 request.insert_header(
1089 "content-type",
1090 format!("multipart/form-data; boundary={boundary}"),
1091 );
1092 let mut chunks = Vec::new();
1093 for (name, value) in fields {
1094 chunks.push(Ok(Bytes::from(format!(
1095 "--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"\r\n\r\n{value}\r\n"
1096 ))));
1097 }
1098 chunks.push(Ok(Bytes::from(format!(
1099 "--{boundary}\r\nContent-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n",
1100 escape_multipart_param(file_field_name),
1101 escape_multipart_param(&file.filename),
1102 file.content_type
1103 ))));
1104 chunks.push(Ok(file.content.clone()));
1105 chunks.push(Ok(Bytes::from(format!("\r\n--{boundary}--\r\n"))));
1106 request.set_body_stream(futures_util::stream::iter(chunks));
1107}
1108
1109fn push_optional_field(fields: &mut Vec<(String, String)>, name: &str, value: Option<&str>) {
1110 if let Some(value) = value {
1111 fields.push((name.to_string(), value.to_string()));
1112 }
1113}
1114
1115fn multipart_boundary(content: &Bytes) -> String {
1116 for counter in 0.. {
1117 let boundary = format!("linger-openai-sdk-audio-boundary-{counter}");
1118 if !contains_bytes(content, boundary.as_bytes()) {
1119 return boundary;
1120 }
1121 }
1122 unreachable!("unbounded boundary counter")
1123}
1124
1125fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
1126 if needle.is_empty() {
1127 return true;
1128 }
1129 haystack
1130 .windows(needle.len())
1131 .any(|window| window == needle)
1132}
1133
1134fn required_string(name: &str, value: Option<String>) -> Result<String, LingerError> {
1135 value
1136 .filter(|value| !value.trim().is_empty())
1137 .ok_or_else(|| LingerError::invalid_config(format!("{name} is required")))
1138}
1139
1140fn validate_optional_string(name: &str, value: Option<&str>) -> Result<(), LingerError> {
1141 if value.is_some_and(|value| value.trim().is_empty()) {
1142 return Err(LingerError::invalid_config(format!(
1143 "{name} must not be empty"
1144 )));
1145 }
1146 Ok(())
1147}
1148
1149fn validate_max_chars(name: &str, value: &str, max_chars: usize) -> Result<(), LingerError> {
1150 if value.chars().count() > max_chars {
1151 return Err(LingerError::invalid_config(format!(
1152 "{name} must be at most {max_chars} characters"
1153 )));
1154 }
1155 Ok(())
1156}
1157
1158fn validate_string_items(name: &str, values: &[String]) -> Result<(), LingerError> {
1159 if values.iter().any(|value| value.trim().is_empty()) {
1160 return Err(LingerError::invalid_config(format!(
1161 "{name} must not contain empty values"
1162 )));
1163 }
1164 Ok(())
1165}
1166
1167fn validate_limited_string_items(
1168 name: &str,
1169 values: &[String],
1170 max: usize,
1171) -> Result<(), LingerError> {
1172 validate_string_items(name, values)?;
1173 if values.len() > max {
1174 return Err(LingerError::invalid_config(format!(
1175 "{name} must contain at most {max} values"
1176 )));
1177 }
1178 Ok(())
1179}
1180
1181fn validate_header_param(name: &str, value: &str) -> Result<(), LingerError> {
1182 if value.trim().is_empty() {
1183 return Err(LingerError::invalid_config(format!("{name} is required")));
1184 }
1185 validate_header_value(name, value)
1186}
1187
1188fn validate_header_value(name: &str, value: &str) -> Result<(), LingerError> {
1189 if value.contains('\r') || value.contains('\n') {
1190 return Err(LingerError::invalid_config(format!(
1191 "{name} must not contain CR or LF"
1192 )));
1193 }
1194 Ok(())
1195}
1196
1197fn escape_multipart_param(value: &str) -> String {
1198 value.replace('\\', "\\\\").replace('"', "\\\"")
1199}
1200
1201struct AudioListQuery<'a> {
1202 limit: Option<u8>,
1203 after: Option<&'a str>,
1204}
1205
1206fn path_with_query(base: &str, params: AudioListQuery<'_>) -> String {
1207 let mut query = Vec::new();
1208 if let Some(limit) = params.limit {
1209 query.push(format!("limit={limit}"));
1210 }
1211 if let Some(after) = params.after {
1212 query.push(format!("after={}", encode_query_value(after)));
1213 }
1214 if query.is_empty() {
1215 base.to_string()
1216 } else {
1217 format!("{base}?{}", query.join("&"))
1218 }
1219}
1220
1221fn encode_query_value(value: &str) -> String {
1222 let mut encoded = String::new();
1223 for byte in value.bytes() {
1224 match byte {
1225 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
1226 encoded.push(byte as char);
1227 }
1228 _ => {
1229 const HEX: &[u8; 16] = b"0123456789ABCDEF";
1230 encoded.push('%');
1231 encoded.push(HEX[(byte >> 4) as usize] as char);
1232 encoded.push(HEX[(byte & 0x0F) as usize] as char);
1233 }
1234 }
1235 }
1236 encoded
1237}