1use std::path::{Path, PathBuf};
9
10pub use openai_client_base::models::create_image_request::{
11 Background, Moderation, OutputFormat, Quality, ResponseFormat, Size, Style,
12};
13pub use openai_client_base::models::{
14 image_input_fidelity::ImageInputFidelityTextVariantEnum, CreateImageRequest, ImageInputFidelity,
15};
16
17use crate::{Builder, Error, Result};
18
19#[derive(Debug, Clone)]
21pub struct ImageGenerationBuilder {
22 prompt: String,
23 model: Option<String>,
24 n: Option<i32>,
25 quality: Option<Quality>,
26 response_format: Option<ResponseFormat>,
27 output_format: Option<OutputFormat>,
28 output_compression: Option<i32>,
29 stream: Option<bool>,
30 #[allow(clippy::option_option)]
31 partial_images: Option<Option<i32>>,
32 size: Option<Size>,
33 moderation: Option<Moderation>,
34 background: Option<Background>,
35 style: Option<Style>,
36 user: Option<String>,
37}
38
39impl ImageGenerationBuilder {
40 #[must_use]
42 pub fn new(prompt: impl Into<String>) -> Self {
43 Self {
44 prompt: prompt.into(),
45 model: None,
46 n: None,
47 quality: None,
48 response_format: None,
49 output_format: None,
50 output_compression: None,
51 stream: None,
52 partial_images: None,
53 size: None,
54 moderation: None,
55 background: None,
56 style: None,
57 user: None,
58 }
59 }
60
61 #[must_use]
63 pub fn model(mut self, model: impl Into<String>) -> Self {
64 self.model = Some(model.into());
65 self
66 }
67
68 #[must_use]
70 pub fn n(mut self, n: i32) -> Self {
71 self.n = Some(n);
72 self
73 }
74
75 #[must_use]
77 pub fn quality(mut self, quality: Quality) -> Self {
78 self.quality = Some(quality);
79 self
80 }
81
82 #[must_use]
84 pub fn response_format(mut self, format: ResponseFormat) -> Self {
85 self.response_format = Some(format);
86 self
87 }
88
89 #[must_use]
91 pub fn output_format(mut self, format: OutputFormat) -> Self {
92 self.output_format = Some(format);
93 self
94 }
95
96 #[must_use]
98 pub fn output_compression(mut self, compression: i32) -> Self {
99 self.output_compression = Some(compression);
100 self
101 }
102
103 #[must_use]
105 pub fn stream(mut self, stream: bool) -> Self {
106 self.stream = Some(stream);
107 self
108 }
109
110 #[must_use]
112 pub fn partial_images(mut self, partial_images: Option<i32>) -> Self {
113 self.partial_images = Some(partial_images);
114 self
115 }
116
117 #[must_use]
119 pub fn size(mut self, size: Size) -> Self {
120 self.size = Some(size);
121 self
122 }
123
124 #[must_use]
126 pub fn moderation(mut self, moderation: Moderation) -> Self {
127 self.moderation = Some(moderation);
128 self
129 }
130
131 #[must_use]
133 pub fn background(mut self, background: Background) -> Self {
134 self.background = Some(background);
135 self
136 }
137
138 #[must_use]
140 pub fn style(mut self, style: Style) -> Self {
141 self.style = Some(style);
142 self
143 }
144
145 #[must_use]
147 pub fn user(mut self, user: impl Into<String>) -> Self {
148 self.user = Some(user.into());
149 self
150 }
151
152 #[must_use]
154 pub fn prompt(&self) -> &str {
155 &self.prompt
156 }
157}
158
159impl Builder<CreateImageRequest> for ImageGenerationBuilder {
160 fn build(self) -> Result<CreateImageRequest> {
161 if let Some(n) = self.n {
162 if !(1..=10).contains(&n) {
163 return Err(Error::InvalidRequest(format!(
164 "Image generation `n` must be between 1 and 10 (got {n})"
165 )));
166 }
167 }
168
169 if let Some(Some(partial)) = self.partial_images {
170 if !(0..=3).contains(&partial) {
171 return Err(Error::InvalidRequest(format!(
172 "Partial image count must be between 0 and 3 (got {partial})"
173 )));
174 }
175 }
176
177 if let Some(compression) = self.output_compression {
178 if !(0..=100).contains(&compression) {
179 return Err(Error::InvalidRequest(format!(
180 "Output compression must be between 0 and 100 (got {compression})"
181 )));
182 }
183 }
184
185 Ok(CreateImageRequest {
186 prompt: self.prompt,
187 model: self.model,
188 n: self.n,
189 quality: self.quality,
190 response_format: self.response_format,
191 output_format: self.output_format,
192 output_compression: self.output_compression,
193 stream: self.stream,
194 partial_images: self.partial_images,
195 size: self.size,
196 moderation: self.moderation,
197 background: self.background,
198 style: self.style,
199 user: self.user,
200 })
201 }
202}
203
204#[derive(Debug, Clone)]
206pub struct ImageEditBuilder {
207 image: PathBuf,
208 prompt: String,
209 mask: Option<PathBuf>,
210 background: Option<String>,
211 model: Option<String>,
212 n: Option<i32>,
213 size: Option<String>,
214 response_format: Option<String>,
215 output_format: Option<String>,
216 output_compression: Option<i32>,
217 user: Option<String>,
218 input_fidelity: Option<ImageInputFidelity>,
219 stream: Option<bool>,
220 partial_images: Option<i32>,
221 quality: Option<String>,
222}
223
224impl ImageEditBuilder {
225 #[must_use]
227 pub fn new(image: impl AsRef<Path>, prompt: impl Into<String>) -> Self {
228 Self {
229 image: image.as_ref().to_path_buf(),
230 prompt: prompt.into(),
231 mask: None,
232 background: None,
233 model: None,
234 n: None,
235 size: None,
236 response_format: None,
237 output_format: None,
238 output_compression: None,
239 user: None,
240 input_fidelity: None,
241 stream: None,
242 partial_images: None,
243 quality: None,
244 }
245 }
246
247 #[must_use]
249 pub fn mask(mut self, mask: impl AsRef<Path>) -> Self {
250 self.mask = Some(mask.as_ref().to_path_buf());
251 self
252 }
253
254 #[must_use]
256 pub fn background(mut self, background: impl Into<String>) -> Self {
257 self.background = Some(background.into());
258 self
259 }
260
261 #[must_use]
263 pub fn model(mut self, model: impl Into<String>) -> Self {
264 self.model = Some(model.into());
265 self
266 }
267
268 #[must_use]
270 pub fn n(mut self, n: i32) -> Self {
271 self.n = Some(n);
272 self
273 }
274
275 #[must_use]
277 pub fn size(mut self, size: impl Into<String>) -> Self {
278 self.size = Some(size.into());
279 self
280 }
281
282 #[must_use]
284 pub fn response_format(mut self, format: impl Into<String>) -> Self {
285 self.response_format = Some(format.into());
286 self
287 }
288
289 #[must_use]
291 pub fn output_format(mut self, format: impl Into<String>) -> Self {
292 self.output_format = Some(format.into());
293 self
294 }
295
296 #[must_use]
298 pub fn output_compression(mut self, compression: i32) -> Self {
299 self.output_compression = Some(compression);
300 self
301 }
302
303 #[must_use]
305 pub fn user(mut self, user: impl Into<String>) -> Self {
306 self.user = Some(user.into());
307 self
308 }
309
310 #[must_use]
312 pub fn input_fidelity(mut self, fidelity: ImageInputFidelity) -> Self {
313 self.input_fidelity = Some(fidelity);
314 self
315 }
316
317 #[must_use]
319 pub fn stream(mut self, stream: bool) -> Self {
320 self.stream = Some(stream);
321 self
322 }
323
324 #[must_use]
326 pub fn partial_images(mut self, value: i32) -> Self {
327 self.partial_images = Some(value);
328 self
329 }
330
331 #[must_use]
333 pub fn quality(mut self, quality: impl Into<String>) -> Self {
334 self.quality = Some(quality.into());
335 self
336 }
337
338 #[must_use]
340 pub fn image(&self) -> &Path {
341 &self.image
342 }
343
344 #[must_use]
346 pub fn prompt(&self) -> &str {
347 &self.prompt
348 }
349}
350
351#[derive(Debug, Clone)]
353pub struct ImageEditRequest {
354 pub image: PathBuf,
356 pub prompt: String,
358 pub mask: Option<PathBuf>,
360 pub background: Option<String>,
362 pub model: Option<String>,
364 pub n: Option<i32>,
366 pub size: Option<String>,
368 pub response_format: Option<String>,
370 pub output_format: Option<String>,
372 pub output_compression: Option<i32>,
374 pub user: Option<String>,
376 pub input_fidelity: Option<ImageInputFidelity>,
378 pub stream: Option<bool>,
380 pub partial_images: Option<i32>,
382 pub quality: Option<String>,
384}
385
386impl Builder<ImageEditRequest> for ImageEditBuilder {
387 fn build(self) -> Result<ImageEditRequest> {
388 if let Some(n) = self.n {
389 if !(1..=10).contains(&n) {
390 return Err(Error::InvalidRequest(format!(
391 "Image edit `n` must be between 1 and 10 (got {n})"
392 )));
393 }
394 }
395
396 if let Some(compression) = self.output_compression {
397 if !(0..=100).contains(&compression) {
398 return Err(Error::InvalidRequest(format!(
399 "Output compression must be between 0 and 100 (got {compression})"
400 )));
401 }
402 }
403
404 if let Some(partial) = self.partial_images {
405 if !(0..=3).contains(&partial) {
406 return Err(Error::InvalidRequest(format!(
407 "Partial image count must be between 0 and 3 (got {partial})"
408 )));
409 }
410 }
411
412 Ok(ImageEditRequest {
413 image: self.image,
414 prompt: self.prompt,
415 mask: self.mask,
416 background: self.background,
417 model: self.model,
418 n: self.n,
419 size: self.size,
420 response_format: self.response_format,
421 output_format: self.output_format,
422 output_compression: self.output_compression,
423 user: self.user,
424 input_fidelity: self.input_fidelity,
425 stream: self.stream,
426 partial_images: self.partial_images,
427 quality: self.quality,
428 })
429 }
430}
431
432#[derive(Debug, Clone)]
434pub struct ImageVariationBuilder {
435 image: PathBuf,
436 model: Option<String>,
437 n: Option<i32>,
438 response_format: Option<String>,
439 size: Option<String>,
440 user: Option<String>,
441}
442
443impl ImageVariationBuilder {
444 #[must_use]
446 pub fn new(image: impl AsRef<Path>) -> Self {
447 Self {
448 image: image.as_ref().to_path_buf(),
449 model: None,
450 n: None,
451 response_format: None,
452 size: None,
453 user: None,
454 }
455 }
456
457 #[must_use]
459 pub fn model(mut self, model: impl Into<String>) -> Self {
460 self.model = Some(model.into());
461 self
462 }
463
464 #[must_use]
466 pub fn n(mut self, n: i32) -> Self {
467 self.n = Some(n);
468 self
469 }
470
471 #[must_use]
473 pub fn response_format(mut self, format: impl Into<String>) -> Self {
474 self.response_format = Some(format.into());
475 self
476 }
477
478 #[must_use]
480 pub fn size(mut self, size: impl Into<String>) -> Self {
481 self.size = Some(size.into());
482 self
483 }
484
485 #[must_use]
487 pub fn user(mut self, user: impl Into<String>) -> Self {
488 self.user = Some(user.into());
489 self
490 }
491
492 #[must_use]
494 pub fn image(&self) -> &Path {
495 &self.image
496 }
497}
498
499#[derive(Debug, Clone)]
501pub struct ImageVariationRequest {
502 pub image: PathBuf,
504 pub model: Option<String>,
506 pub n: Option<i32>,
508 pub response_format: Option<String>,
510 pub size: Option<String>,
512 pub user: Option<String>,
514}
515
516impl Builder<ImageVariationRequest> for ImageVariationBuilder {
517 fn build(self) -> Result<ImageVariationRequest> {
518 if let Some(n) = self.n {
519 if !(1..=10).contains(&n) {
520 return Err(Error::InvalidRequest(format!(
521 "Image variation `n` must be between 1 and 10 (got {n})"
522 )));
523 }
524 }
525
526 Ok(ImageVariationRequest {
527 image: self.image,
528 model: self.model,
529 n: self.n,
530 response_format: self.response_format,
531 size: self.size,
532 user: self.user,
533 })
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540
541 #[test]
542 fn builds_image_generation_request() {
543 let request = ImageGenerationBuilder::new("A scenic valley at sunrise")
544 .model("gpt-image-1")
545 .n(2)
546 .quality(Quality::High)
547 .response_format(ResponseFormat::B64Json)
548 .output_format(OutputFormat::Webp)
549 .output_compression(80)
550 .stream(true)
551 .partial_images(Some(2))
552 .size(Size::Variant1536x1024)
553 .moderation(Moderation::Auto)
554 .background(Background::Transparent)
555 .style(Style::Vivid)
556 .user("example-user")
557 .build()
558 .expect("valid generation builder");
559
560 assert_eq!(request.prompt, "A scenic valley at sunrise");
561 assert_eq!(request.model.as_deref(), Some("gpt-image-1"));
562 assert_eq!(request.n, Some(2));
563 assert_eq!(request.quality, Some(Quality::High));
564 assert_eq!(request.response_format, Some(ResponseFormat::B64Json));
565 assert_eq!(request.output_format, Some(OutputFormat::Webp));
566 assert_eq!(request.output_compression, Some(80));
567 assert_eq!(request.stream, Some(true));
568 assert_eq!(request.partial_images, Some(Some(2)));
569 assert_eq!(request.size, Some(Size::Variant1536x1024));
570 assert_eq!(request.moderation, Some(Moderation::Auto));
571 assert_eq!(request.background, Some(Background::Transparent));
572 assert_eq!(request.style, Some(Style::Vivid));
573 assert_eq!(request.user.as_deref(), Some("example-user"));
574 }
575
576 #[test]
577 fn generation_validates_ranges() {
578 let err = ImageGenerationBuilder::new("Prompt")
579 .n(0)
580 .build()
581 .unwrap_err();
582 assert!(matches!(err, Error::InvalidRequest(_)));
583
584 let err = ImageGenerationBuilder::new("Prompt")
585 .output_compression(150)
586 .build()
587 .unwrap_err();
588 assert!(matches!(err, Error::InvalidRequest(_)));
589
590 let err = ImageGenerationBuilder::new("Prompt")
591 .partial_images(Some(5))
592 .build()
593 .unwrap_err();
594 assert!(matches!(err, Error::InvalidRequest(_)));
595 }
596
597 #[test]
598 fn builds_image_edit_request() {
599 let request = ImageEditBuilder::new("image.png", "Remove the background")
600 .mask("mask.png")
601 .background("transparent")
602 .model("gpt-image-1")
603 .n(1)
604 .size("1024x1024")
605 .response_format("b64_json")
606 .output_format("png")
607 .output_compression(90)
608 .user("user-1")
609 .input_fidelity(ImageInputFidelity::TextVariant(
610 ImageInputFidelityTextVariantEnum::High,
611 ))
612 .stream(true)
613 .partial_images(1)
614 .quality("standard")
615 .build()
616 .expect("valid edit builder");
617
618 assert_eq!(request.image, PathBuf::from("image.png"));
619 assert_eq!(request.prompt, "Remove the background");
620 assert_eq!(request.mask, Some(PathBuf::from("mask.png")));
621 assert_eq!(request.background.as_deref(), Some("transparent"));
622 assert_eq!(request.model.as_deref(), Some("gpt-image-1"));
623 assert_eq!(request.size.as_deref(), Some("1024x1024"));
624 assert_eq!(request.response_format.as_deref(), Some("b64_json"));
625 assert_eq!(request.output_format.as_deref(), Some("png"));
626 assert_eq!(request.output_compression, Some(90));
627 assert_eq!(request.stream, Some(true));
628 assert_eq!(request.partial_images, Some(1));
629 }
630
631 #[test]
632 fn edit_validates_ranges() {
633 let err = ImageEditBuilder::new("image.png", "Prompt")
634 .n(20)
635 .build()
636 .unwrap_err();
637 assert!(matches!(err, Error::InvalidRequest(_)));
638
639 let err = ImageEditBuilder::new("image.png", "Prompt")
640 .output_compression(150)
641 .build()
642 .unwrap_err();
643 assert!(matches!(err, Error::InvalidRequest(_)));
644
645 let err = ImageEditBuilder::new("image.png", "Prompt")
646 .partial_images(5)
647 .build()
648 .unwrap_err();
649 assert!(matches!(err, Error::InvalidRequest(_)));
650 }
651
652 #[test]
653 fn builds_image_variation_request() {
654 let request = ImageVariationBuilder::new("image.png")
655 .model("dall-e-2")
656 .n(3)
657 .response_format("url")
658 .size("512x512")
659 .user("user-123")
660 .build()
661 .expect("valid variation builder");
662
663 assert_eq!(request.image, PathBuf::from("image.png"));
664 assert_eq!(request.model.as_deref(), Some("dall-e-2"));
665 assert_eq!(request.n, Some(3));
666 assert_eq!(request.response_format.as_deref(), Some("url"));
667 assert_eq!(request.size.as_deref(), Some("512x512"));
668 }
669
670 #[test]
671 fn variation_validates_n() {
672 let err = ImageVariationBuilder::new("image.png")
673 .n(0)
674 .build()
675 .unwrap_err();
676 assert!(matches!(err, Error::InvalidRequest(_)));
677 }
678}