1use crate::error::LingerError;
2use crate::transport::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 CreateImageRequest {
14 pub prompt: String,
17 #[serde(skip_serializing_if = "Option::is_none")]
20 pub model: Option<String>,
21 #[serde(skip_serializing_if = "Option::is_none")]
24 pub n: Option<u32>,
25 #[serde(skip_serializing_if = "Option::is_none")]
28 pub background: Option<String>,
29 #[serde(skip_serializing_if = "Option::is_none")]
32 pub moderation: Option<String>,
33 #[serde(skip_serializing_if = "Option::is_none")]
36 pub output_format: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
40 pub output_compression: Option<u8>,
41 #[serde(skip_serializing_if = "Option::is_none")]
44 pub quality: Option<String>,
45 #[serde(skip_serializing_if = "Option::is_none")]
48 pub response_format: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
52 pub size: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
56 pub style: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none")]
60 pub user: Option<String>,
61 #[serde(flatten)]
64 pub extra: BTreeMap<String, Value>,
65}
66
67impl CreateImageRequest {
68 pub fn builder() -> CreateImageRequestBuilder {
71 CreateImageRequestBuilder::default()
72 }
73}
74
75#[derive(Clone, Debug, Default)]
78#[non_exhaustive]
79pub struct CreateImageRequestBuilder {
80 prompt: Option<String>,
81 model: Option<String>,
82 n: Option<u32>,
83 background: Option<String>,
84 moderation: Option<String>,
85 output_format: Option<String>,
86 output_compression: Option<u8>,
87 quality: Option<String>,
88 response_format: Option<String>,
89 size: Option<String>,
90 style: Option<String>,
91 user: Option<String>,
92 extra: BTreeMap<String, Value>,
93}
94
95impl CreateImageRequestBuilder {
96 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
99 self.prompt = Some(prompt.into());
100 self
101 }
102
103 pub fn model(mut self, model: impl Into<String>) -> Self {
106 self.model = Some(model.into());
107 self
108 }
109
110 pub fn n(mut self, n: u32) -> Self {
113 self.n = Some(n);
114 self
115 }
116
117 pub fn background(mut self, background: impl Into<String>) -> Self {
120 self.background = Some(background.into());
121 self
122 }
123
124 pub fn moderation(mut self, moderation: impl Into<String>) -> Self {
127 self.moderation = Some(moderation.into());
128 self
129 }
130
131 pub fn output_format(mut self, output_format: impl Into<String>) -> Self {
134 self.output_format = Some(output_format.into());
135 self
136 }
137
138 pub fn output_compression(mut self, output_compression: u8) -> Self {
141 self.output_compression = Some(output_compression);
142 self
143 }
144
145 pub fn quality(mut self, quality: impl Into<String>) -> Self {
148 self.quality = Some(quality.into());
149 self
150 }
151
152 pub fn response_format(mut self, response_format: impl Into<String>) -> Self {
155 self.response_format = Some(response_format.into());
156 self
157 }
158
159 pub fn size(mut self, size: impl Into<String>) -> Self {
162 self.size = Some(size.into());
163 self
164 }
165
166 pub fn style(mut self, style: impl Into<String>) -> Self {
169 self.style = Some(style.into());
170 self
171 }
172
173 pub fn user(mut self, user: impl Into<String>) -> Self {
176 self.user = Some(user.into());
177 self
178 }
179
180 pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
183 self.extra.insert(name.into(), value);
184 self
185 }
186
187 pub fn build(self) -> Result<CreateImageRequest, LingerError> {
190 let prompt = self
191 .prompt
192 .filter(|value| !value.trim().is_empty())
193 .ok_or_else(|| LingerError::invalid_config("prompt is required"))?;
194 validate_optional_string("model", self.model.as_deref())?;
195 validate_optional_string("background", self.background.as_deref())?;
196 validate_optional_string("moderation", self.moderation.as_deref())?;
197 validate_optional_string("output_format", self.output_format.as_deref())?;
198 validate_optional_string("quality", self.quality.as_deref())?;
199 validate_optional_string("response_format", self.response_format.as_deref())?;
200 validate_optional_string("size", self.size.as_deref())?;
201 validate_optional_string("style", self.style.as_deref())?;
202 validate_optional_string("user", self.user.as_deref())?;
203 if self.n.is_some_and(|n| !(1..=10).contains(&n)) {
204 return Err(LingerError::invalid_config("n must be between 1 and 10"));
205 }
206 if self
207 .output_compression
208 .is_some_and(|output_compression| output_compression > 100)
209 {
210 return Err(LingerError::invalid_config(
211 "output_compression must be between 0 and 100",
212 ));
213 }
214 Ok(CreateImageRequest {
215 prompt,
216 model: self.model,
217 n: self.n,
218 background: self.background,
219 moderation: self.moderation,
220 output_format: self.output_format,
221 output_compression: self.output_compression,
222 quality: self.quality,
223 response_format: self.response_format,
224 size: self.size,
225 style: self.style,
226 user: self.user,
227 extra: self.extra,
228 })
229 }
230}
231
232#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
235#[serde(untagged)]
236pub enum ImageInput {
237 FileId {
240 file_id: String,
243 },
244 ImageUrl {
247 image_url: String,
250 },
251}
252
253impl ImageInput {
254 pub fn file_id(file_id: impl Into<String>) -> Self {
257 Self::FileId {
258 file_id: file_id.into(),
259 }
260 }
261
262 pub fn image_url(image_url: impl Into<String>) -> Self {
265 Self::ImageUrl {
266 image_url: image_url.into(),
267 }
268 }
269
270 fn validate(&self) -> Result<(), LingerError> {
271 let (name, value) = match self {
272 Self::FileId { file_id } => ("file_id", file_id),
273 Self::ImageUrl { image_url } => ("image_url", image_url),
274 };
275 if value.trim().is_empty() {
276 return Err(LingerError::invalid_config(format!("{name} is required")));
277 }
278 Ok(())
279 }
280}
281
282#[derive(Clone, Debug, Serialize, PartialEq)]
285#[non_exhaustive]
286pub struct CreateImageEditRequest {
287 pub images: Vec<ImageInput>,
290 pub prompt: String,
293 #[serde(skip_serializing_if = "Option::is_none")]
296 pub model: Option<String>,
297 #[serde(skip_serializing_if = "Option::is_none")]
300 pub n: Option<u32>,
301 #[serde(skip_serializing_if = "Option::is_none")]
304 pub size: Option<String>,
305 #[serde(skip_serializing_if = "Option::is_none")]
308 pub background: Option<String>,
309 #[serde(skip_serializing_if = "Option::is_none")]
312 pub input_fidelity: Option<String>,
313 #[serde(skip_serializing_if = "Option::is_none")]
316 pub moderation: Option<String>,
317 #[serde(skip_serializing_if = "Option::is_none")]
320 pub quality: Option<String>,
321 #[serde(skip_serializing_if = "Option::is_none")]
324 pub output_compression: Option<u8>,
325 #[serde(skip_serializing_if = "Option::is_none")]
328 pub response_format: Option<String>,
329 #[serde(skip_serializing_if = "Option::is_none")]
332 pub user: Option<String>,
333 #[serde(flatten)]
336 pub extra: BTreeMap<String, Value>,
337}
338
339impl CreateImageEditRequest {
340 pub fn builder() -> CreateImageEditRequestBuilder {
343 CreateImageEditRequestBuilder::default()
344 }
345}
346
347#[derive(Clone, Debug, Default)]
350#[non_exhaustive]
351pub struct CreateImageEditRequestBuilder {
352 images: Vec<ImageInput>,
353 prompt: Option<String>,
354 model: Option<String>,
355 n: Option<u32>,
356 size: Option<String>,
357 background: Option<String>,
358 input_fidelity: Option<String>,
359 moderation: Option<String>,
360 quality: Option<String>,
361 output_compression: Option<u8>,
362 response_format: Option<String>,
363 user: Option<String>,
364 extra: BTreeMap<String, Value>,
365}
366
367impl CreateImageEditRequestBuilder {
368 pub fn image(mut self, image: ImageInput) -> Self {
371 self.images.push(image);
372 self
373 }
374
375 pub fn images(mut self, images: impl IntoIterator<Item = ImageInput>) -> Self {
378 self.images = images.into_iter().collect();
379 self
380 }
381
382 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
385 self.prompt = Some(prompt.into());
386 self
387 }
388
389 pub fn model(mut self, model: impl Into<String>) -> Self {
392 self.model = Some(model.into());
393 self
394 }
395
396 pub fn n(mut self, n: u32) -> Self {
399 self.n = Some(n);
400 self
401 }
402
403 pub fn size(mut self, size: impl Into<String>) -> Self {
406 self.size = Some(size.into());
407 self
408 }
409
410 pub fn background(mut self, background: impl Into<String>) -> Self {
413 self.background = Some(background.into());
414 self
415 }
416
417 pub fn input_fidelity(mut self, input_fidelity: impl Into<String>) -> Self {
420 self.input_fidelity = Some(input_fidelity.into());
421 self
422 }
423
424 pub fn moderation(mut self, moderation: impl Into<String>) -> Self {
427 self.moderation = Some(moderation.into());
428 self
429 }
430
431 pub fn quality(mut self, quality: impl Into<String>) -> Self {
434 self.quality = Some(quality.into());
435 self
436 }
437
438 pub fn output_compression(mut self, output_compression: u8) -> Self {
441 self.output_compression = Some(output_compression);
442 self
443 }
444
445 pub fn response_format(mut self, response_format: impl Into<String>) -> Self {
448 self.response_format = Some(response_format.into());
449 self
450 }
451
452 pub fn user(mut self, user: impl Into<String>) -> Self {
455 self.user = Some(user.into());
456 self
457 }
458
459 pub fn extra(mut self, name: impl Into<String>, value: Value) -> Self {
462 self.extra.insert(name.into(), value);
463 self
464 }
465
466 pub fn build(self) -> Result<CreateImageEditRequest, LingerError> {
469 if self.images.is_empty() {
470 return Err(LingerError::invalid_config("images is required"));
471 }
472 for image in &self.images {
473 image.validate()?;
474 }
475 let prompt = self
476 .prompt
477 .filter(|value| !value.trim().is_empty())
478 .ok_or_else(|| LingerError::invalid_config("prompt is required"))?;
479 validate_optional_string("model", self.model.as_deref())?;
480 validate_optional_string("size", self.size.as_deref())?;
481 validate_optional_string("background", self.background.as_deref())?;
482 validate_optional_string("input_fidelity", self.input_fidelity.as_deref())?;
483 validate_optional_string("moderation", self.moderation.as_deref())?;
484 validate_optional_string("quality", self.quality.as_deref())?;
485 validate_optional_string("response_format", self.response_format.as_deref())?;
486 validate_optional_string("user", self.user.as_deref())?;
487 if self.n.is_some_and(|n| !(1..=10).contains(&n)) {
488 return Err(LingerError::invalid_config("n must be between 1 and 10"));
489 }
490 if self
491 .output_compression
492 .is_some_and(|output_compression| output_compression > 100)
493 {
494 return Err(LingerError::invalid_config(
495 "output_compression must be between 0 and 100",
496 ));
497 }
498 Ok(CreateImageEditRequest {
499 images: self.images,
500 prompt,
501 model: self.model,
502 n: self.n,
503 size: self.size,
504 background: self.background,
505 input_fidelity: self.input_fidelity,
506 moderation: self.moderation,
507 quality: self.quality,
508 output_compression: self.output_compression,
509 response_format: self.response_format,
510 user: self.user,
511 extra: self.extra,
512 })
513 }
514}
515
516#[derive(Clone, Debug, PartialEq, Eq)]
519#[non_exhaustive]
520pub struct ImageUpload {
521 pub filename: String,
524 pub content_type: String,
527 content: Bytes,
528}
529
530impl ImageUpload {
531 pub fn from_bytes(
534 filename: impl Into<String>,
535 content: impl Into<Bytes>,
536 ) -> Result<Self, LingerError> {
537 let filename = filename.into();
538 validate_header_param("filename", &filename)?;
539 Ok(Self {
540 filename,
541 content_type: "application/octet-stream".to_string(),
542 content: content.into(),
543 })
544 }
545
546 pub fn content_type(mut self, content_type: impl Into<String>) -> Result<Self, LingerError> {
549 let content_type = content_type.into();
550 validate_header_value("content_type", &content_type)?;
551 self.content_type = content_type;
552 Ok(self)
553 }
554
555 pub fn bytes(&self) -> Bytes {
558 self.content.clone()
559 }
560}
561
562#[derive(Clone, Debug, PartialEq)]
565#[non_exhaustive]
566pub struct CreateImageVariationRequest {
567 pub image: ImageUpload,
570 pub model: Option<String>,
573 pub n: Option<u32>,
576 pub response_format: Option<String>,
579 pub size: Option<String>,
582 pub user: Option<String>,
585}
586
587impl CreateImageVariationRequest {
588 pub fn builder() -> CreateImageVariationRequestBuilder {
591 CreateImageVariationRequestBuilder::default()
592 }
593
594 pub(crate) fn apply_multipart_body(&self, request: &mut HttpRequest) {
595 let boundary = multipart_boundary(&self.image.content);
596 request.insert_header(
597 "content-type",
598 format!("multipart/form-data; boundary={boundary}"),
599 );
600 request.set_body_stream(self.multipart_stream(boundary));
601 }
602
603 fn multipart_stream(
604 &self,
605 boundary: String,
606 ) -> impl futures_core::Stream<Item = Result<Bytes, LingerError>> {
607 let mut chunks = Vec::new();
608 push_optional_text_field(&mut chunks, &boundary, "model", self.model.as_deref());
609 push_optional_text_field(&mut chunks, &boundary, "size", self.size.as_deref());
610 push_optional_text_field(
611 &mut chunks,
612 &boundary,
613 "response_format",
614 self.response_format.as_deref(),
615 );
616 push_optional_text_field(&mut chunks, &boundary, "user", self.user.as_deref());
617 if let Some(n) = self.n {
618 push_text_field(&mut chunks, &boundary, "n", &n.to_string());
619 }
620 chunks.push(Ok(Bytes::from(format!(
621 "--{boundary}\r\nContent-Disposition: form-data; name=\"image\"; filename=\"{}\"\r\nContent-Type: {}\r\n\r\n",
622 escape_multipart_param(&self.image.filename),
623 self.image.content_type
624 ))));
625 chunks.push(Ok(self.image.content.clone()));
626 chunks.push(Ok(Bytes::from(format!("\r\n--{boundary}--\r\n"))));
627 futures_util::stream::iter(chunks)
628 }
629}
630
631#[derive(Clone, Debug, Default)]
634#[non_exhaustive]
635pub struct CreateImageVariationRequestBuilder {
636 image: Option<ImageUpload>,
637 model: Option<String>,
638 n: Option<u32>,
639 response_format: Option<String>,
640 size: Option<String>,
641 user: Option<String>,
642}
643
644impl CreateImageVariationRequestBuilder {
645 pub fn image(mut self, image: ImageUpload) -> Self {
648 self.image = Some(image);
649 self
650 }
651
652 pub fn model(mut self, model: impl Into<String>) -> Self {
655 self.model = Some(model.into());
656 self
657 }
658
659 pub fn n(mut self, n: u32) -> Self {
662 self.n = Some(n);
663 self
664 }
665
666 pub fn response_format(mut self, response_format: impl Into<String>) -> Self {
669 self.response_format = Some(response_format.into());
670 self
671 }
672
673 pub fn size(mut self, size: impl Into<String>) -> Self {
676 self.size = Some(size.into());
677 self
678 }
679
680 pub fn user(mut self, user: impl Into<String>) -> Self {
683 self.user = Some(user.into());
684 self
685 }
686
687 pub fn build(self) -> Result<CreateImageVariationRequest, LingerError> {
690 let image = self
691 .image
692 .ok_or_else(|| LingerError::invalid_config("image is required"))?;
693 validate_optional_string("model", self.model.as_deref())?;
694 validate_optional_string("response_format", self.response_format.as_deref())?;
695 validate_optional_string("size", self.size.as_deref())?;
696 validate_optional_string("user", self.user.as_deref())?;
697 if self.n.is_some_and(|n| !(1..=10).contains(&n)) {
698 return Err(LingerError::invalid_config("n must be between 1 and 10"));
699 }
700 Ok(CreateImageVariationRequest {
701 image,
702 model: self.model,
703 n: self.n,
704 response_format: self.response_format,
705 size: self.size,
706 user: self.user,
707 })
708 }
709}
710
711#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
714#[non_exhaustive]
715pub struct ImagesResponse {
716 pub created: u64,
719 #[serde(default)]
722 pub data: Vec<Image>,
723 #[serde(default)]
726 pub background: Option<String>,
727 #[serde(default)]
730 pub output_format: Option<String>,
731 #[serde(default)]
734 pub quality: Option<String>,
735 #[serde(default)]
738 pub size: Option<String>,
739 #[serde(default)]
742 pub usage: Option<ImageUsage>,
743 #[serde(flatten)]
746 pub extra: BTreeMap<String, Value>,
747 #[serde(skip)]
750 request_id: Option<RequestId>,
751}
752
753impl ImagesResponse {
754 pub(crate) fn with_request_id(mut self, request_id: Option<RequestId>) -> Self {
755 self.request_id = request_id;
756 self
757 }
758
759 pub fn request_id(&self) -> Option<&RequestId> {
762 self.request_id.as_ref()
763 }
764}
765
766#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
769#[non_exhaustive]
770pub struct Image {
771 #[serde(default)]
774 pub url: Option<String>,
775 #[serde(default)]
778 pub b64_json: Option<String>,
779 #[serde(default)]
782 pub revised_prompt: Option<String>,
783 #[serde(flatten)]
786 pub extra: BTreeMap<String, Value>,
787}
788
789#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
792#[non_exhaustive]
793pub struct ImageUsage {
794 pub input_tokens: u64,
797 pub output_tokens: u64,
800 pub total_tokens: u64,
803 #[serde(default)]
806 pub input_tokens_details: ImageTokenDetails,
807 #[serde(default)]
810 pub output_tokens_details: ImageTokenDetails,
811}
812
813#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
816#[non_exhaustive]
817pub struct ImageTokenDetails {
818 #[serde(default)]
821 pub text_tokens: Option<u64>,
822 #[serde(default)]
825 pub image_tokens: Option<u64>,
826}
827
828fn validate_optional_string(name: &str, value: Option<&str>) -> Result<(), LingerError> {
829 if value.is_some_and(|value| value.trim().is_empty()) {
830 return Err(LingerError::invalid_config(format!(
831 "{name} must not be empty"
832 )));
833 }
834 Ok(())
835}
836
837fn push_text_field(
838 chunks: &mut Vec<Result<Bytes, LingerError>>,
839 boundary: &str,
840 name: &str,
841 value: &str,
842) {
843 chunks.push(Ok(Bytes::from(format!(
844 "--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"\r\n\r\n{value}\r\n"
845 ))));
846}
847
848fn push_optional_text_field(
849 chunks: &mut Vec<Result<Bytes, LingerError>>,
850 boundary: &str,
851 name: &str,
852 value: Option<&str>,
853) {
854 if let Some(value) = value {
855 push_text_field(chunks, boundary, name, value);
856 }
857}
858
859fn multipart_boundary(content: &Bytes) -> String {
860 for counter in 0.. {
861 let boundary = format!("linger-openai-sdk-image-boundary-{counter}");
862 if !contains_bytes(content, boundary.as_bytes()) {
863 return boundary;
864 }
865 }
866 unreachable!("unbounded boundary counter")
867}
868
869fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
870 if needle.is_empty() {
871 return true;
872 }
873 haystack
874 .windows(needle.len())
875 .any(|window| window == needle)
876}
877
878fn validate_header_param(name: &str, value: &str) -> Result<(), LingerError> {
879 if value.trim().is_empty() {
880 return Err(LingerError::invalid_config(format!("{name} is required")));
881 }
882 validate_header_value(name, value)
883}
884
885fn validate_header_value(name: &str, value: &str) -> Result<(), LingerError> {
886 if value.contains('\r') || value.contains('\n') {
887 return Err(LingerError::invalid_config(format!(
888 "{name} must not contain CR or LF"
889 )));
890 }
891 Ok(())
892}
893
894fn escape_multipart_param(value: &str) -> String {
895 value.replace('\\', "\\\\").replace('"', "\\\"")
896}