port flux pro on fal to pipeline v2

This commit is contained in:
Brandon Thomas committed 2026-05-26 10:50:41 -04:00
1 parent 370d2fc494
commit d7357028b7
12 files changed
+1209

No files matched your search

@@ -12,6 +12,8 @@ use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image_v2::providers::fal::flux_1_dev::build::build_fal_flux_1_dev;
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::build::build_fal_flux_1_schnell;
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1::build::build_fal_flux_pro_1p1;
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1_ultra::build::build_fal_flux_pro_1p1_ultra;
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::build::build_fal_gpt_image_1;
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::build::build_fal_gpt_image_1p5;
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::build::build_fal_gpt_image_2;
@@ -102,6 +104,8 @@ impl GenerateImageRequestBuilder {
match (self.provider, self.model) {
(Provider::Fal, CommonImageModel::Flux1Dev) => true,
(Provider::Fal, CommonImageModel::Flux1Schnell) => true,
(Provider::Fal, CommonImageModel::FluxPro11) => true,
(Provider::Fal, CommonImageModel::FluxPro11Ultra) => true,
(Provider::Fal, CommonImageModel::GptImage1) => true,
(Provider::Fal, CommonImageModel::GptImage1p5) => true,
(Provider::Fal, CommonImageModel::GptImage2) => true,
@@ -115,6 +119,8 @@ impl GenerateImageRequestBuilder {
match (self.provider, self.model) {
(Provider::Fal, CommonImageModel::Flux1Dev) => build_fal_flux_1_dev(self),
(Provider::Fal, CommonImageModel::Flux1Schnell) => build_fal_flux_1_schnell(self),
(Provider::Fal, CommonImageModel::FluxPro11) => build_fal_flux_pro_1p1(self),
(Provider::Fal, CommonImageModel::FluxPro11Ultra) => build_fal_flux_pro_1p1_ultra(self),
(Provider::Fal, CommonImageModel::GptImage1) => build_fal_gpt_image_1(self),
(Provider::Fal, CommonImageModel::GptImage1p5) => build_fal_gpt_image_1p5(self),
(Provider::Fal, CommonImageModel::GptImage2) => build_fal_gpt_image_2(self),
@@ -7,6 +7,10 @@ use crate::generate::generate_image_v2::providers::fal::flux_1_dev::cost::FalFlu
use crate::generate::generate_image_v2::providers::fal::flux_1_dev::request::FalFlux1DevRequestState;
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::cost::FalFlux1SchnellCostState;
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::request::FalFlux1SchnellRequestState;
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1::cost::FalFluxPro1p1CostState;
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1::request::FalFluxPro1p1RequestState;
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1_ultra::cost::FalFluxPro1p1UltraCostState;
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1_ultra::request::FalFluxPro1p1UltraRequestState;
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::cost::FalGptImage1CostState;
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::request::FalGptImage1RequestState;
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::cost::FalGptImage1p5CostState;
@@ -22,6 +26,8 @@ use crate::generate::generate_image_v2::providers::fal::nano_banana_pro::request
pub enum ImageGenerationRequest {
FalFlux1Dev(FalFlux1DevRequestState),
FalFlux1Schnell(FalFlux1SchnellRequestState),
FalFluxPro1p1(FalFluxPro1p1RequestState),
FalFluxPro1p1Ultra(FalFluxPro1p1UltraRequestState),
FalGptImage1(FalGptImage1RequestState),
FalGptImage1p5(FalGptImage1p5RequestState),
FalGptImage2(FalGptImage2RequestState),
@@ -34,6 +40,8 @@ impl ImageGenerationRequest {
match self {
Self::FalFlux1Dev(_) => Provider::Fal,
Self::FalFlux1Schnell(_) => Provider::Fal,
Self::FalFluxPro1p1(_) => Provider::Fal,
Self::FalFluxPro1p1Ultra(_) => Provider::Fal,
Self::FalGptImage1(_) => Provider::Fal,
Self::FalGptImage1p5(_) => Provider::Fal,
Self::FalGptImage2(_) => Provider::Fal,
@@ -50,6 +58,12 @@ impl ImageGenerationRequest {
Self::FalFlux1Schnell(request) => {
Ok(FalFlux1SchnellCostState::from_request(request).estimate_cost())
}
Self::FalFluxPro1p1(request) => {
Ok(FalFluxPro1p1CostState::from_request(request).estimate_cost())
}
Self::FalFluxPro1p1Ultra(request) => {
Ok(FalFluxPro1p1UltraCostState::from_request(request).estimate_cost())
}
Self::FalGptImage1(request) => {
Ok(FalGptImage1CostState::from_request(request).estimate_cost())
}
@@ -78,6 +92,15 @@ impl ImageGenerationRequest {
let fal_client = client.get_fal_webhook_optional_client_ref()?;
request.send(fal_client).await
}
Self::FalFluxPro1p1(request) => {
// Flux Pro 1.1 is webhook-required — uses the webhook-only RouterFalClient.
let fal_client = client.get_fal_client_ref()?;
request.send(fal_client).await
}
Self::FalFluxPro1p1Ultra(request) => {
let fal_client = client.get_fal_client_ref()?;
request.send(fal_client).await
}
Self::FalGptImage1(request) => {
let fal_client = client.get_fal_webhook_optional_client_ref()?;
request.send(fal_client).await
@@ -0,0 +1,251 @@
//! Combinatorial cost-parity tests between v1 (`build().estimate_costs()`) and v2
//! (`build2().estimate_cost()`) for the Fal-hosted image models.
//!
//! Both pipelines must produce identical prices for every request shape we
//! expect routers to send. Image cost is simpler than video — it just scales
//! with `num_images` — but we still sweep aspect ratio + batch size + strategy
//! variations to catch any drift in the request plumbing.
#![cfg(test)]
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_image_model::CommonImageModel;
use crate::api::common_quality::CommonQuality;
use crate::api::common_resolution::CommonResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
fn base_builder(model: CommonImageModel) -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model,
provider: Provider::Fal,
prompt: Some("a cat in space".to_string()),
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
/// Returns `Some(cost_in_usd_cents)` from the v1 pipeline. If v1 errors out
/// (e.g. unsupported batch size with `ErrorOut`), returns `None` so the
/// parity test compares apples to apples.
fn v1_cost(builder: &GenerateImageRequestBuilder) -> Option<u64> {
builder
.build()
.ok()
.and_then(|plan| plan.estimate_costs().cost_in_usd_cents)
}
/// Same as `v1_cost`, but for the v2 pipeline.
fn v2_cost(builder: GenerateImageRequestBuilder) -> Option<u64> {
builder
.build2()
.ok()
.and_then(|dor| dor.estimate_cost().ok())
.and_then(|estimate| estimate.cost_in_usd_cents)
}
fn v1_credits(builder: &GenerateImageRequestBuilder) -> Option<u64> {
builder
.build()
.ok()
.and_then(|plan| plan.estimate_costs().cost_in_credits)
}
fn v2_credits(builder: GenerateImageRequestBuilder) -> Option<u64> {
builder
.build2()
.ok()
.and_then(|dor| dor.estimate_cost().ok())
.and_then(|estimate| estimate.cost_in_credits)
}
fn all_aspect_ratios() -> &'static [Option<CommonAspectRatio>] {
&[
None,
Some(CommonAspectRatio::Auto),
Some(CommonAspectRatio::Auto2k),
Some(CommonAspectRatio::Auto4k),
Some(CommonAspectRatio::Square),
Some(CommonAspectRatio::SquareHd),
Some(CommonAspectRatio::WideFourByThree),
Some(CommonAspectRatio::WideFiveByFour),
Some(CommonAspectRatio::WideThreeByTwo),
Some(CommonAspectRatio::WideSixteenByNine),
Some(CommonAspectRatio::WideTwentyOneByNine),
Some(CommonAspectRatio::Wide),
Some(CommonAspectRatio::TallThreeByFour),
Some(CommonAspectRatio::TallFourByFive),
Some(CommonAspectRatio::TallTwoByThree),
Some(CommonAspectRatio::TallNineBySixteen),
Some(CommonAspectRatio::TallNineByTwentyOne),
Some(CommonAspectRatio::Tall),
]
}
fn all_strategies() -> &'static [RequestMismatchMitigationStrategy] {
&[
RequestMismatchMitigationStrategy::ErrorOut,
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
]
}
// ── Flux Pro 1.1 ──
mod flux_pro_1p1 {
use super::*;
fn base() -> GenerateImageRequestBuilder {
base_builder(CommonImageModel::FluxPro11)
}
#[test]
fn cost_parity_full_combinatorial() {
let batches = [None, Some(1u16), Some(2), Some(3), Some(4)];
let mut combos = 0;
for &aspect_ratio in all_aspect_ratios() {
for &batch in &batches {
for &strategy in all_strategies() {
let mut builder = base();
builder.aspect_ratio = aspect_ratio;
builder.image_batch_count = batch;
builder.request_mismatch_mitigation_strategy = strategy;
let v1 = v1_cost(&builder);
let v2 = v2_cost(builder.clone());
assert_eq!(
v1, v2,
"flux_pro_1p1 cost mismatch: ar={:?} batch={:?} strat={:?} → v1={:?} v2={:?}",
aspect_ratio, batch, strategy, v1, v2,
);
let v1c = v1_credits(&builder);
let v2c = v2_credits(builder.clone());
assert_eq!(
v1c, v2c,
"flux_pro_1p1 credits mismatch: ar={:?} batch={:?} strat={:?} → v1={:?} v2={:?}",
aspect_ratio, batch, strategy, v1c, v2c,
);
combos += 1;
}
}
}
assert_eq!(combos, all_aspect_ratios().len() * batches.len() * all_strategies().len());
}
#[test]
fn cost_parity_out_of_range_batches() {
// Batches above 4 should error on ErrorOut and clamp to Four otherwise.
for &batch in &[5u16, 7, 10, 100] {
for &strategy in all_strategies() {
let mut builder = base();
builder.image_batch_count = Some(batch);
builder.request_mismatch_mitigation_strategy = strategy;
let v1 = v1_cost(&builder);
let v2 = v2_cost(builder.clone());
assert_eq!(
v1, v2,
"flux_pro_1p1 out-of-range cost mismatch: batch={} strat={:?} → v1={:?} v2={:?}",
batch, strategy, v1, v2,
);
}
}
}
#[test]
fn cost_parity_with_extra_fields_set() {
// v1 ignores resolution/quality/horizontal_angle/etc., so v2 should too.
let mut builder = base();
builder.image_batch_count = Some(2);
builder.resolution = Some(CommonResolution::TenEightyP);
builder.quality = Some(CommonQuality::High);
builder.horizontal_angle = Some(45.0);
builder.vertical_angle = Some(15.0);
builder.zoom = Some(2.0);
assert_eq!(v1_cost(&builder), v2_cost(builder.clone()));
}
}
// ── Flux Pro 1.1 Ultra ──
mod flux_pro_1p1_ultra {
use super::*;
fn base() -> GenerateImageRequestBuilder {
base_builder(CommonImageModel::FluxPro11Ultra)
}
#[test]
fn cost_parity_full_combinatorial() {
let batches = [None, Some(1u16), Some(2), Some(3), Some(4)];
let mut combos = 0;
for &aspect_ratio in all_aspect_ratios() {
for &batch in &batches {
for &strategy in all_strategies() {
let mut builder = base();
builder.aspect_ratio = aspect_ratio;
builder.image_batch_count = batch;
builder.request_mismatch_mitigation_strategy = strategy;
let v1 = v1_cost(&builder);
let v2 = v2_cost(builder.clone());
assert_eq!(
v1, v2,
"flux_pro_1p1_ultra cost mismatch: ar={:?} batch={:?} strat={:?} → v1={:?} v2={:?}",
aspect_ratio, batch, strategy, v1, v2,
);
let v1c = v1_credits(&builder);
let v2c = v2_credits(builder.clone());
assert_eq!(
v1c, v2c,
"flux_pro_1p1_ultra credits mismatch: ar={:?} batch={:?} strat={:?} → v1={:?} v2={:?}",
aspect_ratio, batch, strategy, v1c, v2c,
);
combos += 1;
}
}
}
assert_eq!(combos, all_aspect_ratios().len() * batches.len() * all_strategies().len());
}
#[test]
fn cost_parity_out_of_range_batches() {
for &batch in &[5u16, 7, 10, 100] {
for &strategy in all_strategies() {
let mut builder = base();
builder.image_batch_count = Some(batch);
builder.request_mismatch_mitigation_strategy = strategy;
let v1 = v1_cost(&builder);
let v2 = v2_cost(builder.clone());
assert_eq!(
v1, v2,
"flux_pro_1p1_ultra out-of-range cost mismatch: batch={} strat={:?} → v1={:?} v2={:?}",
batch, strategy, v1, v2,
);
}
}
}
#[test]
fn cost_parity_image_inputs_rejected_by_both() {
// v1 errors on image_inputs (text-to-image only); v2 should match.
let mut builder = base();
builder.image_inputs = Some(ImageListRef::Urls(vec!["https://example.com/x.jpg".to_string()]));
assert_eq!(v1_cost(&builder), None);
assert_eq!(v2_cost(builder), None);
}
}
@@ -0,0 +1,302 @@
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_text_to_image_webhook::{
FluxPro11AspectRatio, FluxPro11NumImages, FluxPro11Request,
};
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1::request::FalFluxPro1p1RequestState;
pub fn build_fal_flux_pro_1p1(
builder: GenerateImageRequestBuilder,
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
let strategy = builder.request_mismatch_mitigation_strategy;
// Flux Pro 1.1 is text-to-image only.
if builder.image_inputs.is_some() {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_inputs",
value: "Flux Pro 1.1 is text-to-image only".to_string(),
}));
}
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio, strategy)?;
let num_images = plan_num_images(builder.image_batch_count, strategy)?;
let request = FluxPro11Request {
prompt: builder.prompt.clone().unwrap_or_default(),
aspect_ratio,
num_images,
};
Ok(ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::FalFluxPro1p1(FalFluxPro1p1RequestState { request }),
))
}
fn plan_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<FluxPro11AspectRatio, ArtcraftRouterError> {
use FluxPro11AspectRatio as Ar;
match aspect_ratio {
None => Ok(Ar::Square),
Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Square),
Some(CommonAspectRatio::Square) => Ok(Ar::Square),
Some(CommonAspectRatio::SquareHd) => Ok(Ar::SquareHd),
Some(CommonAspectRatio::WideFourByThree) => Ok(Ar::LandscapeFourByThree),
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::LandscapeSixteenByNine),
Some(CommonAspectRatio::TallThreeByFour) => Ok(Ar::PortraitThreeByFour),
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::PortraitNineBySixteen),
Some(CommonAspectRatio::WideFiveByFour) => Ok(Ar::LandscapeFourByThree),
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Ar::LandscapeFourByThree),
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Ar::LandscapeSixteenByNine),
Some(CommonAspectRatio::TallFourByFive) => Ok(Ar::PortraitThreeByFour),
Some(CommonAspectRatio::TallTwoByThree) => Ok(Ar::PortraitThreeByFour),
Some(CommonAspectRatio::TallNineByTwentyOne) => Ok(Ar::PortraitNineBySixteen),
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
_ => Ok(Ar::Square),
},
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<FluxPro11NumImages, ArtcraftRouterError> {
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(FluxPro11NumImages::One),
2 => Ok(FluxPro11NumImages::Two),
3 => Ok(FluxPro11NumImages::Three),
4 => Ok(FluxPro11NumImages::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
_ => Ok(FluxPro11NumImages::Four),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common_image_model::CommonImageModel;
use crate::api::image_list_ref::ImageListRef;
use crate::api::provider::Provider;
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::FluxPro11,
provider: Provider::Fal,
prompt: Some("a cat in space".to_string()),
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
fn unwrap_request(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> FluxPro11Request {
let ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::FalFluxPro1p1(state)
) = result.expect("build should succeed") else {
panic!("expected FalFluxPro1p1 variant")
};
state.request
}
// ── Mode rejection ──
#[test]
fn image_inputs_present_yields_error() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
..base_builder()
};
assert!(matches!(
build_fal_flux_pro_1p1(builder),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "image_inputs", .. }))
));
}
// ── Prompt ──
mod prompt_tests {
use super::*;
#[test]
fn prompt_passed_through() {
let builder = GenerateImageRequestBuilder {
prompt: Some("my custom prompt".to_string()),
..base_builder()
};
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
assert_eq!(req.prompt, "my custom prompt");
}
#[test]
fn missing_prompt_defaults_to_empty() {
let builder = GenerateImageRequestBuilder { prompt: None, ..base_builder() };
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
assert_eq!(req.prompt, "");
}
}
// ── Aspect ratio ──
mod aspect_ratio_tests {
use super::*;
#[test]
fn default_is_square() {
let req = unwrap_request(build_fal_flux_pro_1p1(base_builder()));
assert!(matches!(req.aspect_ratio, FluxPro11AspectRatio::Square));
}
#[test]
fn auto_maps_to_square() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Auto),
..base_builder()
};
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
assert!(matches!(req.aspect_ratio, FluxPro11AspectRatio::Square));
}
#[test]
fn square_hd() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::SquareHd),
..base_builder()
};
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
assert!(matches!(req.aspect_ratio, FluxPro11AspectRatio::SquareHd));
}
#[test]
fn wide_sixteen_by_nine() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
..base_builder()
};
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
assert!(matches!(req.aspect_ratio, FluxPro11AspectRatio::LandscapeSixteenByNine));
}
#[test]
fn tall_nine_by_sixteen() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
..base_builder()
};
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
assert!(matches!(req.aspect_ratio, FluxPro11AspectRatio::PortraitNineBySixteen));
}
#[test]
fn wide_twenty_one_by_nine_collapses_to_sixteen_by_nine() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideTwentyOneByNine),
..base_builder()
};
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
assert!(matches!(req.aspect_ratio, FluxPro11AspectRatio::LandscapeSixteenByNine));
}
#[test]
fn tall_four_by_five_collapses_to_three_by_four() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
..base_builder()
};
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
assert!(matches!(req.aspect_ratio, FluxPro11AspectRatio::PortraitThreeByFour));
}
}
// ── Num images ──
mod num_images_tests {
use super::*;
#[test]
fn default_is_one() {
let req = unwrap_request(build_fal_flux_pro_1p1(base_builder()));
assert!(matches!(req.num_images, FluxPro11NumImages::One));
}
#[test]
fn explicit_three() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(3),
..base_builder()
};
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
assert!(matches!(req.num_images, FluxPro11NumImages::Three));
}
#[test]
fn zero_is_error() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(0),
..base_builder()
};
assert!(matches!(
build_fal_flux_pro_1p1(builder),
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
));
}
#[test]
fn over_four_error_out() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(7),
..base_builder()
};
assert!(matches!(
build_fal_flux_pro_1p1(builder),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "image_batch_count", .. }))
));
}
#[test]
fn over_four_clamps_with_upgrade() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(7),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_builder()
};
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
assert!(matches!(req.num_images, FluxPro11NumImages::Four));
}
}
}
@@ -0,0 +1,86 @@
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1::request::FalFluxPro1p1RequestState;
pub struct FalFluxPro1p1CostState {
cost_in_usd_cents: u64,
}
impl FalFluxPro1p1CostState {
pub fn from_request(request: &FalFluxPro1p1RequestState) -> Self {
// Delegate to the fal_client cost calculator: $0.04 per image (1MP base).
Self {
cost_in_usd_cents: request.request.calculate_cost_in_cents(),
}
}
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
// Matches v1: credits == cents (1:1 with USD cents).
ImageGenerationCostEstimate {
cost_in_credits: Some(self.cost_in_usd_cents),
cost_in_usd_cents: Some(self.cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_text_to_image_webhook::{
FluxPro11AspectRatio, FluxPro11NumImages, FluxPro11Request,
};
fn cost_for(n: FluxPro11NumImages) -> ImageGenerationCostEstimate {
let state = FalFluxPro1p1CostState::from_request(&FalFluxPro1p1RequestState {
request: FluxPro11Request {
prompt: "test".to_string(),
aspect_ratio: FluxPro11AspectRatio::Square,
num_images: n,
},
});
state.estimate_cost()
}
#[test]
fn one_image_is_4_cents() {
assert_eq!(cost_for(FluxPro11NumImages::One).cost_in_usd_cents, Some(4));
}
#[test]
fn two_images_is_8_cents() {
assert_eq!(cost_for(FluxPro11NumImages::Two).cost_in_usd_cents, Some(8));
}
#[test]
fn three_images_is_12_cents() {
assert_eq!(cost_for(FluxPro11NumImages::Three).cost_in_usd_cents, Some(12));
}
#[test]
fn four_images_is_16_cents() {
assert_eq!(cost_for(FluxPro11NumImages::Four).cost_in_usd_cents, Some(16));
}
#[test]
fn credits_equal_cents() {
// v1 set cost_in_credits to the same USD cents value; preserve that.
let cost = cost_for(FluxPro11NumImages::Two);
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
}
#[test]
fn cost_flags_match_v1() {
let cost = cost_for(FluxPro11NumImages::One);
assert!(!cost.is_free);
assert!(!cost.is_unlimited);
assert!(!cost.is_rate_limited);
assert!(!cost.has_watermark);
assert!(cost.failures_are_refunded.is_none());
}
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -0,0 +1,82 @@
use std::fmt::Debug;
use std::sync::Arc;
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_text_to_image_webhook::{
enqueue_flux_pro_11_text_to_image_webhook, FluxPro11Args, FluxPro11Request,
};
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
FalImageResponsePayload, GenerateImageResponse,
};
#[derive(Clone, Debug)]
pub struct FalFluxPro1p1RequestState {
pub request: FluxPro11Request,
}
impl FalFluxPro1p1RequestState {
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(self.request.clone());
let args = FluxPro11Args {
request: self.request.clone(),
webhook_url: client.webhook_url.as_str(),
api_key: &client.api_key,
};
let webhook_response = enqueue_flux_pro_11_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_status_url: None,
maybe_response_url: None,
maybe_outbound_request: Some(outbound),
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
use fal_client::creds::fal_api_key::FalApiKey;
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_text_to_image_webhook::{
FluxPro11AspectRatio, FluxPro11NumImages,
};
fn read_fal_api_key() -> FalApiKey {
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
.expect("Failed to read fal_api_key.txt");
FalApiKey::from_str(secret.trim())
}
fn client_with_webhook() -> RouterFalClient {
RouterFalClient::new(
read_fal_api_key(),
"https://example.com/fal-webhook-test".to_string(),
)
}
fn t2i_state() -> FalFluxPro1p1RequestState {
FalFluxPro1p1RequestState {
request: FluxPro11Request {
prompt: "a corgi wearing sunglasses on a surfboard".to_string(),
aspect_ratio: FluxPro11AspectRatio::LandscapeSixteenByNine,
num_images: FluxPro11NumImages::One,
},
}
}
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_via_webhook() {
let client = client_with_webhook();
let response = t2i_state().send(&client).await.expect("send should succeed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Webhook t2i — request_id: {:?}, gateway_request_id: {:?}", payload.request_id, payload.gateway_request_id);
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
}
}
@@ -0,0 +1,281 @@
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_ultra_text_to_image_webhook::{
FluxPro11UltraAspectRatio, FluxPro11UltraNumImages, FluxPro11UltraRequest,
};
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1_ultra::request::FalFluxPro1p1UltraRequestState;
pub fn build_fal_flux_pro_1p1_ultra(
builder: GenerateImageRequestBuilder,
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
let strategy = builder.request_mismatch_mitigation_strategy;
// Flux Pro 1.1 Ultra is text-to-image only.
if builder.image_inputs.is_some() {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_inputs",
value: "Flux Pro 1.1 Ultra is text-to-image only".to_string(),
}));
}
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio, strategy)?;
let num_images = plan_num_images(builder.image_batch_count, strategy)?;
let request = FluxPro11UltraRequest {
prompt: builder.prompt.clone().unwrap_or_default(),
aspect_ratio,
num_images,
};
Ok(ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::FalFluxPro1p1Ultra(FalFluxPro1p1UltraRequestState { request }),
))
}
fn plan_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<FluxPro11UltraAspectRatio, ArtcraftRouterError> {
use FluxPro11UltraAspectRatio as Ar;
match aspect_ratio {
None => Ok(Ar::Square),
Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Square),
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Ar::Square),
Some(CommonAspectRatio::WideFourByThree) => Ok(Ar::LandscapeFourByThree),
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Ar::LandscapeThreeByTwo),
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::LandscapeSixteenByNine),
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Ar::LandscapeTwentyOneByNine),
Some(CommonAspectRatio::TallThreeByFour) => Ok(Ar::PortraitThreeByFour),
Some(CommonAspectRatio::TallTwoByThree) => Ok(Ar::PortraitTwoByThree),
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::PortraitNineBySixteen),
Some(CommonAspectRatio::TallNineByTwentyOne) => Ok(Ar::PortraitNineByTwentyOne),
Some(CommonAspectRatio::WideFiveByFour) => Ok(Ar::LandscapeFourByThree),
Some(CommonAspectRatio::TallFourByFive) => Ok(Ar::PortraitThreeByFour),
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
_ => Ok(Ar::Square),
},
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<FluxPro11UltraNumImages, ArtcraftRouterError> {
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(FluxPro11UltraNumImages::One),
2 => Ok(FluxPro11UltraNumImages::Two),
3 => Ok(FluxPro11UltraNumImages::Three),
4 => Ok(FluxPro11UltraNumImages::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
_ => Ok(FluxPro11UltraNumImages::Four),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common_image_model::CommonImageModel;
use crate::api::image_list_ref::ImageListRef;
use crate::api::provider::Provider;
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::FluxPro11Ultra,
provider: Provider::Fal,
prompt: Some("a cat in space".to_string()),
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
fn unwrap_request(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> FluxPro11UltraRequest {
let ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::FalFluxPro1p1Ultra(state)
) = result.expect("build should succeed") else {
panic!("expected FalFluxPro1p1Ultra variant")
};
state.request
}
// ── Mode rejection ──
#[test]
fn image_inputs_present_yields_error() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
..base_builder()
};
assert!(matches!(
build_fal_flux_pro_1p1_ultra(builder),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "image_inputs", .. }))
));
}
// ── Prompt ──
mod prompt_tests {
use super::*;
#[test]
fn prompt_passed_through() {
let builder = GenerateImageRequestBuilder {
prompt: Some("my custom prompt".to_string()),
..base_builder()
};
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(builder));
assert_eq!(req.prompt, "my custom prompt");
}
#[test]
fn missing_prompt_defaults_to_empty() {
let builder = GenerateImageRequestBuilder { prompt: None, ..base_builder() };
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(builder));
assert_eq!(req.prompt, "");
}
}
// ── Aspect ratio ──
mod aspect_ratio_tests {
use super::*;
#[test]
fn default_is_square() {
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(base_builder()));
assert!(matches!(req.aspect_ratio, FluxPro11UltraAspectRatio::Square));
}
#[test]
fn wide_sixteen_by_nine() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
..base_builder()
};
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(builder));
assert!(matches!(req.aspect_ratio, FluxPro11UltraAspectRatio::LandscapeSixteenByNine));
}
#[test]
fn wide_three_by_two_has_direct_mapping() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideThreeByTwo),
..base_builder()
};
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(builder));
assert!(matches!(req.aspect_ratio, FluxPro11UltraAspectRatio::LandscapeThreeByTwo));
}
#[test]
fn tall_two_by_three_has_direct_mapping() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallTwoByThree),
..base_builder()
};
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(builder));
assert!(matches!(req.aspect_ratio, FluxPro11UltraAspectRatio::PortraitTwoByThree));
}
#[test]
fn wide_twenty_one_by_nine_has_direct_mapping() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideTwentyOneByNine),
..base_builder()
};
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(builder));
assert!(matches!(req.aspect_ratio, FluxPro11UltraAspectRatio::LandscapeTwentyOneByNine));
}
#[test]
fn square_hd_collapses_to_square() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::SquareHd),
..base_builder()
};
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(builder));
assert!(matches!(req.aspect_ratio, FluxPro11UltraAspectRatio::Square));
}
}
// ── Num images ──
mod num_images_tests {
use super::*;
#[test]
fn default_is_one() {
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(base_builder()));
assert!(matches!(req.num_images, FluxPro11UltraNumImages::One));
}
#[test]
fn zero_is_error() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(0),
..base_builder()
};
assert!(matches!(
build_fal_flux_pro_1p1_ultra(builder),
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
));
}
#[test]
fn over_four_error_out() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(5),
..base_builder()
};
assert!(matches!(
build_fal_flux_pro_1p1_ultra(builder),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "image_batch_count", .. }))
));
}
#[test]
fn over_four_clamps_with_upgrade() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(7),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_builder()
};
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(builder));
assert!(matches!(req.num_images, FluxPro11UltraNumImages::Four));
}
}
}
@@ -0,0 +1,85 @@
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1_ultra::request::FalFluxPro1p1UltraRequestState;
pub struct FalFluxPro1p1UltraCostState {
cost_in_usd_cents: u64,
}
impl FalFluxPro1p1UltraCostState {
pub fn from_request(request: &FalFluxPro1p1UltraRequestState) -> Self {
// Delegate to the fal_client cost calculator: $0.06 per image.
Self {
cost_in_usd_cents: request.request.calculate_cost_in_cents(),
}
}
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
// Matches v1: credits == cents (1:1 with USD cents).
ImageGenerationCostEstimate {
cost_in_credits: Some(self.cost_in_usd_cents),
cost_in_usd_cents: Some(self.cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_ultra_text_to_image_webhook::{
FluxPro11UltraAspectRatio, FluxPro11UltraNumImages, FluxPro11UltraRequest,
};
fn cost_for(n: FluxPro11UltraNumImages) -> ImageGenerationCostEstimate {
let state = FalFluxPro1p1UltraCostState::from_request(&FalFluxPro1p1UltraRequestState {
request: FluxPro11UltraRequest {
prompt: "test".to_string(),
aspect_ratio: FluxPro11UltraAspectRatio::Square,
num_images: n,
},
});
state.estimate_cost()
}
#[test]
fn one_image_is_6_cents() {
assert_eq!(cost_for(FluxPro11UltraNumImages::One).cost_in_usd_cents, Some(6));
}
#[test]
fn two_images_is_12_cents() {
assert_eq!(cost_for(FluxPro11UltraNumImages::Two).cost_in_usd_cents, Some(12));
}
#[test]
fn three_images_is_18_cents() {
assert_eq!(cost_for(FluxPro11UltraNumImages::Three).cost_in_usd_cents, Some(18));
}
#[test]
fn four_images_is_24_cents() {
assert_eq!(cost_for(FluxPro11UltraNumImages::Four).cost_in_usd_cents, Some(24));
}
#[test]
fn credits_equal_cents() {
let cost = cost_for(FluxPro11UltraNumImages::Two);
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
}
#[test]
fn cost_flags_match_v1() {
let cost = cost_for(FluxPro11UltraNumImages::One);
assert!(!cost.is_free);
assert!(!cost.is_unlimited);
assert!(!cost.is_rate_limited);
assert!(!cost.has_watermark);
assert!(cost.failures_are_refunded.is_none());
}
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -0,0 +1,82 @@
use std::fmt::Debug;
use std::sync::Arc;
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_ultra_text_to_image_webhook::{
enqueue_flux_pro_11_ultra_text_to_image_webhook, FluxPro11UltraArgs, FluxPro11UltraRequest,
};
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
FalImageResponsePayload, GenerateImageResponse,
};
#[derive(Clone, Debug)]
pub struct FalFluxPro1p1UltraRequestState {
pub request: FluxPro11UltraRequest,
}
impl FalFluxPro1p1UltraRequestState {
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(self.request.clone());
let args = FluxPro11UltraArgs {
request: self.request.clone(),
webhook_url: client.webhook_url.as_str(),
api_key: &client.api_key,
};
let webhook_response = enqueue_flux_pro_11_ultra_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: webhook_response.request_id,
gateway_request_id: webhook_response.gateway_request_id,
maybe_status_url: None,
maybe_response_url: None,
maybe_outbound_request: Some(outbound),
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
use fal_client::creds::fal_api_key::FalApiKey;
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_ultra_text_to_image_webhook::{
FluxPro11UltraAspectRatio, FluxPro11UltraNumImages,
};
fn read_fal_api_key() -> FalApiKey {
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
.expect("Failed to read fal_api_key.txt");
FalApiKey::from_str(secret.trim())
}
fn client_with_webhook() -> RouterFalClient {
RouterFalClient::new(
read_fal_api_key(),
"https://example.com/fal-webhook-test".to_string(),
)
}
fn t2i_state() -> FalFluxPro1p1UltraRequestState {
FalFluxPro1p1UltraRequestState {
request: FluxPro11UltraRequest {
prompt: "a corgi wearing sunglasses on a surfboard".to_string(),
aspect_ratio: FluxPro11UltraAspectRatio::LandscapeSixteenByNine,
num_images: FluxPro11UltraNumImages::One,
},
}
}
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_via_webhook() {
let client = client_with_webhook();
let response = t2i_state().send(&client).await.expect("send should succeed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Webhook t2i — request_id: {:?}, gateway_request_id: {:?}", payload.request_id, payload.gateway_request_id);
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
}
}
@@ -1,7 +1,12 @@
pub mod flux_1_dev;
pub mod flux_1_schnell;
pub mod flux_pro_1p1;
pub mod flux_pro_1p1_ultra;
pub mod gpt_image_1;
pub mod gpt_image_1p5;
pub mod gpt_image_2;
pub mod nano_banana_2;
pub mod nano_banana_pro;
#[cfg(test)]
mod cost_parity_tests;