use std::path::{Path, PathBuf};
pub use openai_client_base::models::create_image_request::{
Background, Moderation, OutputFormat, Quality, ResponseFormat, Size, Style,
};
pub use openai_client_base::models::{CreateImageRequest, InputFidelity};
pub type ImageInputFidelity = InputFidelity;
use crate::{Builder, Error, Result};
#[derive(Debug, Clone)]
pub struct ImageGenerationBuilder {
prompt: String,
model: Option<String>,
n: Option<i32>,
quality: Option<Quality>,
response_format: Option<ResponseFormat>,
output_format: Option<OutputFormat>,
output_compression: Option<i32>,
stream: Option<bool>,
#[allow(clippy::option_option)]
partial_images: Option<Option<i32>>,
size: Option<Size>,
moderation: Option<Moderation>,
background: Option<Background>,
style: Option<Style>,
user: Option<String>,
}
impl ImageGenerationBuilder {
#[must_use]
pub fn new(prompt: impl Into<String>) -> Self {
Self {
prompt: prompt.into(),
model: None,
n: None,
quality: None,
response_format: None,
output_format: None,
output_compression: None,
stream: None,
partial_images: None,
size: None,
moderation: None,
background: None,
style: None,
user: None,
}
}
#[must_use]
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
#[must_use]
pub fn n(mut self, n: i32) -> Self {
self.n = Some(n);
self
}
#[must_use]
pub fn quality(mut self, quality: Quality) -> Self {
self.quality = Some(quality);
self
}
#[must_use]
pub fn response_format(mut self, format: ResponseFormat) -> Self {
self.response_format = Some(format);
self
}
#[must_use]
pub fn output_format(mut self, format: OutputFormat) -> Self {
self.output_format = Some(format);
self
}
#[must_use]
pub fn output_compression(mut self, compression: i32) -> Self {
self.output_compression = Some(compression);
self
}
#[must_use]
pub fn stream(mut self, stream: bool) -> Self {
self.stream = Some(stream);
self
}
#[must_use]
pub fn partial_images(mut self, partial_images: Option<i32>) -> Self {
self.partial_images = Some(partial_images);
self
}
#[must_use]
pub fn size(mut self, size: Size) -> Self {
self.size = Some(size);
self
}
#[must_use]
pub fn moderation(mut self, moderation: Moderation) -> Self {
self.moderation = Some(moderation);
self
}
#[must_use]
pub fn background(mut self, background: Background) -> Self {
self.background = Some(background);
self
}
#[must_use]
pub fn style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
#[must_use]
pub fn user(mut self, user: impl Into<String>) -> Self {
self.user = Some(user.into());
self
}
#[must_use]
pub fn prompt(&self) -> &str {
&self.prompt
}
}
impl Builder<CreateImageRequest> for ImageGenerationBuilder {
fn build(self) -> Result<CreateImageRequest> {
if let Some(n) = self.n {
if !(1..=10).contains(&n) {
return Err(Error::InvalidRequest(format!(
"Image generation `n` must be between 1 and 10 (got {n})"
)));
}
}
if let Some(Some(partial)) = self.partial_images {
if !(0..=3).contains(&partial) {
return Err(Error::InvalidRequest(format!(
"Partial image count must be between 0 and 3 (got {partial})"
)));
}
}
if let Some(compression) = self.output_compression {
if !(0..=100).contains(&compression) {
return Err(Error::InvalidRequest(format!(
"Output compression must be between 0 and 100 (got {compression})"
)));
}
}
Ok(CreateImageRequest {
prompt: self.prompt,
model: self.model,
n: self.n,
quality: self.quality,
response_format: self.response_format,
output_format: self.output_format,
output_compression: self.output_compression,
stream: self.stream,
partial_images: self.partial_images,
size: self.size,
moderation: self.moderation,
background: self.background,
style: self.style,
user: self.user,
})
}
}
#[derive(Debug, Clone)]
pub struct ImageEditBuilder {
image: PathBuf,
prompt: String,
mask: Option<PathBuf>,
background: Option<String>,
model: Option<String>,
n: Option<i32>,
size: Option<String>,
response_format: Option<String>,
output_format: Option<String>,
output_compression: Option<i32>,
user: Option<String>,
input_fidelity: Option<ImageInputFidelity>,
stream: Option<bool>,
partial_images: Option<i32>,
quality: Option<String>,
}
impl ImageEditBuilder {
#[must_use]
pub fn new(image: impl AsRef<Path>, prompt: impl Into<String>) -> Self {
Self {
image: image.as_ref().to_path_buf(),
prompt: prompt.into(),
mask: None,
background: None,
model: None,
n: None,
size: None,
response_format: None,
output_format: None,
output_compression: None,
user: None,
input_fidelity: None,
stream: None,
partial_images: None,
quality: None,
}
}
#[must_use]
pub fn mask(mut self, mask: impl AsRef<Path>) -> Self {
self.mask = Some(mask.as_ref().to_path_buf());
self
}
#[must_use]
pub fn background(mut self, background: impl Into<String>) -> Self {
self.background = Some(background.into());
self
}
#[must_use]
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
#[must_use]
pub fn n(mut self, n: i32) -> Self {
self.n = Some(n);
self
}
#[must_use]
pub fn size(mut self, size: impl Into<String>) -> Self {
self.size = Some(size.into());
self
}
#[must_use]
pub fn response_format(mut self, format: impl Into<String>) -> Self {
self.response_format = Some(format.into());
self
}
#[must_use]
pub fn output_format(mut self, format: impl Into<String>) -> Self {
self.output_format = Some(format.into());
self
}
#[must_use]
pub fn output_compression(mut self, compression: i32) -> Self {
self.output_compression = Some(compression);
self
}
#[must_use]
pub fn user(mut self, user: impl Into<String>) -> Self {
self.user = Some(user.into());
self
}
#[must_use]
pub fn input_fidelity(mut self, fidelity: ImageInputFidelity) -> Self {
self.input_fidelity = Some(fidelity);
self
}
#[must_use]
pub fn stream(mut self, stream: bool) -> Self {
self.stream = Some(stream);
self
}
#[must_use]
pub fn partial_images(mut self, value: i32) -> Self {
self.partial_images = Some(value);
self
}
#[must_use]
pub fn quality(mut self, quality: impl Into<String>) -> Self {
self.quality = Some(quality.into());
self
}
#[must_use]
pub fn image(&self) -> &Path {
&self.image
}
#[must_use]
pub fn prompt(&self) -> &str {
&self.prompt
}
}
#[derive(Debug, Clone)]
pub struct ImageEditRequest {
pub image: PathBuf,
pub prompt: String,
pub mask: Option<PathBuf>,
pub background: Option<String>,
pub model: Option<String>,
pub n: Option<i32>,
pub size: Option<String>,
pub response_format: Option<String>,
pub output_format: Option<String>,
pub output_compression: Option<i32>,
pub user: Option<String>,
pub input_fidelity: Option<ImageInputFidelity>,
pub stream: Option<bool>,
pub partial_images: Option<i32>,
pub quality: Option<String>,
}
impl Builder<ImageEditRequest> for ImageEditBuilder {
fn build(self) -> Result<ImageEditRequest> {
if let Some(n) = self.n {
if !(1..=10).contains(&n) {
return Err(Error::InvalidRequest(format!(
"Image edit `n` must be between 1 and 10 (got {n})"
)));
}
}
if let Some(compression) = self.output_compression {
if !(0..=100).contains(&compression) {
return Err(Error::InvalidRequest(format!(
"Output compression must be between 0 and 100 (got {compression})"
)));
}
}
if let Some(partial) = self.partial_images {
if !(0..=3).contains(&partial) {
return Err(Error::InvalidRequest(format!(
"Partial image count must be between 0 and 3 (got {partial})"
)));
}
}
Ok(ImageEditRequest {
image: self.image,
prompt: self.prompt,
mask: self.mask,
background: self.background,
model: self.model,
n: self.n,
size: self.size,
response_format: self.response_format,
output_format: self.output_format,
output_compression: self.output_compression,
user: self.user,
input_fidelity: self.input_fidelity,
stream: self.stream,
partial_images: self.partial_images,
quality: self.quality,
})
}
}
#[derive(Debug, Clone)]
pub struct ImageVariationBuilder {
image: PathBuf,
model: Option<String>,
n: Option<i32>,
response_format: Option<String>,
size: Option<String>,
user: Option<String>,
}
impl ImageVariationBuilder {
#[must_use]
pub fn new(image: impl AsRef<Path>) -> Self {
Self {
image: image.as_ref().to_path_buf(),
model: None,
n: None,
response_format: None,
size: None,
user: None,
}
}
#[must_use]
pub fn model(mut self, model: impl Into<String>) -> Self {
self.model = Some(model.into());
self
}
#[must_use]
pub fn n(mut self, n: i32) -> Self {
self.n = Some(n);
self
}
#[must_use]
pub fn response_format(mut self, format: impl Into<String>) -> Self {
self.response_format = Some(format.into());
self
}
#[must_use]
pub fn size(mut self, size: impl Into<String>) -> Self {
self.size = Some(size.into());
self
}
#[must_use]
pub fn user(mut self, user: impl Into<String>) -> Self {
self.user = Some(user.into());
self
}
#[must_use]
pub fn image(&self) -> &Path {
&self.image
}
}
#[derive(Debug, Clone)]
pub struct ImageVariationRequest {
pub image: PathBuf,
pub model: Option<String>,
pub n: Option<i32>,
pub response_format: Option<String>,
pub size: Option<String>,
pub user: Option<String>,
}
impl Builder<ImageVariationRequest> for ImageVariationBuilder {
fn build(self) -> Result<ImageVariationRequest> {
if let Some(n) = self.n {
if !(1..=10).contains(&n) {
return Err(Error::InvalidRequest(format!(
"Image variation `n` must be between 1 and 10 (got {n})"
)));
}
}
Ok(ImageVariationRequest {
image: self.image,
model: self.model,
n: self.n,
response_format: self.response_format,
size: self.size,
user: self.user,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builds_image_generation_request() {
let request = ImageGenerationBuilder::new("A scenic valley at sunrise")
.model("gpt-image-1")
.n(2)
.quality(Quality::High)
.response_format(ResponseFormat::B64Json)
.output_format(OutputFormat::Webp)
.output_compression(80)
.stream(true)
.partial_images(Some(2))
.size(Size::Variant1536x1024)
.moderation(Moderation::Auto)
.background(Background::Transparent)
.style(Style::Vivid)
.user("example-user")
.build()
.expect("valid generation builder");
assert_eq!(request.prompt, "A scenic valley at sunrise");
assert_eq!(request.model.as_deref(), Some("gpt-image-1"));
assert_eq!(request.n, Some(2));
assert_eq!(request.quality, Some(Quality::High));
assert_eq!(request.response_format, Some(ResponseFormat::B64Json));
assert_eq!(request.output_format, Some(OutputFormat::Webp));
assert_eq!(request.output_compression, Some(80));
assert_eq!(request.stream, Some(true));
assert_eq!(request.partial_images, Some(Some(2)));
assert_eq!(request.size, Some(Size::Variant1536x1024));
assert_eq!(request.moderation, Some(Moderation::Auto));
assert_eq!(request.background, Some(Background::Transparent));
assert_eq!(request.style, Some(Style::Vivid));
assert_eq!(request.user.as_deref(), Some("example-user"));
}
#[test]
fn generation_validates_ranges() {
let err = ImageGenerationBuilder::new("Prompt")
.n(0)
.build()
.unwrap_err();
assert!(matches!(err, Error::InvalidRequest(_)));
let err = ImageGenerationBuilder::new("Prompt")
.output_compression(150)
.build()
.unwrap_err();
assert!(matches!(err, Error::InvalidRequest(_)));
let err = ImageGenerationBuilder::new("Prompt")
.partial_images(Some(5))
.build()
.unwrap_err();
assert!(matches!(err, Error::InvalidRequest(_)));
}
#[test]
fn builds_image_edit_request() {
let request = ImageEditBuilder::new("image.png", "Remove the background")
.mask("mask.png")
.background("transparent")
.model("gpt-image-1")
.n(1)
.size("1024x1024")
.response_format("b64_json")
.output_format("png")
.output_compression(90)
.user("user-1")
.input_fidelity(ImageInputFidelity::High)
.stream(true)
.partial_images(1)
.quality("standard")
.build()
.expect("valid edit builder");
assert_eq!(request.image, PathBuf::from("image.png"));
assert_eq!(request.prompt, "Remove the background");
assert_eq!(request.mask, Some(PathBuf::from("mask.png")));
assert_eq!(request.background.as_deref(), Some("transparent"));
assert_eq!(request.model.as_deref(), Some("gpt-image-1"));
assert_eq!(request.size.as_deref(), Some("1024x1024"));
assert_eq!(request.response_format.as_deref(), Some("b64_json"));
assert_eq!(request.output_format.as_deref(), Some("png"));
assert_eq!(request.output_compression, Some(90));
assert_eq!(request.stream, Some(true));
assert_eq!(request.partial_images, Some(1));
}
#[test]
fn edit_validates_ranges() {
let err = ImageEditBuilder::new("image.png", "Prompt")
.n(20)
.build()
.unwrap_err();
assert!(matches!(err, Error::InvalidRequest(_)));
let err = ImageEditBuilder::new("image.png", "Prompt")
.output_compression(150)
.build()
.unwrap_err();
assert!(matches!(err, Error::InvalidRequest(_)));
let err = ImageEditBuilder::new("image.png", "Prompt")
.partial_images(5)
.build()
.unwrap_err();
assert!(matches!(err, Error::InvalidRequest(_)));
}
#[test]
fn builds_image_variation_request() {
let request = ImageVariationBuilder::new("image.png")
.model("dall-e-2")
.n(3)
.response_format("url")
.size("512x512")
.user("user-123")
.build()
.expect("valid variation builder");
assert_eq!(request.image, PathBuf::from("image.png"));
assert_eq!(request.model.as_deref(), Some("dall-e-2"));
assert_eq!(request.n, Some(3));
assert_eq!(request.response_format.as_deref(), Some("url"));
assert_eq!(request.size.as_deref(), Some("512x512"));
}
#[test]
fn variation_validates_n() {
let err = ImageVariationBuilder::new("image.png")
.n(0)
.build()
.unwrap_err();
assert!(matches!(err, Error::InvalidRequest(_)));
}
}