openai_ergonomic/builders/
images.rs

1//! Image-generation API builders.
2//!
3//! This module offers ergonomic wrappers around `openai-client-base`'s image
4//! endpoints. Builders provide fluent setters, lightweight validation, and
5//! produce request types that can be supplied directly to the generated client
6//! functions.
7
8use 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/// Builder for image generation (`/images/generations`).
20#[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    /// Create a new generation builder with the required prompt text.
41    #[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    /// Override the model (`gpt-image-1`, `dall-e-3`, etc.).
62    #[must_use]
63    pub fn model(mut self, model: impl Into<String>) -> Self {
64        self.model = Some(model.into());
65        self
66    }
67
68    /// Set how many images to generate (1-10).
69    #[must_use]
70    pub fn n(mut self, n: i32) -> Self {
71        self.n = Some(n);
72        self
73    }
74
75    /// Select image quality.
76    #[must_use]
77    pub fn quality(mut self, quality: Quality) -> Self {
78        self.quality = Some(quality);
79        self
80    }
81
82    /// Select the response format for `dall-e-*` models.
83    #[must_use]
84    pub fn response_format(mut self, format: ResponseFormat) -> Self {
85        self.response_format = Some(format);
86        self
87    }
88
89    /// Choose the binary output format (only supported for `gpt-image-1`).
90    #[must_use]
91    pub fn output_format(mut self, format: OutputFormat) -> Self {
92        self.output_format = Some(format);
93        self
94    }
95
96    /// Tune image compression (0-100). Only applies to JPEG/WEBP outputs.
97    #[must_use]
98    pub fn output_compression(mut self, compression: i32) -> Self {
99        self.output_compression = Some(compression);
100        self
101    }
102
103    /// Enable streaming responses.
104    #[must_use]
105    pub fn stream(mut self, stream: bool) -> Self {
106        self.stream = Some(stream);
107        self
108    }
109
110    /// Configure the number of partial images to emit when streaming (0-3).
111    #[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    /// Select the output size preset.
118    #[must_use]
119    pub fn size(mut self, size: Size) -> Self {
120        self.size = Some(size);
121        self
122    }
123
124    /// Control content moderation (`auto`/`low`).
125    #[must_use]
126    pub fn moderation(mut self, moderation: Moderation) -> Self {
127        self.moderation = Some(moderation);
128        self
129    }
130
131    /// Configure transparent/opaque backgrounds.
132    #[must_use]
133    pub fn background(mut self, background: Background) -> Self {
134        self.background = Some(background);
135        self
136    }
137
138    /// Select stylistic hints supported by `dall-e-3`.
139    #[must_use]
140    pub fn style(mut self, style: Style) -> Self {
141        self.style = Some(style);
142        self
143    }
144
145    /// Attach an end-user identifier for abuse monitoring.
146    #[must_use]
147    pub fn user(mut self, user: impl Into<String>) -> Self {
148        self.user = Some(user.into());
149        self
150    }
151
152    /// Borrow the configured prompt.
153    #[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/// Builder describing an image edit request (`/images/edits`).
205#[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    /// Create a new edit request using a base image and prompt.
226    #[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    /// Supply a mask file that indicates editable regions.
248    #[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    /// Control the generated background (`transparent`, `opaque`, ... as string).
255    #[must_use]
256    pub fn background(mut self, background: impl Into<String>) -> Self {
257        self.background = Some(background.into());
258        self
259    }
260
261    /// Override the model (defaults to `gpt-image-1`).
262    #[must_use]
263    pub fn model(mut self, model: impl Into<String>) -> Self {
264        self.model = Some(model.into());
265        self
266    }
267
268    /// Configure the number of images to generate (1-10).
269    #[must_use]
270    pub fn n(mut self, n: i32) -> Self {
271        self.n = Some(n);
272        self
273    }
274
275    /// Set the output size (e.g. `1024x1024`).
276    #[must_use]
277    pub fn size(mut self, size: impl Into<String>) -> Self {
278        self.size = Some(size.into());
279        self
280    }
281
282    /// Choose the response format (`url`, `b64_json`).
283    #[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    /// Choose the binary output format (`png`, `jpeg`, `webp`).
290    #[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    /// Configure output compression (0-100).
297    #[must_use]
298    pub fn output_compression(mut self, compression: i32) -> Self {
299        self.output_compression = Some(compression);
300        self
301    }
302
303    /// Attach an end-user identifier.
304    #[must_use]
305    pub fn user(mut self, user: impl Into<String>) -> Self {
306        self.user = Some(user.into());
307        self
308    }
309
310    /// Control fidelity for the input image (`low`/`high`).
311    #[must_use]
312    pub fn input_fidelity(mut self, fidelity: ImageInputFidelity) -> Self {
313        self.input_fidelity = Some(fidelity);
314        self
315    }
316
317    /// Enable streaming responses for edits.
318    #[must_use]
319    pub fn stream(mut self, stream: bool) -> Self {
320        self.stream = Some(stream);
321        self
322    }
323
324    /// Configure partial image count when streaming (0-3).
325    #[must_use]
326    pub fn partial_images(mut self, value: i32) -> Self {
327        self.partial_images = Some(value);
328        self
329    }
330
331    /// Provide quality hints (`low`, `medium`, `high`, ...).
332    #[must_use]
333    pub fn quality(mut self, quality: impl Into<String>) -> Self {
334        self.quality = Some(quality.into());
335        self
336    }
337
338    /// Borrow the underlying image path.
339    #[must_use]
340    pub fn image(&self) -> &Path {
341        &self.image
342    }
343
344    /// Borrow the edit prompt.
345    #[must_use]
346    pub fn prompt(&self) -> &str {
347        &self.prompt
348    }
349}
350
351/// Fully prepared payload for the edit endpoint.
352#[derive(Debug, Clone)]
353pub struct ImageEditRequest {
354    /// Path to the original image that will be edited.
355    pub image: PathBuf,
356    /// Natural-language instructions describing the edit.
357    pub prompt: String,
358    /// Optional mask describing editable regions.
359    pub mask: Option<PathBuf>,
360    /// Optional background mode (`transparent`, `opaque`, ...).
361    pub background: Option<String>,
362    /// Model identifier to use for the edit operation.
363    pub model: Option<String>,
364    /// Number of images to generate (1-10).
365    pub n: Option<i32>,
366    /// Requested output size (e.g. `1024x1024`).
367    pub size: Option<String>,
368    /// Response format for non-streaming outputs (`url`, `b64_json`).
369    pub response_format: Option<String>,
370    /// Binary output format (`png`, `jpeg`, `webp`).
371    pub output_format: Option<String>,
372    /// Compression level for JPEG/WEBP outputs (0-100).
373    pub output_compression: Option<i32>,
374    /// End-user identifier for abuse monitoring.
375    pub user: Option<String>,
376    /// Fidelity configuration for how closely to follow the input image.
377    pub input_fidelity: Option<ImageInputFidelity>,
378    /// Whether to stream incremental results.
379    pub stream: Option<bool>,
380    /// Number of partial images to emit while streaming.
381    pub partial_images: Option<i32>,
382    /// Additional quality hints accepted by the service.
383    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/// Builder describing an image variation request (`/images/variations`).
433#[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    /// Create a variation builder for the provided image file.
445    #[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    /// Override the variation model.
458    #[must_use]
459    pub fn model(mut self, model: impl Into<String>) -> Self {
460        self.model = Some(model.into());
461        self
462    }
463
464    /// Number of variations to generate (1-10).
465    #[must_use]
466    pub fn n(mut self, n: i32) -> Self {
467        self.n = Some(n);
468        self
469    }
470
471    /// Choose the response format (`url`, `b64_json`).
472    #[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    /// Select the image size preset.
479    #[must_use]
480    pub fn size(mut self, size: impl Into<String>) -> Self {
481        self.size = Some(size.into());
482        self
483    }
484
485    /// Attach an end-user identifier.
486    #[must_use]
487    pub fn user(mut self, user: impl Into<String>) -> Self {
488        self.user = Some(user.into());
489        self
490    }
491
492    /// Borrow the base image path.
493    #[must_use]
494    pub fn image(&self) -> &Path {
495        &self.image
496    }
497}
498
499/// Fully prepared payload for the variation endpoint.
500#[derive(Debug, Clone)]
501pub struct ImageVariationRequest {
502    /// Path to the source image to transform.
503    pub image: PathBuf,
504    /// Optional model override.
505    pub model: Option<String>,
506    /// Number of variations to create (1-10).
507    pub n: Option<i32>,
508    /// Response format (`url`, `b64_json`).
509    pub response_format: Option<String>,
510    /// Output size (e.g. `512x512`).
511    pub size: Option<String>,
512    /// End-user identifier for abuse monitoring.
513    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}