mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-06 02:00:46 +00:00
port flux pro on fal to pipeline v2
This commit is contained in:
1 parent
370d2fc494
commit
d7357028b7
12 files changed
+1209
No files matched your search
+6
@@ -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),
|
||||
|
||||
+23
@@ -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
|
||||
|
||||
+251
@@ -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);
|
||||
}
|
||||
}
|
||||
+302
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+86
@@ -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());
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+82
@@ -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());
|
||||
}
|
||||
}
|
||||
+281
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+85
@@ -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());
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+82
@@ -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());
|
||||
}
|
||||
}
|
||||
+5
@@ -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;
|
||||
Reference in new issue
Block a user