Compare commits

...
Author SHA1 Message Date
Brandon Thomas 2330acc05f do not change frontend 2026-05-14 18:53:05 -04:00
Brandon Thomas 9b83cece63 gpt-image-1 2026-05-14 18:52:07 -04:00
Brandon Thomas adcf10d109 gpt-image-1 2026-05-14 18:50:01 -04:00
Brandon Thomas c34663ed3d gpt-image-1.5 text-to-image 2026-05-14 18:45:56 -04:00
Brandon Thomas d6c033425d gpt-image-1.5 2026-05-14 18:43:15 -04:00
Brandon Thomas cf97e4cb32 rename/cleanup 2026-05-14 18:34:16 -04:00
Brandon Thomas 4f43b198ad rename 2026-05-14 18:27:18 -04:00
Brandon Thomas e1260d2c08 rename 2026-05-14 18:26:30 -04:00
Brandon Thomas 5aca0db36a rename 2026-05-14 18:21:56 -04:00
Brandon Thomas 130842c889 rename 2026-05-14 18:20:06 -04:00
Brandon Thomas 03a96e0732 gpt-image-2 2026-05-14 18:13:39 -04:00
Brandon Thomas 9a3dd2c9ae gpt-image-2 text-to-image 2026-05-14 18:07:12 -04:00
36 changed files with 2511 additions and 61 deletions
@@ -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<String>,
/// 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<String>,
/// Output image size.
pub image_size: Option<GptImage1EditImageSize>,
/// Quality level.
pub quality: Option<GptImage1EditImageQuality>,
/// Input fidelity level.
pub input_fidelity: Option<GptImage1EditImageInputFidelity>,
/// Background mode.
pub background: Option<GptImage1EditImageBackground>,
/// Output format.
pub output_format: Option<GptImage1EditImageOutputFormat>,
}
#[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<Self::RawRequest, FalErrorPlus> {
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
}
@@ -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<GptImage1EditImageQuality>,
image_size: Option<GptImage1EditImageSize>,
) -> 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);
}
}
}
@@ -0,0 +1,3 @@
pub mod api;
mod raw_request;
mod cost;
@@ -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<String>,
/// 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<String>,
@@ -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<GptImage1NonByokEditImageFile>,
}
pub fn gpt_image_1_non_byok_edit_image(
params: GptImage1NonByokEditImageInput,
) -> FalRequest<GptImage1NonByokEditImageInput, GptImage1NonByokEditImageOutput> {
FalRequest::new("fal-ai/gpt-image-1/edit-image", params)
pub struct GptImage1EditImageOutput {
pub images: Vec<GptImage1EditImageFile>,
}
@@ -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<String>,
/// 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<String>,
/// Output image size.
pub image_size: Option<GptImage1p5EditImageSize>,
/// Background mode.
pub background: Option<GptImage1p5EditImageBackground>,
/// Quality level.
pub quality: Option<GptImage1p5EditImageQuality>,
/// Input fidelity level.
pub input_fidelity: Option<GptImage1p5EditImageInputFidelity>,
/// Output format.
pub output_format: Option<GptImage1p5EditImageOutputFormat>,
}
#[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<Self::RawRequest, FalErrorPlus> {
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
}
@@ -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<GptImage1p5EditImageQuality>,
image_size: Option<GptImage1p5EditImageSize>,
) -> 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);
}
}
}
@@ -0,0 +1,3 @@
pub mod api;
mod raw_request;
mod cost;
@@ -0,0 +1,48 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct GptImage1p5EditImageInput {
pub prompt: String,
pub image_urls: Vec<String>,
/// 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<String>,
/// "1024x1024", "1536x1024", "1024x1536"
#[serde(skip_serializing_if = "Option::is_none")]
pub image_size: Option<String>,
/// "auto", "transparent", "opaque"
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<String>,
/// "low", "medium", "high"
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<String>,
/// "low", "high"
#[serde(skip_serializing_if = "Option::is_none")]
pub input_fidelity: Option<String>,
/// 1 - 4
/// Default: 1
#[serde(skip_serializing_if = "Option::is_none")]
pub num_images: Option<u8>,
/// "jpeg", "png", "webp"
/// Default: "png"
#[serde(skip_serializing_if = "Option::is_none")]
pub output_format: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GptImage1p5EditImageFile {
pub url: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GptImage1p5EditImageOutput {
pub images: Vec<GptImage1p5EditImageFile>,
}
@@ -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<String>,
/// Number of images to generate.
pub num_images: GptImage2EditImageNumImages,
/// Optional mask URL indicating what part of the image to edit.
pub mask_url: Option<String>,
/// Output image size.
pub image_size: Option<GptImage2EditImageSize>,
/// Quality level.
pub quality: Option<GptImage2EditImageQuality>,
/// Output format.
pub output_format: Option<GptImage2EditImageOutputFormat>,
}
#[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<Self::RawRequest, FalErrorPlus> {
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
}
@@ -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<GptImage2EditImageQuality>,
image_size: Option<GptImage2EditImageSize>,
) -> 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);
}
}
}
@@ -0,0 +1,3 @@
pub mod api;
mod raw_request;
mod cost;
@@ -0,0 +1,41 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct GptImage2EditImageInput {
pub prompt: String,
pub image_urls: Vec<String>,
/// 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<String>,
/// 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<String>,
/// "low", "medium", "high"
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<String>,
/// 1 - 4
/// Default: 1
#[serde(skip_serializing_if = "Option::is_none")]
pub num_images: Option<u8>,
/// "jpeg", "png", "webp"
/// Default: "png"
#[serde(skip_serializing_if = "Option::is_none")]
pub output_format: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GptImage2EditImageFile {
pub url: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GptImage2EditImageOutput {
pub images: Vec<GptImage2EditImageFile>,
}
@@ -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;
@@ -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<GptImage1TextToImageSize>,
/// Quality level.
pub quality: Option<GptImage1TextToImageQuality>,
/// Background mode.
pub background: Option<GptImage1TextToImageBackground>,
/// Output format.
pub output_format: Option<GptImage1TextToImageOutputFormat>,
}
#[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<Self::RawRequest, FalErrorPlus> {
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
}
@@ -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<GptImage1TextToImageQuality>,
image_size: Option<GptImage1TextToImageSize>,
) -> 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);
}
}
}
@@ -0,0 +1,3 @@
pub mod api;
mod raw_request;
mod cost;
@@ -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<GptImage1NonByokTextToImageFile>,
}
pub fn gpt_image_1_non_byok_text_to_image(
params: GptImage1NonByokTextToImageInput,
) -> FalRequest<GptImage1NonByokTextToImageInput, GptImage1NonByokTextToImageOutput> {
FalRequest::new("fal-ai/gpt-image-1/text-to-image", params)
pub struct GptImage1TextToImageOutput {
pub images: Vec<GptImage1TextToImageFile>,
}
@@ -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<GptImage1p5TextToImageSize>,
/// Background mode.
pub background: Option<GptImage1p5TextToImageBackground>,
/// Quality level.
pub quality: Option<GptImage1p5TextToImageQuality>,
/// Output format.
pub output_format: Option<GptImage1p5TextToImageOutputFormat>,
}
#[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<Self::RawRequest, FalErrorPlus> {
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
}
@@ -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<GptImage1p5TextToImageQuality>,
image_size: Option<GptImage1p5TextToImageSize>,
) -> 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);
}
}
}
@@ -0,0 +1,3 @@
pub mod api;
mod raw_request;
mod cost;
@@ -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<String>,
/// "auto", "transparent", "opaque"
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<String>,
/// "low", "medium", "high"
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<String>,
/// 1 - 4
/// Default: 1
#[serde(skip_serializing_if = "Option::is_none")]
pub num_images: Option<u8>,
/// "jpeg", "png", "webp"
/// Default: "png"
#[serde(skip_serializing_if = "Option::is_none")]
pub output_format: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GptImage1p5TextToImageFile {
pub url: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GptImage1p5TextToImageOutput {
pub images: Vec<GptImage1p5TextToImageFile>,
}
@@ -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<GptImage2TextToImageSize>,
/// Quality level.
pub quality: Option<GptImage2TextToImageQuality>,
/// Output format.
pub output_format: Option<GptImage2TextToImageOutputFormat>,
}
#[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<Self::RawRequest, FalErrorPlus> {
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
}
@@ -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<GptImage2TextToImageQuality>,
image_size: Option<GptImage2TextToImageSize>,
) -> 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);
}
}
}
@@ -0,0 +1,3 @@
pub mod api;
mod raw_request;
mod cost;
@@ -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<String>,
/// "low", "medium", "high"
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<String>,
/// 1 - 4
/// Default: 1
#[serde(skip_serializing_if = "Option::is_none")]
pub num_images: Option<u8>,
/// "jpeg", "png", "webp"
/// Default: "png"
#[serde(skip_serializing_if = "Option::is_none")]
pub output_format: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GptImage2TextToImageFile {
pub url: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GptImage2TextToImageOutput {
pub images: Vec<GptImage2TextToImageFile>,
}
@@ -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;
@@ -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<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 GptImage1ByokEditImageOutput {
// Response fields parsed elsewhere
}
pub fn gpt_image_1_byok_edit_image(
params: GptImage1ByokEditImageInput,
) -> FalRequest<GptImage1ByokEditImageInput, GptImage1ByokEditImageOutput> {
FalRequest::new("fal-ai/gpt-image-1/edit-image/byok", params)
}
@@ -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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mask_image_url: Option<String>,
/// "auto", "1024x1024", "1536x1024", "1024x1536"
pub image_size: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_size: Option<String>,
/// "low", "medium", "high"
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<String>,
/// "low", "high"
#[serde(skip_serializing_if = "Option::is_none")]
pub input_fidelity: Option<String>,
/// 1 - 4
pub num_images: u8,
/// Default: 1
#[serde(skip_serializing_if = "Option::is_none")]
pub num_images: Option<u8>,
/// "auto", "low", "medium", "high"
pub quality: String,
/// "auto", "transparent", "opaque"
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<String>,
/// 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<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GptImage1EditImageFile {
pub url: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GptImage1EditImageOutput {
// Response fields parsed elsewhere
pub images: Vec<GptImage1EditImageFile>,
}
pub fn gpt_image_1_edit_image(
params: GptImage1EditImageInput,
) -> FalRequest<GptImage1EditImageInput, GptImage1EditImageOutput> {
FalRequest::new("fal-ai/gpt-image-1/edit-image/byok", params)
FalRequest::new("fal-ai/gpt-image-1/edit-image", params)
}
@@ -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;
@@ -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<GptImage1ByokTextToImageInput, GptImage1ByokTextToImageOutput> {
FalRequest::new("fal-ai/gpt-image-1/text-to-image/byok", params)
}
@@ -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<String>,
/// "low", "medium", "high"
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<String>,
/// 1 - 4
pub num_images: u8,
/// Default: 1
#[serde(skip_serializing_if = "Option::is_none")]
pub num_images: Option<u8>,
/// "auto", "low", "medium", "high"
pub quality: String,
/// "auto", "transparent", "opaque"
#[serde(skip_serializing_if = "Option::is_none")]
pub background: Option<String>,
/// 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<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GptImage1TextToImageFile {
pub url: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GptImage1TextToImageOutput {
// Response fields parsed elsewhere
pub images: Vec<GptImage1TextToImageFile>,
}
pub fn gpt_image_1_text_to_image(
params: GptImage1TextToImageInput,
) -> FalRequest<GptImage1TextToImageInput, GptImage1TextToImageOutput> {
FalRequest::new("fal-ai/gpt-image-1/text-to-image/byok", params)
FalRequest::new("fal-ai/gpt-image-1/text-to-image", params)
}
@@ -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;
@@ -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<V: IntoUrl>(
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<V: IntoUrl>(
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;
@@ -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<R: IntoUrl>(
.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<R: IntoUrl>(
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;
@@ -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<V: IntoUrl>(
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<V: IntoUrl>(
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;
@@ -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<R: IntoUrl>(
.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<R: IntoUrl>(
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;