From 08f169cebf4e876abbc30d634ba83f9c4e263eaf Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 14 May 2026 18:54:12 -0400 Subject: [PATCH] fal gpt-image implementations using new api (#1518) --- .../image/edit/gpt_image_1_edit_image/api.rs | 207 +++++++++++++++++ .../image/edit/gpt_image_1_edit_image/cost.rs | 182 +++++++++++++++ .../image/edit/gpt_image_1_edit_image/mod.rs | 3 + .../gpt_image_1_edit_image/raw_request.rs} | 19 +- .../edit/gpt_image_1p5_edit_image/api.rs | 205 +++++++++++++++++ .../edit/gpt_image_1p5_edit_image/cost.rs | 171 ++++++++++++++ .../edit/gpt_image_1p5_edit_image/mod.rs | 3 + .../gpt_image_1p5_edit_image/raw_request.rs | 48 ++++ .../image/edit/gpt_image_2_edit_image/api.rs | 174 +++++++++++++++ .../image/edit/gpt_image_2_edit_image/cost.rs | 209 ++++++++++++++++++ .../image/edit/gpt_image_2_edit_image/mod.rs | 3 + .../gpt_image_2_edit_image/raw_request.rs | 41 ++++ .../src/requests/api/image/edit/mod.rs | 3 + .../text/gpt_image_1_text_to_image/api.rs | 173 +++++++++++++++ .../text/gpt_image_1_text_to_image/cost.rs | 179 +++++++++++++++ .../text/gpt_image_1_text_to_image/mod.rs | 3 + .../gpt_image_1_text_to_image/raw_request.rs} | 19 +- .../text/gpt_image_1p5_text_to_image/api.rs | 171 ++++++++++++++ .../text/gpt_image_1p5_text_to_image/cost.rs | 168 ++++++++++++++ .../text/gpt_image_1p5_text_to_image/mod.rs | 3 + .../raw_request.rs | 38 ++++ .../text/gpt_image_2_text_to_image/api.rs | 155 +++++++++++++ .../text/gpt_image_2_text_to_image/cost.rs | 185 ++++++++++++++++ .../text/gpt_image_2_text_to_image/mod.rs | 3 + .../gpt_image_2_text_to_image/raw_request.rs | 34 +++ .../src/requests/api/image/text/mod.rs | 3 + .../edit/http_gpt_image_1_byok_edit_image.rs | 32 +++ .../image/edit/http_gpt_image_1_edit_image.rs | 41 +++- .../src/requests/http/image/edit/mod.rs | 2 +- .../http_gpt_image_1_byok_text_to_image.rs | 30 +++ .../text/http_gpt_image_1_text_to_image.rs | 35 ++- .../src/requests/http/image/text/mod.rs | 2 +- ...eue_gpt_image_1_byok_edit_image_webhook.rs | 6 +- .../enqueue_gpt_image_1_edit_image_webhook.rs | 8 +- ..._gpt_image_1_byok_text_to_image_webhook.rs | 6 +- ...queue_gpt_image_1_text_to_image_webhook.rs | 8 +- 36 files changed, 2511 insertions(+), 61 deletions(-) create mode 100644 crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1_edit_image/api.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1_edit_image/cost.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1_edit_image/mod.rs rename crates/api_clients/fal_client/src/requests/{http/image/edit/http_gpt_image_1_non_byok_edit_image.rs => api/image/edit/gpt_image_1_edit_image/raw_request.rs} (62%) create mode 100644 crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1p5_edit_image/api.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1p5_edit_image/cost.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1p5_edit_image/mod.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1p5_edit_image/raw_request.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_2_edit_image/api.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_2_edit_image/cost.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_2_edit_image/mod.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_2_edit_image/raw_request.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1_text_to_image/api.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1_text_to_image/cost.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1_text_to_image/mod.rs rename crates/api_clients/fal_client/src/requests/{http/image/text/http_gpt_image_1_non_byok_text_to_image.rs => api/image/text/gpt_image_1_text_to_image/raw_request.rs} (55%) create mode 100644 crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1p5_text_to_image/api.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1p5_text_to_image/cost.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1p5_text_to_image/mod.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1p5_text_to_image/raw_request.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_2_text_to_image/api.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_2_text_to_image/cost.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_2_text_to_image/mod.rs create mode 100644 crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_2_text_to_image/raw_request.rs create mode 100644 crates/api_clients/fal_client/src/requests/http/image/edit/http_gpt_image_1_byok_edit_image.rs create mode 100644 crates/api_clients/fal_client/src/requests/http/image/text/http_gpt_image_1_byok_text_to_image.rs diff --git a/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1_edit_image/api.rs b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1_edit_image/api.rs new file mode 100644 index 0000000000..d587a479f4 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1_edit_image/api.rs @@ -0,0 +1,207 @@ +use crate::error::fal_error_plus::FalErrorPlus; +use crate::requests::api::image::edit::gpt_image_1_edit_image::raw_request::{ + GptImage1EditImageInput, GptImage1EditImageOutput, +}; +use crate::requests::traits::fal_endpoint_trait::FalEndpoint; + +#[derive(Clone, Debug)] +pub struct GptImage1EditImageRequest { + /// Text prompt describing the edit to make. + pub prompt: String, + + /// One or more source image URLs to edit. + pub image_urls: Vec, + + /// Number of images to generate. + pub num_images: GptImage1EditImageNumImages, + + /// Optional mask URL indicating what part of the image to edit. + pub mask_image_url: Option, + + /// Output image size. + pub image_size: Option, + + /// Quality level. + pub quality: Option, + + /// Input fidelity level. + pub input_fidelity: Option, + + /// Background mode. + pub background: Option, + + /// Output format. + pub output_format: Option, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1EditImageNumImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1EditImageSize { + Auto, + /// 1024x1024 + Square, + /// 1536x1024 + Horizontal, + /// 1024x1536 + Vertical, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1EditImageQuality { + Low, + Medium, + High, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1EditImageInputFidelity { + Low, + High, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1EditImageBackground { + Auto, + Transparent, + Opaque, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1EditImageOutputFormat { + Jpeg, + Png, + Webp, +} + +impl FalEndpoint for GptImage1EditImageRequest { + const ENDPOINT: &str = "fal-ai/gpt-image-1/edit-image"; + + type RawRequest = GptImage1EditImageInput; + type RawResponse = GptImage1EditImageOutput; + + fn to_raw_request(&self) -> Result { + let num_images = match self.num_images { + GptImage1EditImageNumImages::One => 1, + GptImage1EditImageNumImages::Two => 2, + GptImage1EditImageNumImages::Three => 3, + GptImage1EditImageNumImages::Four => 4, + }; + + let image_size = self.image_size.map(|s| match s { + GptImage1EditImageSize::Auto => "auto", + GptImage1EditImageSize::Square => "1024x1024", + GptImage1EditImageSize::Horizontal => "1536x1024", + GptImage1EditImageSize::Vertical => "1024x1536", + }.to_string()); + + let quality = self.quality.map(|q| match q { + GptImage1EditImageQuality::Low => "low", + GptImage1EditImageQuality::Medium => "medium", + GptImage1EditImageQuality::High => "high", + }.to_string()); + + let input_fidelity = self.input_fidelity.map(|f| match f { + GptImage1EditImageInputFidelity::Low => "low", + GptImage1EditImageInputFidelity::High => "high", + }.to_string()); + + let background = self.background.map(|b| match b { + GptImage1EditImageBackground::Auto => "auto", + GptImage1EditImageBackground::Transparent => "transparent", + GptImage1EditImageBackground::Opaque => "opaque", + }.to_string()); + + let output_format = Some(match self.output_format { + Some(GptImage1EditImageOutputFormat::Jpeg) => "jpeg", + Some(GptImage1EditImageOutputFormat::Png) => "png", + Some(GptImage1EditImageOutputFormat::Webp) => "webp", + None => "png", + }.to_string()); + + Ok(Self::RawRequest { + prompt: self.prompt.clone(), + image_urls: self.image_urls.clone(), + num_images: Some(num_images), + mask_image_url: self.mask_image_url.clone(), + image_size, + quality, + input_fidelity, + background, + output_format, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::traits::fal_endpoint_trait::FalEndpoint; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::{ERNEST_SCARED_STUPID_IMAGE_URL, GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL}; + + #[tokio::test] + #[ignore] // manually test — requires real API key, incurs costs + async fn test_edit_image_queue() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let request = GptImage1EditImageRequest { + prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly".to_string(), + image_urls: vec![ + GHOST_IMAGE_URL.to_string(), + TREX_SKELETON_IMAGE_URL.to_string(), + ERNEST_SCARED_STUPID_IMAGE_URL.to_string(), + ], + num_images: GptImage1EditImageNumImages::One, + mask_image_url: None, + image_size: Some(GptImage1EditImageSize::Horizontal), + quality: Some(GptImage1EditImageQuality::Medium), + input_fidelity: None, + background: None, + output_format: None, + }; + + let result = request.send_queue_request(&api_key).await?; + println!("Request ID: {}", result.request_id); + assert!(!result.request_id.is_empty()); + Ok(()) + } + + #[tokio::test] + #[ignore] // manually test — requires real API key, incurs costs + async fn test_edit_image_webhook() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let request = GptImage1EditImageRequest { + image_urls: vec![GHOST_IMAGE_URL.to_string()], + prompt: "make the ghost wear a top hat".to_string(), + num_images: GptImage1EditImageNumImages::One, + mask_image_url: None, + image_size: None, + quality: Some(GptImage1EditImageQuality::High), + input_fidelity: None, + background: None, + output_format: Some(GptImage1EditImageOutputFormat::Png), + }; + + let result = request.send_webhook_request( + &api_key, + "https://example.com/webhook", + ).await?; + println!("Request ID: {:?}", result.request_id); + assert!(result.request_id.is_some()); + Ok(()) + } + + // NB: Pricing tests are in cost.rs +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1_edit_image/cost.rs b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1_edit_image/cost.rs new file mode 100644 index 0000000000..e943ef801c --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1_edit_image/cost.rs @@ -0,0 +1,182 @@ +use crate::requests::api::image::edit::gpt_image_1_edit_image::api::{ + GptImage1EditImageNumImages, GptImage1EditImageQuality, + GptImage1EditImageRequest, GptImage1EditImageSize, +}; +use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; + +impl FalRequestCostCalculator for GptImage1EditImageRequest { + fn calculate_cost_in_cents(&self) -> UsdCents { + // Per fal docs (fal-ai/gpt-image-1/edit-image): + // Low: $0.011 (1024x1024) / $0.016 (other) per image + // Medium: $0.042 (1024x1024) / $0.063 (other) per image + // High: $0.167 (1024x1024) / $0.25 (other) per image + let use_quality = self.quality.unwrap_or(GptImage1EditImageQuality::Medium); + let is_square = matches!( + self.image_size, + None | Some(GptImage1EditImageSize::Square) | Some(GptImage1EditImageSize::Auto) + ); + + let base_cost: u64 = match (use_quality, is_square) { + (GptImage1EditImageQuality::Low, true) => 2, + (GptImage1EditImageQuality::Low, false) => 2, + (GptImage1EditImageQuality::Medium, true) => 5, + (GptImage1EditImageQuality::Medium, false) => 7, + (GptImage1EditImageQuality::High, true) => 17, + (GptImage1EditImageQuality::High, false) => 25, + }; + + let n: u64 = match self.num_images { + GptImage1EditImageNumImages::One => 1, + GptImage1EditImageNumImages::Two => 2, + GptImage1EditImageNumImages::Three => 3, + GptImage1EditImageNumImages::Four => 4, + }; + base_cost * n + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_request( + num_images: GptImage1EditImageNumImages, + quality: Option, + image_size: Option, + ) -> GptImage1EditImageRequest { + GptImage1EditImageRequest { + prompt: "test".to_string(), + image_urls: vec!["https://example.com/image.png".to_string()], + num_images, + mask_image_url: None, + image_size, + quality, + input_fidelity: None, + background: None, + output_format: None, + } + } + + mod default_tests { + use super::*; + + #[test] + fn cost_defaults_one_image() { + // Default quality=Medium, size=Square => 5 cents + assert_eq!( + make_request(GptImage1EditImageNumImages::One, None, None) + .calculate_cost_in_cents(), 5); + } + + #[test] + fn cost_defaults_four_images() { + assert_eq!( + make_request(GptImage1EditImageNumImages::Four, None, None) + .calculate_cost_in_cents(), 20); + } + } + + mod low_quality_tests { + use super::*; + + #[test] + fn cost_low_square_one_image() { + assert_eq!( + make_request(GptImage1EditImageNumImages::One, Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Square)) + .calculate_cost_in_cents(), 2); + } + + #[test] + fn cost_low_horizontal_one_image() { + assert_eq!( + make_request(GptImage1EditImageNumImages::One, Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Horizontal)) + .calculate_cost_in_cents(), 2); + } + + #[test] + fn cost_low_auto_four_images() { + // Auto is treated as square + assert_eq!( + make_request(GptImage1EditImageNumImages::Four, Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Auto)) + .calculate_cost_in_cents(), 8); + } + } + + mod medium_quality_tests { + use super::*; + + #[test] + fn cost_medium_square_one_image() { + assert_eq!( + make_request(GptImage1EditImageNumImages::One, Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Square)) + .calculate_cost_in_cents(), 5); + } + + #[test] + fn cost_medium_auto_one_image() { + assert_eq!( + make_request(GptImage1EditImageNumImages::One, Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Auto)) + .calculate_cost_in_cents(), 5); + } + + #[test] + fn cost_medium_horizontal_one_image() { + assert_eq!( + make_request(GptImage1EditImageNumImages::One, Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Horizontal)) + .calculate_cost_in_cents(), 7); + } + + #[test] + fn cost_medium_vertical_one_image() { + assert_eq!( + make_request(GptImage1EditImageNumImages::One, Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Vertical)) + .calculate_cost_in_cents(), 7); + } + + #[test] + fn cost_medium_horizontal_three_images() { + assert_eq!( + make_request(GptImage1EditImageNumImages::Three, Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Horizontal)) + .calculate_cost_in_cents(), 21); + } + } + + mod high_quality_tests { + use super::*; + + #[test] + fn cost_high_square_one_image() { + assert_eq!( + make_request(GptImage1EditImageNumImages::One, Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Square)) + .calculate_cost_in_cents(), 17); + } + + #[test] + fn cost_high_horizontal_one_image() { + assert_eq!( + make_request(GptImage1EditImageNumImages::One, Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Horizontal)) + .calculate_cost_in_cents(), 25); + } + + #[test] + fn cost_high_vertical_one_image() { + assert_eq!( + make_request(GptImage1EditImageNumImages::One, Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Vertical)) + .calculate_cost_in_cents(), 25); + } + + #[test] + fn cost_high_square_two_images() { + assert_eq!( + make_request(GptImage1EditImageNumImages::Two, Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Square)) + .calculate_cost_in_cents(), 34); + } + + #[test] + fn cost_high_horizontal_four_images() { + assert_eq!( + make_request(GptImage1EditImageNumImages::Four, Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Horizontal)) + .calculate_cost_in_cents(), 100); + } + } +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1_edit_image/mod.rs b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1_edit_image/mod.rs new file mode 100644 index 0000000000..a0a589f2cd --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1_edit_image/mod.rs @@ -0,0 +1,3 @@ +pub mod api; +mod raw_request; +mod cost; diff --git a/crates/api_clients/fal_client/src/requests/http/image/edit/http_gpt_image_1_non_byok_edit_image.rs b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1_edit_image/raw_request.rs similarity index 62% rename from crates/api_clients/fal_client/src/requests/http/image/edit/http_gpt_image_1_non_byok_edit_image.rs rename to crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1_edit_image/raw_request.rs index ecb736d4a6..b7efaea2e2 100644 --- a/crates/api_clients/fal_client/src/requests/http/image/edit/http_gpt_image_1_non_byok_edit_image.rs +++ b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1_edit_image/raw_request.rs @@ -1,15 +1,12 @@ -use crate::requests::core_api::fal_request::FalRequest; use serde::{Deserialize, Serialize}; -/// Non-BYOK edit-image (image-to-image) binding for -/// `fal-ai/gpt-image-1/edit-image`. Fal hosts this model directly; pricing is -/// billed by Fal at their published rates. #[derive(Debug, Serialize, Deserialize, Default)] -pub struct GptImage1NonByokEditImageInput { +pub struct GptImage1EditImageInput { pub prompt: String, pub image_urls: Vec, + /// The URL of the mask image indicating what part of the image to edit. #[serde(skip_serializing_if = "Option::is_none")] pub mask_image_url: Option, @@ -41,17 +38,11 @@ pub struct GptImage1NonByokEditImageInput { } #[derive(Debug, Serialize, Deserialize)] -pub struct GptImage1NonByokEditImageFile { +pub struct GptImage1EditImageFile { pub url: String, } #[derive(Debug, Serialize, Deserialize)] -pub struct GptImage1NonByokEditImageOutput { - pub images: Vec, -} - -pub fn gpt_image_1_non_byok_edit_image( - params: GptImage1NonByokEditImageInput, -) -> FalRequest { - FalRequest::new("fal-ai/gpt-image-1/edit-image", params) +pub struct GptImage1EditImageOutput { + pub images: Vec, } diff --git a/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1p5_edit_image/api.rs b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1p5_edit_image/api.rs new file mode 100644 index 0000000000..b0e232e14d --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1p5_edit_image/api.rs @@ -0,0 +1,205 @@ +use crate::error::fal_error_plus::FalErrorPlus; +use crate::requests::api::image::edit::gpt_image_1p5_edit_image::raw_request::{ + GptImage1p5EditImageInput, GptImage1p5EditImageOutput, +}; +use crate::requests::traits::fal_endpoint_trait::FalEndpoint; + +#[derive(Clone, Debug)] +pub struct GptImage1p5EditImageRequest { + /// Text prompt describing the edit to make. + pub prompt: String, + + /// One or more source image URLs to edit. + pub image_urls: Vec, + + /// Number of images to generate. + pub num_images: GptImage1p5EditImageNumImages, + + /// Optional mask URL indicating what part of the image to edit. + pub mask_image_url: Option, + + /// Output image size. + pub image_size: Option, + + /// Background mode. + pub background: Option, + + /// Quality level. + pub quality: Option, + + /// Input fidelity level. + pub input_fidelity: Option, + + /// Output format. + pub output_format: Option, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1p5EditImageNumImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1p5EditImageSize { + /// 1024x1024 + Square, + /// 1536x1024 + Wide, + /// 1024x1536 + Tall, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1p5EditImageBackground { + Auto, + Transparent, + Opaque, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1p5EditImageQuality { + Low, + Medium, + High, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1p5EditImageInputFidelity { + Low, + High, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1p5EditImageOutputFormat { + Jpeg, + Png, + Webp, +} + +impl FalEndpoint for GptImage1p5EditImageRequest { + const ENDPOINT: &str = "fal-ai/gpt-image-1.5/edit"; + + type RawRequest = GptImage1p5EditImageInput; + type RawResponse = GptImage1p5EditImageOutput; + + fn to_raw_request(&self) -> Result { + let num_images = match self.num_images { + GptImage1p5EditImageNumImages::One => 1, + GptImage1p5EditImageNumImages::Two => 2, + GptImage1p5EditImageNumImages::Three => 3, + GptImage1p5EditImageNumImages::Four => 4, + }; + + let image_size = self.image_size.map(|s| match s { + GptImage1p5EditImageSize::Square => "1024x1024", + GptImage1p5EditImageSize::Wide => "1536x1024", + GptImage1p5EditImageSize::Tall => "1024x1536", + }.to_string()); + + let background = self.background.map(|b| match b { + GptImage1p5EditImageBackground::Auto => "auto", + GptImage1p5EditImageBackground::Transparent => "transparent", + GptImage1p5EditImageBackground::Opaque => "opaque", + }.to_string()); + + let quality = self.quality.map(|q| match q { + GptImage1p5EditImageQuality::Low => "low", + GptImage1p5EditImageQuality::Medium => "medium", + GptImage1p5EditImageQuality::High => "high", + }.to_string()); + + let input_fidelity = self.input_fidelity.map(|f| match f { + GptImage1p5EditImageInputFidelity::Low => "low", + GptImage1p5EditImageInputFidelity::High => "high", + }.to_string()); + + let output_format = Some(match self.output_format { + Some(GptImage1p5EditImageOutputFormat::Jpeg) => "jpeg", + Some(GptImage1p5EditImageOutputFormat::Png) => "png", + Some(GptImage1p5EditImageOutputFormat::Webp) => "webp", + None => "png", + }.to_string()); + + Ok(Self::RawRequest { + prompt: self.prompt.clone(), + image_urls: self.image_urls.clone(), + num_images: Some(num_images), + mask_image_url: self.mask_image_url.clone(), + image_size, + background, + quality, + input_fidelity, + output_format, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::traits::fal_endpoint_trait::FalEndpoint; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::{ERNEST_SCARED_STUPID_IMAGE_URL, GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL}; + + #[tokio::test] + #[ignore] // manually test — requires real API key, incurs costs + async fn test_edit_image_queue() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let request = GptImage1p5EditImageRequest { + image_urls: vec![ + GHOST_IMAGE_URL.to_string(), + TREX_SKELETON_IMAGE_URL.to_string(), + ERNEST_SCARED_STUPID_IMAGE_URL.to_string(), + ], + prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly".to_string(), + num_images: GptImage1p5EditImageNumImages::Two, + mask_image_url: None, + image_size: None, + background: None, + quality: None, + input_fidelity: None, + output_format: None, + }; + + let result = request.send_queue_request(&api_key).await?; + println!("Request ID: {}", result.request_id); + assert!(!result.request_id.is_empty()); + Ok(()) + } + + #[tokio::test] + #[ignore] // manually test — requires real API key, incurs costs + async fn test_edit_image_webhook() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let request = GptImage1p5EditImageRequest { + image_urls: vec![GHOST_IMAGE_URL.to_string()], + prompt: "make the ghost wear a top hat".to_string(), + num_images: GptImage1p5EditImageNumImages::One, + mask_image_url: None, + image_size: Some(GptImage1p5EditImageSize::Square), + background: None, + quality: Some(GptImage1p5EditImageQuality::High), + input_fidelity: None, + output_format: Some(GptImage1p5EditImageOutputFormat::Png), + }; + + let result = request.send_webhook_request( + &api_key, + "https://example.com/webhook", + ).await?; + println!("Request ID: {:?}", result.request_id); + assert!(result.request_id.is_some()); + Ok(()) + } + + // NB: Pricing tests are in cost.rs +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1p5_edit_image/cost.rs b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1p5_edit_image/cost.rs new file mode 100644 index 0000000000..c7012f970e --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1p5_edit_image/cost.rs @@ -0,0 +1,171 @@ +use crate::requests::api::image::edit::gpt_image_1p5_edit_image::api::{ + GptImage1p5EditImageNumImages, GptImage1p5EditImageQuality, + GptImage1p5EditImageRequest, GptImage1p5EditImageSize, +}; +use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; + +impl FalRequestCostCalculator for GptImage1p5EditImageRequest { + fn calculate_cost_in_cents(&self) -> UsdCents { + // Per fal docs (fal-ai/gpt-image-1.5/edit): + // Low: $0.009 (1024x1024) / $0.013 (other) per image + // Medium: $0.034 (1024x1024) / $0.050-$0.051 (other) per image + // High: $0.133 (1024x1024) / $0.199-$0.200 (other) per image + let use_quality = self.quality.unwrap_or(GptImage1p5EditImageQuality::Medium); + let use_size = self.image_size.unwrap_or(GptImage1p5EditImageSize::Square); + + let base_cost = match (use_quality, use_size) { + (GptImage1p5EditImageQuality::Low, GptImage1p5EditImageSize::Square) => 1, + (GptImage1p5EditImageQuality::Low, _) => 1, + (GptImage1p5EditImageQuality::Medium, GptImage1p5EditImageSize::Square) => 3, + (GptImage1p5EditImageQuality::Medium, _) => 5, + (GptImage1p5EditImageQuality::High, GptImage1p5EditImageSize::Square) => 13, + (GptImage1p5EditImageQuality::High, _) => 20, + }; + + let cost = match self.num_images { + GptImage1p5EditImageNumImages::One => base_cost, + GptImage1p5EditImageNumImages::Two => base_cost * 2, + GptImage1p5EditImageNumImages::Three => base_cost * 3, + GptImage1p5EditImageNumImages::Four => base_cost * 4, + }; + cost as UsdCents + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_request( + num_images: GptImage1p5EditImageNumImages, + quality: Option, + image_size: Option, + ) -> GptImage1p5EditImageRequest { + GptImage1p5EditImageRequest { + prompt: "test".to_string(), + image_urls: vec!["https://example.com/image.png".to_string()], + num_images, + mask_image_url: None, + image_size, + background: None, + quality, + input_fidelity: None, + output_format: None, + } + } + + mod default_tests { + use super::*; + + #[test] + fn cost_defaults_one_image() { + // Default quality=Medium, size=Square => 3 cents + assert_eq!( + make_request(GptImage1p5EditImageNumImages::One, None, None) + .calculate_cost_in_cents(), 3); + } + + #[test] + fn cost_defaults_four_images() { + assert_eq!( + make_request(GptImage1p5EditImageNumImages::Four, None, None) + .calculate_cost_in_cents(), 12); + } + } + + mod low_quality_tests { + use super::*; + + #[test] + fn cost_low_square_one_image() { + assert_eq!( + make_request(GptImage1p5EditImageNumImages::One, Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Square)) + .calculate_cost_in_cents(), 1); + } + + #[test] + fn cost_low_wide_one_image() { + assert_eq!( + make_request(GptImage1p5EditImageNumImages::One, Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Wide)) + .calculate_cost_in_cents(), 1); + } + + #[test] + fn cost_low_tall_four_images() { + assert_eq!( + make_request(GptImage1p5EditImageNumImages::Four, Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Tall)) + .calculate_cost_in_cents(), 4); + } + } + + mod medium_quality_tests { + use super::*; + + #[test] + fn cost_medium_square_one_image() { + assert_eq!( + make_request(GptImage1p5EditImageNumImages::One, Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Square)) + .calculate_cost_in_cents(), 3); + } + + #[test] + fn cost_medium_wide_one_image() { + assert_eq!( + make_request(GptImage1p5EditImageNumImages::One, Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Wide)) + .calculate_cost_in_cents(), 5); + } + + #[test] + fn cost_medium_tall_one_image() { + assert_eq!( + make_request(GptImage1p5EditImageNumImages::One, Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Tall)) + .calculate_cost_in_cents(), 5); + } + + #[test] + fn cost_medium_wide_three_images() { + assert_eq!( + make_request(GptImage1p5EditImageNumImages::Three, Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Wide)) + .calculate_cost_in_cents(), 15); + } + } + + mod high_quality_tests { + use super::*; + + #[test] + fn cost_high_square_one_image() { + assert_eq!( + make_request(GptImage1p5EditImageNumImages::One, Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Square)) + .calculate_cost_in_cents(), 13); + } + + #[test] + fn cost_high_wide_one_image() { + assert_eq!( + make_request(GptImage1p5EditImageNumImages::One, Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Wide)) + .calculate_cost_in_cents(), 20); + } + + #[test] + fn cost_high_tall_one_image() { + assert_eq!( + make_request(GptImage1p5EditImageNumImages::One, Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Tall)) + .calculate_cost_in_cents(), 20); + } + + #[test] + fn cost_high_square_two_images() { + assert_eq!( + make_request(GptImage1p5EditImageNumImages::Two, Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Square)) + .calculate_cost_in_cents(), 26); + } + + #[test] + fn cost_high_wide_four_images() { + assert_eq!( + make_request(GptImage1p5EditImageNumImages::Four, Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Wide)) + .calculate_cost_in_cents(), 80); + } + } +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1p5_edit_image/mod.rs b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1p5_edit_image/mod.rs new file mode 100644 index 0000000000..a0a589f2cd --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1p5_edit_image/mod.rs @@ -0,0 +1,3 @@ +pub mod api; +mod raw_request; +mod cost; diff --git a/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1p5_edit_image/raw_request.rs b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1p5_edit_image/raw_request.rs new file mode 100644 index 0000000000..d17609e128 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_1p5_edit_image/raw_request.rs @@ -0,0 +1,48 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct GptImage1p5EditImageInput { + pub prompt: String, + + pub image_urls: Vec, + + /// The URL of the mask image indicating what part of the image to edit. + #[serde(skip_serializing_if = "Option::is_none")] + pub mask_image_url: Option, + + /// "1024x1024", "1536x1024", "1024x1536" + #[serde(skip_serializing_if = "Option::is_none")] + pub image_size: Option, + + /// "auto", "transparent", "opaque" + #[serde(skip_serializing_if = "Option::is_none")] + pub background: Option, + + /// "low", "medium", "high" + #[serde(skip_serializing_if = "Option::is_none")] + pub quality: Option, + + /// "low", "high" + #[serde(skip_serializing_if = "Option::is_none")] + pub input_fidelity: Option, + + /// 1 - 4 + /// Default: 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_images: Option, + + /// "jpeg", "png", "webp" + /// Default: "png" + #[serde(skip_serializing_if = "Option::is_none")] + pub output_format: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GptImage1p5EditImageFile { + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GptImage1p5EditImageOutput { + pub images: Vec, +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_2_edit_image/api.rs b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_2_edit_image/api.rs new file mode 100644 index 0000000000..0b6ac0a566 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_2_edit_image/api.rs @@ -0,0 +1,174 @@ +use crate::error::fal_error_plus::FalErrorPlus; +use crate::requests::api::image::edit::gpt_image_2_edit_image::raw_request::{ + GptImage2EditImageInput, GptImage2EditImageOutput, +}; +use crate::requests::traits::fal_endpoint_trait::FalEndpoint; + +#[derive(Clone, Debug)] +pub struct GptImage2EditImageRequest { + /// Text prompt describing the edit to make. + pub prompt: String, + + /// One or more source image URLs to edit. + pub image_urls: Vec, + + /// Number of images to generate. + pub num_images: GptImage2EditImageNumImages, + + /// Optional mask URL indicating what part of the image to edit. + pub mask_url: Option, + + /// Output image size. + pub image_size: Option, + + /// Quality level. + pub quality: Option, + + /// Output format. + pub output_format: Option, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage2EditImageNumImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage2EditImageSize { + SquareHd, + Square, + Portrait4x3, + Portrait16x9, + Landscape4x3, + Landscape16x9, + Auto, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage2EditImageQuality { + Low, + Medium, + High, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage2EditImageOutputFormat { + Jpeg, + Png, + Webp, +} + +impl FalEndpoint for GptImage2EditImageRequest { + const ENDPOINT: &str = "openai/gpt-image-2/edit"; + + type RawRequest = GptImage2EditImageInput; + type RawResponse = GptImage2EditImageOutput; + + fn to_raw_request(&self) -> Result { + let num_images = match self.num_images { + GptImage2EditImageNumImages::One => 1, + GptImage2EditImageNumImages::Two => 2, + GptImage2EditImageNumImages::Three => 3, + GptImage2EditImageNumImages::Four => 4, + }; + + let image_size = self.image_size.map(|s| match s { + GptImage2EditImageSize::SquareHd => "square_hd", + GptImage2EditImageSize::Square => "square", + GptImage2EditImageSize::Portrait4x3 => "portrait_4_3", + GptImage2EditImageSize::Portrait16x9 => "portrait_16_9", + GptImage2EditImageSize::Landscape4x3 => "landscape_4_3", + GptImage2EditImageSize::Landscape16x9 => "landscape_16_9", + GptImage2EditImageSize::Auto => "auto", + }.to_string()); + + let quality = self.quality.map(|q| match q { + GptImage2EditImageQuality::Low => "low", + GptImage2EditImageQuality::Medium => "medium", + GptImage2EditImageQuality::High => "high", + }.to_string()); + + let output_format = Some(match self.output_format { + Some(GptImage2EditImageOutputFormat::Jpeg) => "jpeg", + Some(GptImage2EditImageOutputFormat::Png) => "png", + Some(GptImage2EditImageOutputFormat::Webp) => "webp", + None => "png", + }.to_string()); + + Ok(Self::RawRequest { + prompt: self.prompt.clone(), + image_urls: self.image_urls.clone(), + num_images: Some(num_images), + mask_url: self.mask_url.clone(), + image_size, + quality, + output_format, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::traits::fal_endpoint_trait::FalEndpoint; + use errors::AnyhowResult; + use std::fs::read_to_string; + use test_data::web::image_urls::{ERNEST_SCARED_STUPID_IMAGE_URL, GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL}; + + #[tokio::test] + #[ignore] // manually test — requires real API key, incurs costs + async fn test_edit_image_queue() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let request = GptImage2EditImageRequest { + image_urls: vec![ + GHOST_IMAGE_URL.to_string(), + TREX_SKELETON_IMAGE_URL.to_string(), + ERNEST_SCARED_STUPID_IMAGE_URL.to_string(), + ], + prompt: "add the ghost and scared man to the image of the t-rex skeleton, make it look spooky but friendly".to_string(), + num_images: GptImage2EditImageNumImages::Two, + mask_url: None, + image_size: None, + quality: None, + output_format: None, + }; + + let result = request.send_queue_request(&api_key).await?; + println!("Request ID: {}", result.request_id); + assert!(!result.request_id.is_empty()); + Ok(()) + } + + #[tokio::test] + #[ignore] // manually test — requires real API key, incurs costs + async fn test_edit_image_webhook() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let request = GptImage2EditImageRequest { + image_urls: vec![GHOST_IMAGE_URL.to_string()], + prompt: "make the ghost wear a top hat".to_string(), + num_images: GptImage2EditImageNumImages::One, + mask_url: None, + image_size: Some(GptImage2EditImageSize::Square), + quality: Some(GptImage2EditImageQuality::High), + output_format: Some(GptImage2EditImageOutputFormat::Png), + }; + + let result = request.send_webhook_request( + &api_key, + "https://example.com/webhook", + ).await?; + println!("Request ID: {:?}", result.request_id); + assert!(result.request_id.is_some()); + Ok(()) + } + + // NB: Pricing tests are in cost.rs +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_2_edit_image/cost.rs b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_2_edit_image/cost.rs new file mode 100644 index 0000000000..1f296e0cb4 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_2_edit_image/cost.rs @@ -0,0 +1,209 @@ +use crate::requests::api::image::edit::gpt_image_2_edit_image::api::{ + GptImage2EditImageNumImages, GptImage2EditImageQuality, + GptImage2EditImageRequest, GptImage2EditImageSize, +}; +use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; + +impl FalRequestCostCalculator for GptImage2EditImageRequest { + fn calculate_cost_in_cents(&self) -> UsdCents { + // Cost table (per image) by approximate pixel dimensions: + // + // landscape_4_3 (~1024x768): low=$0.01, medium=$0.04, high=$0.15 + // square (~1024x1024): low=$0.01, medium=$0.06, high=$0.22 + // portrait_4_3 (~768x1024): low=$0.01, medium=$0.04, high=$0.15 + // landscape_16_9(~1920x1080): low=$0.01, medium=$0.04, high=$0.16 + // portrait_16_9 (~1080x1920): low=$0.01, medium=$0.04, high=$0.16 + // square_hd (~2048x2048): low=$0.01, medium=$0.06, high=$0.23 + // auto (varies): estimated as square_hd (worst case) + let use_quality = self.quality.unwrap_or(GptImage2EditImageQuality::High); + let use_size = self.image_size.unwrap_or(GptImage2EditImageSize::Square); + + use GptImage2EditImageQuality::*; + use GptImage2EditImageSize::*; + + let base_cost = match (use_quality, use_size) { + (Low, _) => 1, + (Medium, Landscape4x3 | Portrait4x3 | Landscape16x9 | Portrait16x9) => 4, + (Medium, Square) => 6, + (Medium, SquareHd) => 6, + (Medium, Auto) => 6, + (High, Landscape4x3 | Portrait4x3) => 15, + (High, Landscape16x9 | Portrait16x9) => 16, + (High, Square) => 22, + (High, SquareHd) => 23, + (High, Auto) => 23, + }; + + let cost = match self.num_images { + GptImage2EditImageNumImages::One => base_cost, + GptImage2EditImageNumImages::Two => base_cost * 2, + GptImage2EditImageNumImages::Three => base_cost * 3, + GptImage2EditImageNumImages::Four => base_cost * 4, + }; + cost as UsdCents + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_request( + num_images: GptImage2EditImageNumImages, + quality: Option, + image_size: Option, + ) -> GptImage2EditImageRequest { + GptImage2EditImageRequest { + prompt: "test".to_string(), + image_urls: vec!["https://example.com/image.png".to_string()], + num_images, + mask_url: None, + image_size, + quality, + output_format: None, + } + } + + mod default_tests { + use super::*; + + #[test] + fn cost_defaults_one_image() { + // Default quality=High, size=Square => 22 cents + assert_eq!( + make_request(GptImage2EditImageNumImages::One, None, None) + .calculate_cost_in_cents(), 22); + } + + #[test] + fn cost_defaults_four_images() { + assert_eq!( + make_request(GptImage2EditImageNumImages::Four, None, None) + .calculate_cost_in_cents(), 88); + } + } + + mod low_quality_tests { + use super::*; + + #[test] + fn cost_low_any_size_one_image() { + assert_eq!( + make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::SquareHd)) + .calculate_cost_in_cents(), 1); + } + + #[test] + fn cost_low_any_size_four_images() { + assert_eq!( + make_request(GptImage2EditImageNumImages::Four, Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto)) + .calculate_cost_in_cents(), 4); + } + } + + mod medium_quality_tests { + use super::*; + + #[test] + fn cost_medium_landscape_4x3() { + assert_eq!( + make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape4x3)) + .calculate_cost_in_cents(), 4); + } + + #[test] + fn cost_medium_portrait_16x9() { + assert_eq!( + make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait16x9)) + .calculate_cost_in_cents(), 4); + } + + #[test] + fn cost_medium_square() { + assert_eq!( + make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Square)) + .calculate_cost_in_cents(), 6); + } + + #[test] + fn cost_medium_square_hd() { + assert_eq!( + make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::SquareHd)) + .calculate_cost_in_cents(), 6); + } + + #[test] + fn cost_medium_auto() { + assert_eq!( + make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto)) + .calculate_cost_in_cents(), 6); + } + + #[test] + fn cost_medium_landscape_4x3_three_images() { + assert_eq!( + make_request(GptImage2EditImageNumImages::Three, Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape4x3)) + .calculate_cost_in_cents(), 12); + } + } + + mod high_quality_tests { + use super::*; + + #[test] + fn cost_high_landscape_4x3() { + assert_eq!( + make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape4x3)) + .calculate_cost_in_cents(), 15); + } + + #[test] + fn cost_high_portrait_4x3() { + assert_eq!( + make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait4x3)) + .calculate_cost_in_cents(), 15); + } + + #[test] + fn cost_high_landscape_16x9() { + assert_eq!( + make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape16x9)) + .calculate_cost_in_cents(), 16); + } + + #[test] + fn cost_high_portrait_16x9() { + assert_eq!( + make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait16x9)) + .calculate_cost_in_cents(), 16); + } + + #[test] + fn cost_high_square() { + assert_eq!( + make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Square)) + .calculate_cost_in_cents(), 22); + } + + #[test] + fn cost_high_square_hd() { + assert_eq!( + make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::SquareHd)) + .calculate_cost_in_cents(), 23); + } + + #[test] + fn cost_high_auto() { + assert_eq!( + make_request(GptImage2EditImageNumImages::One, Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto)) + .calculate_cost_in_cents(), 23); + } + + #[test] + fn cost_high_square_hd_two_images() { + assert_eq!( + make_request(GptImage2EditImageNumImages::Two, Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::SquareHd)) + .calculate_cost_in_cents(), 46); + } + } +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_2_edit_image/mod.rs b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_2_edit_image/mod.rs new file mode 100644 index 0000000000..a0a589f2cd --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_2_edit_image/mod.rs @@ -0,0 +1,3 @@ +pub mod api; +mod raw_request; +mod cost; diff --git a/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_2_edit_image/raw_request.rs b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_2_edit_image/raw_request.rs new file mode 100644 index 0000000000..9710094767 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/edit/gpt_image_2_edit_image/raw_request.rs @@ -0,0 +1,41 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct GptImage2EditImageInput { + pub prompt: String, + + pub image_urls: Vec, + + /// The URL of the mask image to use for the generation. + /// This indicates what part of the image to edit. + #[serde(skip_serializing_if = "Option::is_none")] + pub mask_url: Option, + + /// square_hd, square, portrait_4_3, portrait_16_9, landscape_4_3, landscape_16_9, auto + #[serde(skip_serializing_if = "Option::is_none")] + pub image_size: Option, + + /// "low", "medium", "high" + #[serde(skip_serializing_if = "Option::is_none")] + pub quality: Option, + + /// 1 - 4 + /// Default: 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_images: Option, + + /// "jpeg", "png", "webp" + /// Default: "png" + #[serde(skip_serializing_if = "Option::is_none")] + pub output_format: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GptImage2EditImageFile { + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GptImage2EditImageOutput { + pub images: Vec, +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/edit/mod.rs b/crates/api_clients/fal_client/src/requests/api/image/edit/mod.rs index 7701236def..f80cf97491 100644 --- a/crates/api_clients/fal_client/src/requests/api/image/edit/mod.rs +++ b/crates/api_clients/fal_client/src/requests/api/image/edit/mod.rs @@ -1,4 +1,7 @@ pub mod flux_1_dev_edit_image; pub mod flux_1_schnell_edit_image; +pub mod gpt_image_1_edit_image; +pub mod gpt_image_1p5_edit_image; +pub mod gpt_image_2_edit_image; pub mod nano_banana_2_edit_image; pub mod nano_banana_pro_edit_image; diff --git a/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1_text_to_image/api.rs b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1_text_to_image/api.rs new file mode 100644 index 0000000000..9532d4f5eb --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1_text_to_image/api.rs @@ -0,0 +1,173 @@ +use crate::error::fal_error_plus::FalErrorPlus; +use crate::requests::api::image::text::gpt_image_1_text_to_image::raw_request::{ + GptImage1TextToImageInput, GptImage1TextToImageOutput, +}; +use crate::requests::traits::fal_endpoint_trait::FalEndpoint; + +#[derive(Clone, Debug)] +pub struct GptImage1TextToImageRequest { + /// Text prompt describing the image to generate. + pub prompt: String, + + /// Number of images to generate. + pub num_images: GptImage1TextToImageNumImages, + + /// Output image size. + pub image_size: Option, + + /// Quality level. + pub quality: Option, + + /// Background mode. + pub background: Option, + + /// Output format. + pub output_format: Option, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1TextToImageNumImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1TextToImageSize { + Auto, + /// 1024x1024 + Square, + /// 1536x1024 + Horizontal, + /// 1024x1536 + Vertical, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1TextToImageQuality { + Low, + Medium, + High, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1TextToImageBackground { + Auto, + Transparent, + Opaque, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1TextToImageOutputFormat { + Jpeg, + Png, + Webp, +} + +impl FalEndpoint for GptImage1TextToImageRequest { + const ENDPOINT: &str = "fal-ai/gpt-image-1/text-to-image"; + + type RawRequest = GptImage1TextToImageInput; + type RawResponse = GptImage1TextToImageOutput; + + fn to_raw_request(&self) -> Result { + let num_images = match self.num_images { + GptImage1TextToImageNumImages::One => 1, + GptImage1TextToImageNumImages::Two => 2, + GptImage1TextToImageNumImages::Three => 3, + GptImage1TextToImageNumImages::Four => 4, + }; + + let image_size = self.image_size.map(|s| match s { + GptImage1TextToImageSize::Auto => "auto", + GptImage1TextToImageSize::Square => "1024x1024", + GptImage1TextToImageSize::Horizontal => "1536x1024", + GptImage1TextToImageSize::Vertical => "1024x1536", + }.to_string()); + + let quality = self.quality.map(|q| match q { + GptImage1TextToImageQuality::Low => "low", + GptImage1TextToImageQuality::Medium => "medium", + GptImage1TextToImageQuality::High => "high", + }.to_string()); + + let background = self.background.map(|b| match b { + GptImage1TextToImageBackground::Auto => "auto", + GptImage1TextToImageBackground::Transparent => "transparent", + GptImage1TextToImageBackground::Opaque => "opaque", + }.to_string()); + + let output_format = Some(match self.output_format { + Some(GptImage1TextToImageOutputFormat::Jpeg) => "jpeg", + Some(GptImage1TextToImageOutputFormat::Png) => "png", + Some(GptImage1TextToImageOutputFormat::Webp) => "webp", + None => "png", + }.to_string()); + + Ok(Self::RawRequest { + prompt: self.prompt.clone(), + num_images: Some(num_images), + image_size, + quality, + background, + output_format, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::traits::fal_endpoint_trait::FalEndpoint; + use errors::AnyhowResult; + use std::fs::read_to_string; + + #[tokio::test] + #[ignore] // manually test — requires real API key, incurs costs + async fn test_text_to_image_queue() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let request = GptImage1TextToImageRequest { + prompt: "an anime girl riding on the back of a t-rex".to_string(), + num_images: GptImage1TextToImageNumImages::One, + image_size: Some(GptImage1TextToImageSize::Horizontal), + quality: Some(GptImage1TextToImageQuality::Medium), + background: None, + output_format: None, + }; + + let result = request.send_queue_request(&api_key).await?; + println!("Request ID: {}", result.request_id); + assert!(!result.request_id.is_empty()); + Ok(()) + } + + #[tokio::test] + #[ignore] // manually test — requires real API key, incurs costs + async fn test_text_to_image_webhook() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let request = GptImage1TextToImageRequest { + prompt: "a corgi wearing sunglasses at the beach".to_string(), + num_images: GptImage1TextToImageNumImages::Two, + image_size: Some(GptImage1TextToImageSize::Square), + quality: Some(GptImage1TextToImageQuality::High), + background: Some(GptImage1TextToImageBackground::Opaque), + output_format: Some(GptImage1TextToImageOutputFormat::Png), + }; + + let result = request.send_webhook_request( + &api_key, + "https://example.com/webhook", + ).await?; + println!("Request ID: {:?}", result.request_id); + assert!(result.request_id.is_some()); + Ok(()) + } + + // NB: Pricing tests are in cost.rs +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1_text_to_image/cost.rs b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1_text_to_image/cost.rs new file mode 100644 index 0000000000..fce8ec7ee9 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1_text_to_image/cost.rs @@ -0,0 +1,179 @@ +use crate::requests::api::image::text::gpt_image_1_text_to_image::api::{ + GptImage1TextToImageNumImages, GptImage1TextToImageQuality, + GptImage1TextToImageRequest, GptImage1TextToImageSize, +}; +use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; + +impl FalRequestCostCalculator for GptImage1TextToImageRequest { + fn calculate_cost_in_cents(&self) -> UsdCents { + // Per fal docs (fal-ai/gpt-image-1/text-to-image): + // Low: $0.011 (1024x1024) / $0.016 (other) per image + // Medium: $0.042 (1024x1024) / $0.063 (other) per image + // High: $0.167 (1024x1024) / $0.25 (other) per image + // Default quality is Medium when unspecified, square when size unspecified. + let use_quality = self.quality.unwrap_or(GptImage1TextToImageQuality::Medium); + let is_square = matches!( + self.image_size, + None | Some(GptImage1TextToImageSize::Square) | Some(GptImage1TextToImageSize::Auto) + ); + + let base_cost: u64 = match (use_quality, is_square) { + (GptImage1TextToImageQuality::Low, true) => 2, + (GptImage1TextToImageQuality::Low, false) => 2, + (GptImage1TextToImageQuality::Medium, true) => 5, + (GptImage1TextToImageQuality::Medium, false) => 7, + (GptImage1TextToImageQuality::High, true) => 17, + (GptImage1TextToImageQuality::High, false) => 25, + }; + + let n: u64 = match self.num_images { + GptImage1TextToImageNumImages::One => 1, + GptImage1TextToImageNumImages::Two => 2, + GptImage1TextToImageNumImages::Three => 3, + GptImage1TextToImageNumImages::Four => 4, + }; + base_cost * n + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_request( + num_images: GptImage1TextToImageNumImages, + quality: Option, + image_size: Option, + ) -> GptImage1TextToImageRequest { + GptImage1TextToImageRequest { + prompt: "test".to_string(), + num_images, + image_size, + quality, + background: None, + output_format: None, + } + } + + mod default_tests { + use super::*; + + #[test] + fn cost_defaults_one_image() { + // Default quality=Medium, size=Square => 5 cents + assert_eq!( + make_request(GptImage1TextToImageNumImages::One, None, None) + .calculate_cost_in_cents(), 5); + } + + #[test] + fn cost_defaults_four_images() { + assert_eq!( + make_request(GptImage1TextToImageNumImages::Four, None, None) + .calculate_cost_in_cents(), 20); + } + } + + mod low_quality_tests { + use super::*; + + #[test] + fn cost_low_square_one_image() { + assert_eq!( + make_request(GptImage1TextToImageNumImages::One, Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Square)) + .calculate_cost_in_cents(), 2); + } + + #[test] + fn cost_low_horizontal_one_image() { + assert_eq!( + make_request(GptImage1TextToImageNumImages::One, Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Horizontal)) + .calculate_cost_in_cents(), 2); + } + + #[test] + fn cost_low_auto_four_images() { + assert_eq!( + make_request(GptImage1TextToImageNumImages::Four, Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Auto)) + .calculate_cost_in_cents(), 8); + } + } + + mod medium_quality_tests { + use super::*; + + #[test] + fn cost_medium_square_one_image() { + assert_eq!( + make_request(GptImage1TextToImageNumImages::One, Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Square)) + .calculate_cost_in_cents(), 5); + } + + #[test] + fn cost_medium_auto_one_image() { + assert_eq!( + make_request(GptImage1TextToImageNumImages::One, Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Auto)) + .calculate_cost_in_cents(), 5); + } + + #[test] + fn cost_medium_horizontal_one_image() { + assert_eq!( + make_request(GptImage1TextToImageNumImages::One, Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Horizontal)) + .calculate_cost_in_cents(), 7); + } + + #[test] + fn cost_medium_vertical_one_image() { + assert_eq!( + make_request(GptImage1TextToImageNumImages::One, Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Vertical)) + .calculate_cost_in_cents(), 7); + } + + #[test] + fn cost_medium_horizontal_three_images() { + assert_eq!( + make_request(GptImage1TextToImageNumImages::Three, Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Horizontal)) + .calculate_cost_in_cents(), 21); + } + } + + mod high_quality_tests { + use super::*; + + #[test] + fn cost_high_square_one_image() { + assert_eq!( + make_request(GptImage1TextToImageNumImages::One, Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Square)) + .calculate_cost_in_cents(), 17); + } + + #[test] + fn cost_high_horizontal_one_image() { + assert_eq!( + make_request(GptImage1TextToImageNumImages::One, Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Horizontal)) + .calculate_cost_in_cents(), 25); + } + + #[test] + fn cost_high_vertical_one_image() { + assert_eq!( + make_request(GptImage1TextToImageNumImages::One, Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Vertical)) + .calculate_cost_in_cents(), 25); + } + + #[test] + fn cost_high_square_two_images() { + assert_eq!( + make_request(GptImage1TextToImageNumImages::Two, Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Square)) + .calculate_cost_in_cents(), 34); + } + + #[test] + fn cost_high_vertical_four_images() { + assert_eq!( + make_request(GptImage1TextToImageNumImages::Four, Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Vertical)) + .calculate_cost_in_cents(), 100); + } + } +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1_text_to_image/mod.rs b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1_text_to_image/mod.rs new file mode 100644 index 0000000000..a0a589f2cd --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1_text_to_image/mod.rs @@ -0,0 +1,3 @@ +pub mod api; +mod raw_request; +mod cost; diff --git a/crates/api_clients/fal_client/src/requests/http/image/text/http_gpt_image_1_non_byok_text_to_image.rs b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1_text_to_image/raw_request.rs similarity index 55% rename from crates/api_clients/fal_client/src/requests/http/image/text/http_gpt_image_1_non_byok_text_to_image.rs rename to crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1_text_to_image/raw_request.rs index 9199155247..58eb916026 100644 --- a/crates/api_clients/fal_client/src/requests/http/image/text/http_gpt_image_1_non_byok_text_to_image.rs +++ b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1_text_to_image/raw_request.rs @@ -1,12 +1,7 @@ -use crate::requests::core_api::fal_request::FalRequest; use serde::{Deserialize, Serialize}; -/// Non-BYOK text-to-image binding for `fal-ai/gpt-image-1/text-to-image`. -/// -/// Fal hosts this model directly (no caller-supplied OpenAI API key); pricing -/// is billed by Fal at their published rates. #[derive(Debug, Serialize, Deserialize, Default)] -pub struct GptImage1NonByokTextToImageInput { +pub struct GptImage1TextToImageInput { pub prompt: String, /// "auto", "1024x1024", "1536x1024", "1024x1536" @@ -33,17 +28,11 @@ pub struct GptImage1NonByokTextToImageInput { } #[derive(Debug, Serialize, Deserialize)] -pub struct GptImage1NonByokTextToImageFile { +pub struct GptImage1TextToImageFile { pub url: String, } #[derive(Debug, Serialize, Deserialize)] -pub struct GptImage1NonByokTextToImageOutput { - pub images: Vec, -} - -pub fn gpt_image_1_non_byok_text_to_image( - params: GptImage1NonByokTextToImageInput, -) -> FalRequest { - FalRequest::new("fal-ai/gpt-image-1/text-to-image", params) +pub struct GptImage1TextToImageOutput { + pub images: Vec, } diff --git a/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1p5_text_to_image/api.rs b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1p5_text_to_image/api.rs new file mode 100644 index 0000000000..09d74343de --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1p5_text_to_image/api.rs @@ -0,0 +1,171 @@ +use crate::error::fal_error_plus::FalErrorPlus; +use crate::requests::api::image::text::gpt_image_1p5_text_to_image::raw_request::{ + GptImage1p5TextToImageInput, GptImage1p5TextToImageOutput, +}; +use crate::requests::traits::fal_endpoint_trait::FalEndpoint; + +#[derive(Clone, Debug)] +pub struct GptImage1p5TextToImageRequest { + /// Text prompt describing the image to generate. + pub prompt: String, + + /// Number of images to generate. + pub num_images: GptImage1p5TextToImageNumImages, + + /// Output image size. + pub image_size: Option, + + /// Background mode. + pub background: Option, + + /// Quality level. + pub quality: Option, + + /// Output format. + pub output_format: Option, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1p5TextToImageNumImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1p5TextToImageSize { + /// 1024x1024 + Square, + /// 1536x1024 + Wide, + /// 1024x1536 + Tall, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1p5TextToImageBackground { + Auto, + Transparent, + Opaque, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1p5TextToImageQuality { + Low, + Medium, + High, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage1p5TextToImageOutputFormat { + Jpeg, + Png, + Webp, +} + +impl FalEndpoint for GptImage1p5TextToImageRequest { + const ENDPOINT: &str = "fal-ai/gpt-image-1.5"; + + type RawRequest = GptImage1p5TextToImageInput; + type RawResponse = GptImage1p5TextToImageOutput; + + fn to_raw_request(&self) -> Result { + let num_images = match self.num_images { + GptImage1p5TextToImageNumImages::One => 1, + GptImage1p5TextToImageNumImages::Two => 2, + GptImage1p5TextToImageNumImages::Three => 3, + GptImage1p5TextToImageNumImages::Four => 4, + }; + + let image_size = self.image_size.map(|s| match s { + GptImage1p5TextToImageSize::Square => "1024x1024", + GptImage1p5TextToImageSize::Wide => "1536x1024", + GptImage1p5TextToImageSize::Tall => "1024x1536", + }.to_string()); + + let background = self.background.map(|b| match b { + GptImage1p5TextToImageBackground::Auto => "auto", + GptImage1p5TextToImageBackground::Transparent => "transparent", + GptImage1p5TextToImageBackground::Opaque => "opaque", + }.to_string()); + + let quality = self.quality.map(|q| match q { + GptImage1p5TextToImageQuality::Low => "low", + GptImage1p5TextToImageQuality::Medium => "medium", + GptImage1p5TextToImageQuality::High => "high", + }.to_string()); + + let output_format = Some(match self.output_format { + Some(GptImage1p5TextToImageOutputFormat::Jpeg) => "jpeg", + Some(GptImage1p5TextToImageOutputFormat::Png) => "png", + Some(GptImage1p5TextToImageOutputFormat::Webp) => "webp", + None => "png", + }.to_string()); + + Ok(Self::RawRequest { + prompt: self.prompt.clone(), + num_images: Some(num_images), + image_size, + background, + quality, + output_format, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::traits::fal_endpoint_trait::FalEndpoint; + use errors::AnyhowResult; + use std::fs::read_to_string; + + #[tokio::test] + #[ignore] // manually test — requires real API key, incurs costs + async fn test_text_to_image_queue() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let request = GptImage1p5TextToImageRequest { + prompt: "an anime girl riding on the back of a t-rex".to_string(), + num_images: GptImage1p5TextToImageNumImages::One, + image_size: None, + background: None, + quality: None, + output_format: None, + }; + + let result = request.send_queue_request(&api_key).await?; + println!("Request ID: {}", result.request_id); + assert!(!result.request_id.is_empty()); + Ok(()) + } + + #[tokio::test] + #[ignore] // manually test — requires real API key, incurs costs + async fn test_text_to_image_webhook() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let request = GptImage1p5TextToImageRequest { + prompt: "a corgi wearing sunglasses at the beach".to_string(), + num_images: GptImage1p5TextToImageNumImages::Two, + image_size: Some(GptImage1p5TextToImageSize::Wide), + background: None, + quality: Some(GptImage1p5TextToImageQuality::High), + output_format: Some(GptImage1p5TextToImageOutputFormat::Png), + }; + + let result = request.send_webhook_request( + &api_key, + "https://example.com/webhook", + ).await?; + println!("Request ID: {:?}", result.request_id); + assert!(result.request_id.is_some()); + Ok(()) + } + + // NB: Pricing tests are in cost.rs +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1p5_text_to_image/cost.rs b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1p5_text_to_image/cost.rs new file mode 100644 index 0000000000..be31d8f3c0 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1p5_text_to_image/cost.rs @@ -0,0 +1,168 @@ +use crate::requests::api::image::text::gpt_image_1p5_text_to_image::api::{ + GptImage1p5TextToImageNumImages, GptImage1p5TextToImageQuality, + GptImage1p5TextToImageRequest, GptImage1p5TextToImageSize, +}; +use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; + +impl FalRequestCostCalculator for GptImage1p5TextToImageRequest { + fn calculate_cost_in_cents(&self) -> UsdCents { + // Per fal docs (fal-ai/gpt-image-1.5): + // Low: $0.009 (1024x1024) / $0.013 (other) per image + // Medium: $0.034 (1024x1024) / $0.050-$0.051 (other) per image + // High: $0.133 (1024x1024) / $0.199-$0.200 (other) per image + let use_quality = self.quality.unwrap_or(GptImage1p5TextToImageQuality::Medium); + let use_size = self.image_size.unwrap_or(GptImage1p5TextToImageSize::Square); + + let base_cost = match (use_quality, use_size) { + (GptImage1p5TextToImageQuality::Low, GptImage1p5TextToImageSize::Square) => 1, + (GptImage1p5TextToImageQuality::Low, _) => 1, + (GptImage1p5TextToImageQuality::Medium, GptImage1p5TextToImageSize::Square) => 3, + (GptImage1p5TextToImageQuality::Medium, _) => 5, + (GptImage1p5TextToImageQuality::High, GptImage1p5TextToImageSize::Square) => 13, + (GptImage1p5TextToImageQuality::High, _) => 20, + }; + + let cost = match self.num_images { + GptImage1p5TextToImageNumImages::One => base_cost, + GptImage1p5TextToImageNumImages::Two => base_cost * 2, + GptImage1p5TextToImageNumImages::Three => base_cost * 3, + GptImage1p5TextToImageNumImages::Four => base_cost * 4, + }; + cost as UsdCents + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_request( + num_images: GptImage1p5TextToImageNumImages, + quality: Option, + image_size: Option, + ) -> GptImage1p5TextToImageRequest { + GptImage1p5TextToImageRequest { + prompt: "test".to_string(), + num_images, + image_size, + background: None, + quality, + output_format: None, + } + } + + mod default_tests { + use super::*; + + #[test] + fn cost_defaults_one_image() { + // Default quality=Medium, size=Square => 3 cents + assert_eq!( + make_request(GptImage1p5TextToImageNumImages::One, None, None) + .calculate_cost_in_cents(), 3); + } + + #[test] + fn cost_defaults_four_images() { + assert_eq!( + make_request(GptImage1p5TextToImageNumImages::Four, None, None) + .calculate_cost_in_cents(), 12); + } + } + + mod low_quality_tests { + use super::*; + + #[test] + fn cost_low_square_one_image() { + assert_eq!( + make_request(GptImage1p5TextToImageNumImages::One, Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Square)) + .calculate_cost_in_cents(), 1); + } + + #[test] + fn cost_low_wide_one_image() { + assert_eq!( + make_request(GptImage1p5TextToImageNumImages::One, Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Wide)) + .calculate_cost_in_cents(), 1); + } + + #[test] + fn cost_low_tall_four_images() { + assert_eq!( + make_request(GptImage1p5TextToImageNumImages::Four, Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Tall)) + .calculate_cost_in_cents(), 4); + } + } + + mod medium_quality_tests { + use super::*; + + #[test] + fn cost_medium_square_one_image() { + assert_eq!( + make_request(GptImage1p5TextToImageNumImages::One, Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Square)) + .calculate_cost_in_cents(), 3); + } + + #[test] + fn cost_medium_wide_one_image() { + assert_eq!( + make_request(GptImage1p5TextToImageNumImages::One, Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Wide)) + .calculate_cost_in_cents(), 5); + } + + #[test] + fn cost_medium_tall_one_image() { + assert_eq!( + make_request(GptImage1p5TextToImageNumImages::One, Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Tall)) + .calculate_cost_in_cents(), 5); + } + + #[test] + fn cost_medium_square_three_images() { + assert_eq!( + make_request(GptImage1p5TextToImageNumImages::Three, Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Square)) + .calculate_cost_in_cents(), 9); + } + } + + mod high_quality_tests { + use super::*; + + #[test] + fn cost_high_square_one_image() { + assert_eq!( + make_request(GptImage1p5TextToImageNumImages::One, Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Square)) + .calculate_cost_in_cents(), 13); + } + + #[test] + fn cost_high_wide_one_image() { + assert_eq!( + make_request(GptImage1p5TextToImageNumImages::One, Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Wide)) + .calculate_cost_in_cents(), 20); + } + + #[test] + fn cost_high_tall_one_image() { + assert_eq!( + make_request(GptImage1p5TextToImageNumImages::One, Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Tall)) + .calculate_cost_in_cents(), 20); + } + + #[test] + fn cost_high_square_two_images() { + assert_eq!( + make_request(GptImage1p5TextToImageNumImages::Two, Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Square)) + .calculate_cost_in_cents(), 26); + } + + #[test] + fn cost_high_wide_four_images() { + assert_eq!( + make_request(GptImage1p5TextToImageNumImages::Four, Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Wide)) + .calculate_cost_in_cents(), 80); + } + } +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1p5_text_to_image/mod.rs b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1p5_text_to_image/mod.rs new file mode 100644 index 0000000000..a0a589f2cd --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1p5_text_to_image/mod.rs @@ -0,0 +1,3 @@ +pub mod api; +mod raw_request; +mod cost; diff --git a/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1p5_text_to_image/raw_request.rs b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1p5_text_to_image/raw_request.rs new file mode 100644 index 0000000000..b02a440766 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_1p5_text_to_image/raw_request.rs @@ -0,0 +1,38 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct GptImage1p5TextToImageInput { + pub prompt: String, + + /// "1024x1024", "1536x1024", "1024x1536" + #[serde(skip_serializing_if = "Option::is_none")] + pub image_size: Option, + + /// "auto", "transparent", "opaque" + #[serde(skip_serializing_if = "Option::is_none")] + pub background: Option, + + /// "low", "medium", "high" + #[serde(skip_serializing_if = "Option::is_none")] + pub quality: Option, + + /// 1 - 4 + /// Default: 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_images: Option, + + /// "jpeg", "png", "webp" + /// Default: "png" + #[serde(skip_serializing_if = "Option::is_none")] + pub output_format: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GptImage1p5TextToImageFile { + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GptImage1p5TextToImageOutput { + pub images: Vec, +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_2_text_to_image/api.rs b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_2_text_to_image/api.rs new file mode 100644 index 0000000000..2fa01e194d --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_2_text_to_image/api.rs @@ -0,0 +1,155 @@ +use crate::error::fal_error_plus::FalErrorPlus; +use crate::requests::api::image::text::gpt_image_2_text_to_image::raw_request::{ + GptImage2TextToImageInput, GptImage2TextToImageOutput, +}; +use crate::requests::traits::fal_endpoint_trait::FalEndpoint; + +#[derive(Clone, Debug)] +pub struct GptImage2TextToImageRequest { + /// Text prompt describing the image to generate. + pub prompt: String, + + /// Number of images to generate. + pub num_images: GptImage2TextToImageNumImages, + + /// Output image size. + pub image_size: Option, + + /// Quality level. + pub quality: Option, + + /// Output format. + pub output_format: Option, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage2TextToImageNumImages { + One, + Two, + Three, + Four, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage2TextToImageSize { + SquareHd, + Square, + Portrait4x3, + Portrait16x9, + Landscape4x3, + Landscape16x9, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage2TextToImageQuality { + Low, + Medium, + High, +} + +#[derive(Copy, Clone, Debug)] +pub enum GptImage2TextToImageOutputFormat { + Jpeg, + Png, + Webp, +} + +impl FalEndpoint for GptImage2TextToImageRequest { + const ENDPOINT: &str = "openai/gpt-image-2"; + + type RawRequest = GptImage2TextToImageInput; + type RawResponse = GptImage2TextToImageOutput; + + fn to_raw_request(&self) -> Result { + let num_images = match self.num_images { + GptImage2TextToImageNumImages::One => 1, + GptImage2TextToImageNumImages::Two => 2, + GptImage2TextToImageNumImages::Three => 3, + GptImage2TextToImageNumImages::Four => 4, + }; + + let image_size = self.image_size.map(|s| match s { + GptImage2TextToImageSize::SquareHd => "square_hd", + GptImage2TextToImageSize::Square => "square", + GptImage2TextToImageSize::Portrait4x3 => "portrait_4_3", + GptImage2TextToImageSize::Portrait16x9 => "portrait_16_9", + GptImage2TextToImageSize::Landscape4x3 => "landscape_4_3", + GptImage2TextToImageSize::Landscape16x9 => "landscape_16_9", + }.to_string()); + + let quality = self.quality.map(|q| match q { + GptImage2TextToImageQuality::Low => "low", + GptImage2TextToImageQuality::Medium => "medium", + GptImage2TextToImageQuality::High => "high", + }.to_string()); + + let output_format = Some(match self.output_format { + Some(GptImage2TextToImageOutputFormat::Jpeg) => "jpeg", + Some(GptImage2TextToImageOutputFormat::Png) => "png", + Some(GptImage2TextToImageOutputFormat::Webp) => "webp", + None => "png", + }.to_string()); + + Ok(Self::RawRequest { + prompt: self.prompt.clone(), + num_images: Some(num_images), + image_size, + quality, + output_format, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::creds::fal_api_key::FalApiKey; + use crate::requests::traits::fal_endpoint_trait::FalEndpoint; + use errors::AnyhowResult; + use std::fs::read_to_string; + + #[tokio::test] + #[ignore] // manually test — requires real API key, incurs costs + async fn test_text_to_image_queue() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let request = GptImage2TextToImageRequest { + prompt: "an anime girl riding on the back of a t-rex".to_string(), + num_images: GptImage2TextToImageNumImages::One, + image_size: None, + quality: None, + output_format: None, + }; + + let result = request.send_queue_request(&api_key).await?; + println!("Request ID: {}", result.request_id); + assert!(!result.request_id.is_empty()); + Ok(()) + } + + #[tokio::test] + #[ignore] // manually test — requires real API key, incurs costs + async fn test_text_to_image_webhook() -> AnyhowResult<()> { + let secret = read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")?; + let api_key = FalApiKey::from_str(&secret); + + let request = GptImage2TextToImageRequest { + prompt: "a corgi wearing sunglasses at the beach".to_string(), + num_images: GptImage2TextToImageNumImages::Two, + image_size: Some(GptImage2TextToImageSize::Landscape16x9), + quality: Some(GptImage2TextToImageQuality::High), + output_format: Some(GptImage2TextToImageOutputFormat::Png), + }; + + let result = request.send_webhook_request( + &api_key, + "https://example.com/webhook", + ).await?; + println!("Request ID: {:?}", result.request_id); + assert!(result.request_id.is_some()); + Ok(()) + } + + // NB: Pricing tests are in cost.rs +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_2_text_to_image/cost.rs b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_2_text_to_image/cost.rs new file mode 100644 index 0000000000..4ba2cb36df --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_2_text_to_image/cost.rs @@ -0,0 +1,185 @@ +use crate::requests::api::image::text::gpt_image_2_text_to_image::api::{ + GptImage2TextToImageNumImages, GptImage2TextToImageQuality, + GptImage2TextToImageRequest, GptImage2TextToImageSize, +}; +use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; + +impl FalRequestCostCalculator for GptImage2TextToImageRequest { + fn calculate_cost_in_cents(&self) -> UsdCents { + // Cost table (per image) by approximate pixel dimensions: + // + // landscape_4_3 (~1024x768): low=$0.01, medium=$0.04, high=$0.15 + // square (~1024x1024): low=$0.01, medium=$0.06, high=$0.22 + // portrait_4_3 (~768x1024): low=$0.01, medium=$0.04, high=$0.15 + // landscape_16_9(~1920x1080): low=$0.01, medium=$0.04, high=$0.16 + // portrait_16_9 (~1080x1920): low=$0.01, medium=$0.04, high=$0.16 + // square_hd (~2048x2048): low=$0.01, medium=$0.06, high=$0.23 + // auto (varies): estimated as square + let use_quality = self.quality.unwrap_or(GptImage2TextToImageQuality::High); + let use_size = self.image_size.unwrap_or(GptImage2TextToImageSize::Square); + + use GptImage2TextToImageQuality::*; + use GptImage2TextToImageSize::*; + + let base_cost = match (use_quality, use_size) { + (Low, _) => 1, + (Medium, Landscape4x3 | Portrait4x3 | Landscape16x9 | Portrait16x9) => 4, + (Medium, Square) => 6, + (Medium, SquareHd) => 6, + (High, Landscape4x3 | Portrait4x3) => 15, + (High, Landscape16x9 | Portrait16x9) => 16, + (High, Square) => 22, + (High, SquareHd) => 23, + }; + + let cost = match self.num_images { + GptImage2TextToImageNumImages::One => base_cost, + GptImage2TextToImageNumImages::Two => base_cost * 2, + GptImage2TextToImageNumImages::Three => base_cost * 3, + GptImage2TextToImageNumImages::Four => base_cost * 4, + }; + cost as UsdCents + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_request( + num_images: GptImage2TextToImageNumImages, + quality: Option, + image_size: Option, + ) -> GptImage2TextToImageRequest { + GptImage2TextToImageRequest { + prompt: "test".to_string(), + num_images, + image_size, + quality, + output_format: None, + } + } + + mod default_tests { + use super::*; + + #[test] + fn cost_defaults_one_image() { + // Default quality=High, size=Square => 22 cents + assert_eq!( + make_request(GptImage2TextToImageNumImages::One, None, None) + .calculate_cost_in_cents(), 22); + } + + #[test] + fn cost_defaults_four_images() { + assert_eq!( + make_request(GptImage2TextToImageNumImages::Four, None, None) + .calculate_cost_in_cents(), 88); + } + } + + mod low_quality_tests { + use super::*; + + #[test] + fn cost_low_any_size_one_image() { + // Low quality is always 1 cent per image regardless of size + assert_eq!( + make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::SquareHd)) + .calculate_cost_in_cents(), 1); + } + + #[test] + fn cost_low_any_size_four_images() { + assert_eq!( + make_request(GptImage2TextToImageNumImages::Four, Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Square)) + .calculate_cost_in_cents(), 4); + } + } + + mod medium_quality_tests { + use super::*; + + #[test] + fn cost_medium_landscape_4x3() { + assert_eq!( + make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape4x3)) + .calculate_cost_in_cents(), 4); + } + + #[test] + fn cost_medium_square() { + assert_eq!( + make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Square)) + .calculate_cost_in_cents(), 6); + } + + #[test] + fn cost_medium_square_hd() { + assert_eq!( + make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::SquareHd)) + .calculate_cost_in_cents(), 6); + } + + #[test] + fn cost_medium_portrait_16x9_two_images() { + assert_eq!( + make_request(GptImage2TextToImageNumImages::Two, Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait16x9)) + .calculate_cost_in_cents(), 8); + } + } + + mod high_quality_tests { + use super::*; + + #[test] + fn cost_high_landscape_4x3() { + assert_eq!( + make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape4x3)) + .calculate_cost_in_cents(), 15); + } + + #[test] + fn cost_high_portrait_4x3() { + assert_eq!( + make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait4x3)) + .calculate_cost_in_cents(), 15); + } + + #[test] + fn cost_high_landscape_16x9() { + assert_eq!( + make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape16x9)) + .calculate_cost_in_cents(), 16); + } + + #[test] + fn cost_high_portrait_16x9() { + assert_eq!( + make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait16x9)) + .calculate_cost_in_cents(), 16); + } + + #[test] + fn cost_high_square() { + assert_eq!( + make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Square)) + .calculate_cost_in_cents(), 22); + } + + #[test] + fn cost_high_square_hd() { + assert_eq!( + make_request(GptImage2TextToImageNumImages::One, Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::SquareHd)) + .calculate_cost_in_cents(), 23); + } + + #[test] + fn cost_high_square_hd_three_images() { + assert_eq!( + make_request(GptImage2TextToImageNumImages::Three, Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::SquareHd)) + .calculate_cost_in_cents(), 69); + } + } +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_2_text_to_image/mod.rs b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_2_text_to_image/mod.rs new file mode 100644 index 0000000000..a0a589f2cd --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_2_text_to_image/mod.rs @@ -0,0 +1,3 @@ +pub mod api; +mod raw_request; +mod cost; diff --git a/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_2_text_to_image/raw_request.rs b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_2_text_to_image/raw_request.rs new file mode 100644 index 0000000000..30c52afca2 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/api/image/text/gpt_image_2_text_to_image/raw_request.rs @@ -0,0 +1,34 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct GptImage2TextToImageInput { + pub prompt: String, + + /// square_hd, square, portrait_4_3, portrait_16_9, landscape_4_3, landscape_16_9 + #[serde(skip_serializing_if = "Option::is_none")] + pub image_size: Option, + + /// "low", "medium", "high" + #[serde(skip_serializing_if = "Option::is_none")] + pub quality: Option, + + /// 1 - 4 + /// Default: 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_images: Option, + + /// "jpeg", "png", "webp" + /// Default: "png" + #[serde(skip_serializing_if = "Option::is_none")] + pub output_format: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GptImage2TextToImageFile { + pub url: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GptImage2TextToImageOutput { + pub images: Vec, +} diff --git a/crates/api_clients/fal_client/src/requests/api/image/text/mod.rs b/crates/api_clients/fal_client/src/requests/api/image/text/mod.rs index 8a70cb773f..076ab1b390 100644 --- a/crates/api_clients/fal_client/src/requests/api/image/text/mod.rs +++ b/crates/api_clients/fal_client/src/requests/api/image/text/mod.rs @@ -1,4 +1,7 @@ pub mod flux_1_dev_text_to_image; pub mod flux_1_schnell_text_to_image; +pub mod gpt_image_1_text_to_image; +pub mod gpt_image_1p5_text_to_image; +pub mod gpt_image_2_text_to_image; pub mod nano_banana_2_text_to_image; pub mod nano_banana_pro_text_to_image; diff --git a/crates/api_clients/fal_client/src/requests/http/image/edit/http_gpt_image_1_byok_edit_image.rs b/crates/api_clients/fal_client/src/requests/http/image/edit/http_gpt_image_1_byok_edit_image.rs new file mode 100644 index 0000000000..3ad2643412 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/http/image/edit/http_gpt_image_1_byok_edit_image.rs @@ -0,0 +1,32 @@ +use crate::requests::core_api::fal_request::FalRequest; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct GptImage1ByokEditImageInput { + pub prompt: String, + + pub image_urls: Vec, + + /// "auto", "1024x1024", "1536x1024", "1024x1536" + pub image_size: String, + + /// 1 - 4 + pub num_images: u8, + + /// "auto", "low", "medium", "high" + pub quality: String, + + /// BYOK - caller supplies their own OpenAI API key + pub openai_api_key: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GptImage1ByokEditImageOutput { + // Response fields parsed elsewhere +} + +pub fn gpt_image_1_byok_edit_image( + params: GptImage1ByokEditImageInput, +) -> FalRequest { + FalRequest::new("fal-ai/gpt-image-1/edit-image/byok", params) +} diff --git a/crates/api_clients/fal_client/src/requests/http/image/edit/http_gpt_image_1_edit_image.rs b/crates/api_clients/fal_client/src/requests/http/image/edit/http_gpt_image_1_edit_image.rs index e34e4b8982..49b7b61d11 100644 --- a/crates/api_clients/fal_client/src/requests/http/image/edit/http_gpt_image_1_edit_image.rs +++ b/crates/api_clients/fal_client/src/requests/http/image/edit/http_gpt_image_1_edit_image.rs @@ -1,32 +1,57 @@ use crate::requests::core_api::fal_request::FalRequest; use serde::{Deserialize, Serialize}; +/// Non-BYOK edit-image (image-to-image) binding for +/// `fal-ai/gpt-image-1/edit-image`. Fal hosts this model directly; pricing is +/// billed by Fal at their published rates. #[derive(Debug, Serialize, Deserialize, Default)] pub struct GptImage1EditImageInput { pub prompt: String, pub image_urls: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub mask_image_url: Option, + /// "auto", "1024x1024", "1536x1024", "1024x1536" - pub image_size: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub image_size: Option, + + /// "low", "medium", "high" + #[serde(skip_serializing_if = "Option::is_none")] + pub quality: Option, + + /// "low", "high" + #[serde(skip_serializing_if = "Option::is_none")] + pub input_fidelity: Option, /// 1 - 4 - pub num_images: u8, + /// Default: 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_images: Option, - /// "auto", "low", "medium", "high" - pub quality: String, + /// "auto", "transparent", "opaque" + #[serde(skip_serializing_if = "Option::is_none")] + pub background: Option, - /// BYOK - caller supplies their own OpenAI API key - pub openai_api_key: String, + /// "jpeg", "png", "webp" + /// Default: "png" + #[serde(skip_serializing_if = "Option::is_none")] + pub output_format: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GptImage1EditImageFile { + pub url: String, } #[derive(Debug, Serialize, Deserialize)] pub struct GptImage1EditImageOutput { - // Response fields parsed elsewhere + pub images: Vec, } pub fn gpt_image_1_edit_image( params: GptImage1EditImageInput, ) -> FalRequest { - FalRequest::new("fal-ai/gpt-image-1/edit-image/byok", params) + FalRequest::new("fal-ai/gpt-image-1/edit-image", params) } diff --git a/crates/api_clients/fal_client/src/requests/http/image/edit/mod.rs b/crates/api_clients/fal_client/src/requests/http/image/edit/mod.rs index 09639cae82..326291db24 100644 --- a/crates/api_clients/fal_client/src/requests/http/image/edit/mod.rs +++ b/crates/api_clients/fal_client/src/requests/http/image/edit/mod.rs @@ -2,8 +2,8 @@ pub mod http_flux_1_dev_edit_image; pub mod http_flux_1_schnell_edit_image; pub mod http_flux_pro_kontext_max_edit; pub mod http_gemini_25_flash_edit; +pub mod http_gpt_image_1_byok_edit_image; pub mod http_gpt_image_1_edit_image; -pub mod http_gpt_image_1_non_byok_edit_image; pub mod http_gpt_image_1p5_edit_image; pub mod http_gpt_image_2_edit_image; pub mod http_nano_banana_2_edit_image; diff --git a/crates/api_clients/fal_client/src/requests/http/image/text/http_gpt_image_1_byok_text_to_image.rs b/crates/api_clients/fal_client/src/requests/http/image/text/http_gpt_image_1_byok_text_to_image.rs new file mode 100644 index 0000000000..6dec979733 --- /dev/null +++ b/crates/api_clients/fal_client/src/requests/http/image/text/http_gpt_image_1_byok_text_to_image.rs @@ -0,0 +1,30 @@ +use crate::requests::core_api::fal_request::FalRequest; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct GptImage1ByokTextToImageInput { + pub prompt: String, + + /// "auto", "1024x1024", "1536x1024", "1024x1536" + pub image_size: String, + + /// 1 - 4 + pub num_images: u8, + + /// "auto", "low", "medium", "high" + pub quality: String, + + /// BYOK - caller supplies their own OpenAI API key + pub openai_api_key: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GptImage1ByokTextToImageOutput { + // Response fields parsed elsewhere +} + +pub fn gpt_image_1_byok_text_to_image( + params: GptImage1ByokTextToImageInput, +) -> FalRequest { + FalRequest::new("fal-ai/gpt-image-1/text-to-image/byok", params) +} diff --git a/crates/api_clients/fal_client/src/requests/http/image/text/http_gpt_image_1_text_to_image.rs b/crates/api_clients/fal_client/src/requests/http/image/text/http_gpt_image_1_text_to_image.rs index cdd21f537f..f564a7257c 100644 --- a/crates/api_clients/fal_client/src/requests/http/image/text/http_gpt_image_1_text_to_image.rs +++ b/crates/api_clients/fal_client/src/requests/http/image/text/http_gpt_image_1_text_to_image.rs @@ -1,30 +1,49 @@ use crate::requests::core_api::fal_request::FalRequest; use serde::{Deserialize, Serialize}; +/// Non-BYOK text-to-image binding for `fal-ai/gpt-image-1/text-to-image`. +/// +/// Fal hosts this model directly (no caller-supplied OpenAI API key); pricing +/// is billed by Fal at their published rates. #[derive(Debug, Serialize, Deserialize, Default)] pub struct GptImage1TextToImageInput { pub prompt: String, /// "auto", "1024x1024", "1536x1024", "1024x1536" - pub image_size: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub image_size: Option, + + /// "low", "medium", "high" + #[serde(skip_serializing_if = "Option::is_none")] + pub quality: Option, /// 1 - 4 - pub num_images: u8, + /// Default: 1 + #[serde(skip_serializing_if = "Option::is_none")] + pub num_images: Option, - /// "auto", "low", "medium", "high" - pub quality: String, + /// "auto", "transparent", "opaque" + #[serde(skip_serializing_if = "Option::is_none")] + pub background: Option, - /// BYOK - caller supplies their own OpenAI API key - pub openai_api_key: String, + /// "jpeg", "png", "webp" + /// Default: "png" + #[serde(skip_serializing_if = "Option::is_none")] + pub output_format: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct GptImage1TextToImageFile { + pub url: String, } #[derive(Debug, Serialize, Deserialize)] pub struct GptImage1TextToImageOutput { - // Response fields parsed elsewhere + pub images: Vec, } pub fn gpt_image_1_text_to_image( params: GptImage1TextToImageInput, ) -> FalRequest { - FalRequest::new("fal-ai/gpt-image-1/text-to-image/byok", params) + FalRequest::new("fal-ai/gpt-image-1/text-to-image", params) } diff --git a/crates/api_clients/fal_client/src/requests/http/image/text/mod.rs b/crates/api_clients/fal_client/src/requests/http/image/text/mod.rs index 4b3edbfd1e..3c7f5da210 100644 --- a/crates/api_clients/fal_client/src/requests/http/image/text/mod.rs +++ b/crates/api_clients/fal_client/src/requests/http/image/text/mod.rs @@ -3,8 +3,8 @@ pub mod http_flux_1_schnell_text_to_image; pub mod http_flux_pro_11_text_to_image; pub mod http_flux_pro_11_ultra_text_to_image; pub mod http_gemini_25_flash_text_to_image; -pub mod http_gpt_image_1_non_byok_text_to_image; pub mod http_gpt_image_1_text_to_image; +pub mod http_gpt_image_1_byok_text_to_image; pub mod http_gpt_image_1p5_text_to_image; pub mod http_gpt_image_2_text_to_image; pub mod http_nano_banana_2_text_to_image; diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_gpt_image_1_byok_edit_image_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_gpt_image_1_byok_edit_image_webhook.rs index aacd056ff1..bca0133abb 100644 --- a/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_gpt_image_1_byok_edit_image_webhook.rs +++ b/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_gpt_image_1_byok_edit_image_webhook.rs @@ -8,7 +8,7 @@ use crate::creds::open_ai_api_key::OpenAiApiKey; use crate::error::classify_fal_error::classify_fal_error; use crate::error::fal_error_plus::FalErrorPlus; use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; -use crate::requests::http::image::edit::http_gpt_image_1_edit_image::{gpt_image_1_edit_image, GptImage1EditImageInput}; +use crate::requests::http::image::edit::http_gpt_image_1_byok_edit_image::{gpt_image_1_byok_edit_image, GptImage1ByokEditImageInput}; use crate::requests::core_api::webhook_response::WebhookResponse; use reqwest::IntoUrl; @@ -101,7 +101,7 @@ pub async fn enqueue_gpt_image_1_byok_edit_image_webhook( GptEditImageNumImages::Four => 4, }; - let request = GptImage1EditImageInput { + let request = GptImage1ByokEditImageInput { image_urls: req.image_urls, prompt: req.prompt, image_size: image_size.to_string(), @@ -110,7 +110,7 @@ pub async fn enqueue_gpt_image_1_byok_edit_image_webhook( openai_api_key: args.openai_api_key.0.to_string(), }; - let result = gpt_image_1_edit_image(request) + let result = gpt_image_1_byok_edit_image(request) .with_api_key(&args.api_key.0) .queue_webhook(args.webhook_url) .await; diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_gpt_image_1_edit_image_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_gpt_image_1_edit_image_webhook.rs index 5aa306a1ee..f2e64eb349 100644 --- a/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_gpt_image_1_edit_image_webhook.rs +++ b/crates/api_clients/fal_client/src/requests/webhook/image/edit/enqueue_gpt_image_1_edit_image_webhook.rs @@ -2,8 +2,8 @@ use crate::creds::fal_api_key::FalApiKey; use crate::error::classify_fal_error::classify_fal_error; use crate::error::fal_error_plus::FalErrorPlus; use crate::requests::core_api::webhook_response::WebhookResponse; -use crate::requests::http::image::edit::http_gpt_image_1_non_byok_edit_image::{ - gpt_image_1_non_byok_edit_image, GptImage1NonByokEditImageInput, +use crate::requests::http::image::edit::http_gpt_image_1_edit_image::{ + gpt_image_1_edit_image, GptImage1EditImageInput, }; use crate::requests::traits::fal_request_cost_calculator_trait::{ FalRequestCostCalculator, UsdCents, @@ -172,7 +172,7 @@ pub async fn enqueue_gpt_image_1_edit_image_webhook( .to_string() }); - let request = GptImage1NonByokEditImageInput { + let request = GptImage1EditImageInput { prompt: req.prompt, image_urls: req.image_urls, mask_image_url: req.mask_image_url, @@ -184,7 +184,7 @@ pub async fn enqueue_gpt_image_1_edit_image_webhook( output_format, }; - let result = gpt_image_1_non_byok_edit_image(request) + let result = gpt_image_1_edit_image(request) .with_api_key(&args.api_key.0) .queue_webhook(args.webhook_url) .await; diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_gpt_image_1_byok_text_to_image_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_gpt_image_1_byok_text_to_image_webhook.rs index e704d13294..29504ffb5c 100644 --- a/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_gpt_image_1_byok_text_to_image_webhook.rs +++ b/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_gpt_image_1_byok_text_to_image_webhook.rs @@ -8,7 +8,7 @@ use crate::creds::open_ai_api_key::OpenAiApiKey; use crate::error::classify_fal_error::classify_fal_error; use crate::error::fal_error_plus::FalErrorPlus; use crate::requests::traits::fal_request_cost_calculator_trait::{FalRequestCostCalculator, UsdCents}; -use crate::requests::http::image::text::http_gpt_image_1_text_to_image::{gpt_image_1_text_to_image, GptImage1TextToImageInput}; +use crate::requests::http::image::text::http_gpt_image_1_byok_text_to_image::{gpt_image_1_byok_text_to_image, GptImage1ByokTextToImageInput}; use crate::requests::core_api::webhook_response::WebhookResponse; use reqwest::IntoUrl; @@ -106,7 +106,7 @@ pub async fn enqueue_gpt_image_1_byok_text_to_image_webhook( GptTextToImageNumImages::Four => 4, }; - let request = GptImage1TextToImageInput { + let request = GptImage1ByokTextToImageInput { prompt: req.prompt, image_size: image_size.to_string(), num_images, @@ -114,7 +114,7 @@ pub async fn enqueue_gpt_image_1_byok_text_to_image_webhook( openai_api_key: args.openai_api_key.0.to_string(), }; - let result = gpt_image_1_text_to_image(request) + let result = gpt_image_1_byok_text_to_image(request) .with_api_key(&args.api_key.0) .queue_webhook(args.webhook_url) .await; diff --git a/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_gpt_image_1_text_to_image_webhook.rs b/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_gpt_image_1_text_to_image_webhook.rs index e3296d54c2..64e41d7bd2 100644 --- a/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_gpt_image_1_text_to_image_webhook.rs +++ b/crates/api_clients/fal_client/src/requests/webhook/image/text/enqueue_gpt_image_1_text_to_image_webhook.rs @@ -2,8 +2,8 @@ use crate::creds::fal_api_key::FalApiKey; use crate::error::classify_fal_error::classify_fal_error; use crate::error::fal_error_plus::FalErrorPlus; use crate::requests::core_api::webhook_response::WebhookResponse; -use crate::requests::http::image::text::http_gpt_image_1_non_byok_text_to_image::{ - gpt_image_1_non_byok_text_to_image, GptImage1NonByokTextToImageInput, +use crate::requests::http::image::text::http_gpt_image_1_text_to_image::{ + gpt_image_1_text_to_image, GptImage1TextToImageInput, }; use crate::requests::traits::fal_request_cost_calculator_trait::{ FalRequestCostCalculator, UsdCents, @@ -154,7 +154,7 @@ pub async fn enqueue_gpt_image_1_text_to_image_webhook( .to_string() }); - let request = GptImage1NonByokTextToImageInput { + let request = GptImage1TextToImageInput { prompt: req.prompt, num_images: Some(num_images), image_size, @@ -163,7 +163,7 @@ pub async fn enqueue_gpt_image_1_text_to_image_webhook( output_format, }; - let result = gpt_image_1_non_byok_text_to_image(request) + let result = gpt_image_1_text_to_image(request) .with_api_key(&args.api_key.0) .queue_webhook(args.webhook_url) .await;