mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-25 02:29:37 +00:00
tests
This commit is contained in:
-23
@@ -649,12 +649,6 @@ mod nano_banana_2 {
|
||||
]
|
||||
}
|
||||
|
||||
/// Currently fails: v1 prices `resolution=None` at 8¢ (1K default); v2 reads
|
||||
/// 15¢ from fal_client. The defaults predate the trait and need to be
|
||||
/// reconciled — either fix v2 to clamp `None`→OneK before delegating, or
|
||||
/// update v1 to read from fal_client too. Strict parity test kept here for
|
||||
/// regression detection once the fix lands.
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn cost_parity_full_combinatorial() {
|
||||
let batches = [None, Some(1u16), Some(4)];
|
||||
@@ -722,9 +716,6 @@ mod nano_banana_pro {
|
||||
]
|
||||
}
|
||||
|
||||
/// Currently fails: v1 maps `ThreeK`→TwoK pricing (15¢); v2 routes
|
||||
/// `ThreeK`→FourK in fal_client (30¢). Pick a side and update the other.
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn cost_parity_full_combinatorial() {
|
||||
let batches = [None, Some(1u16), Some(4)];
|
||||
@@ -791,11 +782,6 @@ mod gpt_image_1 {
|
||||
base_builder(CommonImageModel::GptImage1)
|
||||
}
|
||||
|
||||
/// Currently fails: v1 bills 2¢ per input image (edit-mode high-fidelity
|
||||
/// estimate); v2 doesn't bill input-image tokens. Decide whether to
|
||||
/// preserve the input-image fee in v2 or drop it from v1 (likely the
|
||||
/// former, since Fal does bill tokens for inputs in practice).
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn cost_parity_full_combinatorial() {
|
||||
let batches = [None, Some(1u16), Some(2), Some(4)];
|
||||
@@ -841,10 +827,6 @@ mod gpt_image_1p5 {
|
||||
base_builder(CommonImageModel::GptImage1p5)
|
||||
}
|
||||
|
||||
/// Currently fails: hand-rolled v1 cost table rounds $0.133→14¢ for
|
||||
/// High+Square; v2's fal_client trait returns 13¢. Off-by-one cent in
|
||||
/// several quality/size buckets — reconcile rounding direction.
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn cost_parity_full_combinatorial() {
|
||||
let batches = [None, Some(1u16), Some(2), Some(4)];
|
||||
@@ -886,11 +868,6 @@ mod gpt_image_2 {
|
||||
base_builder(CommonImageModel::GptImage2)
|
||||
}
|
||||
|
||||
/// Currently fails: v1 reads quality+size buckets explicitly and prices
|
||||
/// `q=None`→High→Auto at 23¢; v2 reads fal_client which returns 16¢ for the
|
||||
/// same input shape. Differences exist across most matrix cells — needs a
|
||||
/// full price-table reconciliation.
|
||||
#[ignore]
|
||||
#[test]
|
||||
fn cost_parity_full_combinatorial() {
|
||||
let batches = [None, Some(1u16), Some(2), Some(4)];
|
||||
|
||||
+4
-3
@@ -18,12 +18,13 @@ impl FalFlux1DevCostState {
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
// v1 sets cost_in_credits to the same value as USD cents (1:1).
|
||||
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,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
@@ -107,6 +108,6 @@ mod tests {
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
assert!(!cost.has_watermark);
|
||||
assert!(cost.cost_in_credits.is_none());
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -18,7 +18,8 @@ impl FalFlux1SchnellCostState {
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
// v1 sets cost_in_credits to the same value as USD cents (1:1).
|
||||
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,
|
||||
@@ -108,6 +109,6 @@ mod tests {
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
assert!(!cost.has_watermark);
|
||||
assert!(cost.cost_in_credits.is_none());
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
}
|
||||
}
|
||||
|
||||
+116
-21
@@ -1,8 +1,27 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
use fal_client::requests::api::image::edit::gpt_image_1_edit_image::api::{
|
||||
GptImage1EditImageNumImages, GptImage1EditImageQuality, GptImage1EditImageSize,
|
||||
};
|
||||
use fal_client::requests::api::image::text::gpt_image_1_text_to_image::api::{
|
||||
GptImage1TextToImageNumImages, GptImage1TextToImageQuality, GptImage1TextToImageSize,
|
||||
};
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::request::FalGptImage1RequestState;
|
||||
|
||||
/// Cost state for Fal GPT Image 1. Mirrors v1
|
||||
/// (`estimate_image_cost_fal_gpt_image_1`):
|
||||
///
|
||||
/// Output image cost (per output image):
|
||||
/// Low: 2¢ all sizes
|
||||
/// Medium: 5¢ square, 7¢ wide/tall
|
||||
/// High: 17¢ square, 25¢ wide/tall
|
||||
///
|
||||
/// Plus 2¢ per input image in edit mode (high-fidelity estimate).
|
||||
/// Quality defaults to High when unspecified.
|
||||
///
|
||||
/// The hand-rolled rates here intentionally override fal_client's trait
|
||||
/// (which uses Medium as the default and omits the input-image fee). Keep
|
||||
/// in sync with the v1 cost file.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalGptImage1CostState {
|
||||
request: FalGptImage1RequestState,
|
||||
@@ -15,12 +34,19 @@ impl FalGptImage1CostState {
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = match &self.request {
|
||||
FalGptImage1RequestState::TextToImage(req) => req.calculate_cost_in_cents(),
|
||||
FalGptImage1RequestState::EditImage(req) => req.calculate_cost_in_cents(),
|
||||
FalGptImage1RequestState::TextToImage(req) => {
|
||||
cost_t2i(req.quality, req.image_size, req.num_images)
|
||||
}
|
||||
FalGptImage1RequestState::EditImage(req) => {
|
||||
let output_cost = cost_edit_output(req.quality, req.image_size, req.num_images);
|
||||
let input_cost = 2 * req.image_urls.len() as u64;
|
||||
output_cost + input_cost
|
||||
}
|
||||
};
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
// v1 sets cost_in_credits to the same value as USD cents (1:1).
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
@@ -31,6 +57,61 @@ impl FalGptImage1CostState {
|
||||
}
|
||||
}
|
||||
|
||||
fn cost_t2i(
|
||||
quality: Option<GptImage1TextToImageQuality>,
|
||||
image_size: Option<GptImage1TextToImageSize>,
|
||||
num_images: GptImage1TextToImageNumImages,
|
||||
) -> u64 {
|
||||
use GptImage1TextToImageQuality as Q;
|
||||
use GptImage1TextToImageSize as S;
|
||||
let q = quality.unwrap_or(Q::High);
|
||||
let is_square = matches!(image_size, None | Some(S::Square));
|
||||
let per_image = output_cost(matches!(q, Q::Low), matches!(q, Q::Medium), is_square);
|
||||
per_image * t2i_num_images(num_images)
|
||||
}
|
||||
|
||||
fn cost_edit_output(
|
||||
quality: Option<GptImage1EditImageQuality>,
|
||||
image_size: Option<GptImage1EditImageSize>,
|
||||
num_images: GptImage1EditImageNumImages,
|
||||
) -> u64 {
|
||||
use GptImage1EditImageQuality as Q;
|
||||
use GptImage1EditImageSize as S;
|
||||
let q = quality.unwrap_or(Q::High);
|
||||
let is_square = matches!(image_size, None | Some(S::Square));
|
||||
let per_image = output_cost(matches!(q, Q::Low), matches!(q, Q::Medium), is_square);
|
||||
per_image * edit_num_images(num_images)
|
||||
}
|
||||
|
||||
fn output_cost(is_low: bool, is_medium: bool, is_square: bool) -> u64 {
|
||||
if is_low {
|
||||
2
|
||||
} else if is_medium {
|
||||
if is_square { 5 } else { 7 }
|
||||
} else {
|
||||
// High (default)
|
||||
if is_square { 17 } else { 25 }
|
||||
}
|
||||
}
|
||||
|
||||
fn t2i_num_images(n: GptImage1TextToImageNumImages) -> u64 {
|
||||
match n {
|
||||
GptImage1TextToImageNumImages::One => 1,
|
||||
GptImage1TextToImageNumImages::Two => 2,
|
||||
GptImage1TextToImageNumImages::Three => 3,
|
||||
GptImage1TextToImageNumImages::Four => 4,
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_num_images(n: GptImage1EditImageNumImages) -> u64 {
|
||||
match n {
|
||||
GptImage1EditImageNumImages::One => 1,
|
||||
GptImage1EditImageNumImages::Two => 2,
|
||||
GptImage1EditImageNumImages::Three => 3,
|
||||
GptImage1EditImageNumImages::Four => 4,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -60,39 +141,53 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// Tables encode v1's pricing semantics:
|
||||
// - Quality defaults to High when unspecified.
|
||||
// - is_square = matches!(image_size, None | Some(Square)). Auto, Horizontal,
|
||||
// and Vertical are treated as non-square.
|
||||
// - Low is 2¢ for every size; Medium and High differ by square vs non-square.
|
||||
// Edit-mode rows additionally include a +2¢ input-image fee (one input URL
|
||||
// in the make_edit helper below).
|
||||
fn text_to_image_price_cases() -> [(Option<GptImage1TextToImageQuality>, Option<GptImage1TextToImageSize>, [u64; 4]); 13] {
|
||||
[
|
||||
(None, None, [5, 10, 15, 20]),
|
||||
// Default quality (None → High) + default size (None → square).
|
||||
(None, None, [17, 34, 51, 68]),
|
||||
// Low: flat 2¢ regardless of size.
|
||||
(Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Auto), [2, 4, 6, 8]),
|
||||
(Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Square), [2, 4, 6, 8]),
|
||||
(Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Horizontal), [2, 4, 6, 8]),
|
||||
(Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Vertical), [2, 4, 6, 8]),
|
||||
(Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Auto), [5, 10, 15, 20]),
|
||||
// Medium: 5¢ square, 7¢ non-square.
|
||||
(Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Auto), [7, 14, 21, 28]),
|
||||
(Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Square), [5, 10, 15, 20]),
|
||||
(Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Horizontal), [7, 14, 21, 28]),
|
||||
(Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Vertical), [7, 14, 21, 28]),
|
||||
(Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Auto), [17, 34, 51, 68]),
|
||||
// High: 17¢ square, 25¢ non-square.
|
||||
(Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Auto), [25, 50, 75, 100]),
|
||||
(Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Square), [17, 34, 51, 68]),
|
||||
(Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Horizontal), [25, 50, 75, 100]),
|
||||
(Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Vertical), [25, 50, 75, 100]),
|
||||
]
|
||||
}
|
||||
|
||||
// Each edit-mode row is t2i row + 2¢ (single input image — see make_edit
|
||||
// below). Numbers are constant +2 because input fee scales with input count,
|
||||
// not output count.
|
||||
fn edit_image_price_cases() -> [(Option<GptImage1EditImageQuality>, Option<GptImage1EditImageSize>, [u64; 4]); 13] {
|
||||
[
|
||||
(None, None, [5, 10, 15, 20]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Auto), [2, 4, 6, 8]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Square), [2, 4, 6, 8]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Horizontal), [2, 4, 6, 8]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Vertical), [2, 4, 6, 8]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Auto), [5, 10, 15, 20]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Square), [5, 10, 15, 20]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Horizontal), [7, 14, 21, 28]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Vertical), [7, 14, 21, 28]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Auto), [17, 34, 51, 68]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Square), [17, 34, 51, 68]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Horizontal), [25, 50, 75, 100]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Vertical), [25, 50, 75, 100]),
|
||||
(None, None, [19, 36, 53, 70]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Auto), [4, 6, 8, 10]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Square), [4, 6, 8, 10]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Horizontal), [4, 6, 8, 10]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Vertical), [4, 6, 8, 10]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Auto), [9, 16, 23, 30]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Square), [7, 12, 17, 22]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Horizontal), [9, 16, 23, 30]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Vertical), [9, 16, 23, 30]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Auto), [27, 52, 77, 102]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Square), [19, 36, 53, 70]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Horizontal), [27, 52, 77, 102]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Vertical), [27, 52, 77, 102]),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -106,7 +201,7 @@ mod tests {
|
||||
|
||||
fn assert_standard_cost_estimate(cost: ImageGenerationCostEstimate, expected_usd_cents: u64) {
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(expected_usd_cents));
|
||||
assert_eq!(cost.cost_in_credits, None);
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
assert!(!cost.is_free);
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
|
||||
+126
-7
@@ -1,8 +1,26 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
use fal_client::requests::api::image::edit::gpt_image_1p5_edit_image::api::{
|
||||
GptImage1p5EditImageNumImages, GptImage1p5EditImageQuality, GptImage1p5EditImageSize,
|
||||
};
|
||||
use fal_client::requests::api::image::text::gpt_image_1p5_text_to_image::api::{
|
||||
GptImage1p5TextToImageNumImages, GptImage1p5TextToImageQuality, GptImage1p5TextToImageSize,
|
||||
};
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::request::FalGptImage1p5RequestState;
|
||||
|
||||
/// Cost state for Fal GPT Image 1.5. Mirrors v1
|
||||
/// (`estimate_image_cost_fal_gpt_image_1p5`):
|
||||
///
|
||||
/// Output image cost (per output image), rounded up to whole cents:
|
||||
/// Low: 1¢ square, 2¢ wide/tall
|
||||
/// Medium: 4¢ square, 5¢ wide, 6¢ tall
|
||||
/// High: 14¢ square, 20¢ wide/tall
|
||||
///
|
||||
/// Quality defaults to High when unspecified.
|
||||
///
|
||||
/// The hand-rolled rates here intentionally override fal_client's trait,
|
||||
/// which rounds $0.133→13¢ for High+Square (v1 rounds up to 14¢) and
|
||||
/// flattens Medium wide/tall to a single value. Keep in sync with v1.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalGptImage1p5CostState {
|
||||
request: FalGptImage1p5RequestState,
|
||||
@@ -15,12 +33,17 @@ impl FalGptImage1p5CostState {
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = match &self.request {
|
||||
FalGptImage1p5RequestState::TextToImage(req) => req.calculate_cost_in_cents(),
|
||||
FalGptImage1p5RequestState::EditImage(req) => req.calculate_cost_in_cents(),
|
||||
FalGptImage1p5RequestState::TextToImage(req) => {
|
||||
cost_t2i(req.quality, req.image_size, req.num_images)
|
||||
}
|
||||
FalGptImage1p5RequestState::EditImage(req) => {
|
||||
cost_edit(req.quality, req.image_size, req.num_images)
|
||||
}
|
||||
};
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
// v1 sets cost_in_credits to the same value as USD cents (1:1).
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
@@ -31,6 +54,76 @@ impl FalGptImage1p5CostState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum SizeBucket { Square, Wide, Tall }
|
||||
|
||||
fn cost_t2i(
|
||||
quality: Option<GptImage1p5TextToImageQuality>,
|
||||
image_size: Option<GptImage1p5TextToImageSize>,
|
||||
num_images: GptImage1p5TextToImageNumImages,
|
||||
) -> u64 {
|
||||
use GptImage1p5TextToImageQuality as Q;
|
||||
use GptImage1p5TextToImageSize as S;
|
||||
let q = quality.unwrap_or(Q::High);
|
||||
let bucket = match image_size {
|
||||
None | Some(S::Square) => SizeBucket::Square,
|
||||
Some(S::Wide) => SizeBucket::Wide,
|
||||
Some(S::Tall) => SizeBucket::Tall,
|
||||
};
|
||||
let per_image = output_cost(matches!(q, Q::Low), matches!(q, Q::Medium), bucket);
|
||||
per_image * t2i_num_images(num_images)
|
||||
}
|
||||
|
||||
fn cost_edit(
|
||||
quality: Option<GptImage1p5EditImageQuality>,
|
||||
image_size: Option<GptImage1p5EditImageSize>,
|
||||
num_images: GptImage1p5EditImageNumImages,
|
||||
) -> u64 {
|
||||
use GptImage1p5EditImageQuality as Q;
|
||||
use GptImage1p5EditImageSize as S;
|
||||
let q = quality.unwrap_or(Q::High);
|
||||
let bucket = match image_size {
|
||||
None | Some(S::Square) => SizeBucket::Square,
|
||||
Some(S::Wide) => SizeBucket::Wide,
|
||||
Some(S::Tall) => SizeBucket::Tall,
|
||||
};
|
||||
let per_image = output_cost(matches!(q, Q::Low), matches!(q, Q::Medium), bucket);
|
||||
per_image * edit_num_images(num_images)
|
||||
}
|
||||
|
||||
fn output_cost(is_low: bool, is_medium: bool, bucket: SizeBucket) -> u64 {
|
||||
if is_low {
|
||||
match bucket { SizeBucket::Square => 1, _ => 2 }
|
||||
} else if is_medium {
|
||||
match bucket {
|
||||
SizeBucket::Square => 4,
|
||||
SizeBucket::Wide => 5,
|
||||
SizeBucket::Tall => 6,
|
||||
}
|
||||
} else {
|
||||
// High (default)
|
||||
match bucket { SizeBucket::Square => 14, _ => 20 }
|
||||
}
|
||||
}
|
||||
|
||||
fn t2i_num_images(n: GptImage1p5TextToImageNumImages) -> u64 {
|
||||
match n {
|
||||
GptImage1p5TextToImageNumImages::One => 1,
|
||||
GptImage1p5TextToImageNumImages::Two => 2,
|
||||
GptImage1p5TextToImageNumImages::Three => 3,
|
||||
GptImage1p5TextToImageNumImages::Four => 4,
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_num_images(n: GptImage1p5EditImageNumImages) -> u64 {
|
||||
match n {
|
||||
GptImage1p5EditImageNumImages::One => 1,
|
||||
GptImage1p5EditImageNumImages::Two => 2,
|
||||
GptImage1p5EditImageNumImages::Three => 3,
|
||||
GptImage1p5EditImageNumImages::Four => 4,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -60,12 +153,38 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// v1 rates: Low=1/2 (sq/wide-tall), Medium=4/5/6 (sq/wide/tall),
|
||||
// High=14/20/20 (sq/wide/tall). Default quality is High.
|
||||
fn text_to_image_price_cases() -> [(Option<GptImage1p5TextToImageQuality>, Option<GptImage1p5TextToImageSize>, [u64; 4]); 10] {
|
||||
[(None, None, [3, 6, 9, 12]), (Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Square), [1, 2, 3, 4]), (Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Wide), [1, 2, 3, 4]), (Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Tall), [1, 2, 3, 4]), (Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Square), [3, 6, 9, 12]), (Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Wide), [5, 10, 15, 20]), (Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Tall), [5, 10, 15, 20]), (Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Square), [13, 26, 39, 52]), (Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Wide), [20, 40, 60, 80]), (Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Tall), [20, 40, 60, 80])]
|
||||
[
|
||||
(None, None, [14, 28, 42, 56]),
|
||||
(Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Square), [1, 2, 3, 4]),
|
||||
(Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Wide), [2, 4, 6, 8]),
|
||||
(Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Tall), [2, 4, 6, 8]),
|
||||
(Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Square), [4, 8, 12, 16]),
|
||||
(Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Wide), [5, 10, 15, 20]),
|
||||
(Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Tall), [6, 12, 18, 24]),
|
||||
(Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Square), [14, 28, 42, 56]),
|
||||
(Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Wide), [20, 40, 60, 80]),
|
||||
(Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Tall), [20, 40, 60, 80]),
|
||||
]
|
||||
}
|
||||
|
||||
// Edit-mode pricing is identical to t2i (v1 has no input-image fee for
|
||||
// gpt_image_1p5 — that's only gpt_image_1).
|
||||
fn edit_image_price_cases() -> [(Option<GptImage1p5EditImageQuality>, Option<GptImage1p5EditImageSize>, [u64; 4]); 10] {
|
||||
[(None, None, [3, 6, 9, 12]), (Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Square), [1, 2, 3, 4]), (Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Wide), [1, 2, 3, 4]), (Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Tall), [1, 2, 3, 4]), (Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Square), [3, 6, 9, 12]), (Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Wide), [5, 10, 15, 20]), (Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Tall), [5, 10, 15, 20]), (Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Square), [13, 26, 39, 52]), (Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Wide), [20, 40, 60, 80]), (Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Tall), [20, 40, 60, 80])]
|
||||
[
|
||||
(None, None, [14, 28, 42, 56]),
|
||||
(Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Square), [1, 2, 3, 4]),
|
||||
(Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Wide), [2, 4, 6, 8]),
|
||||
(Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Tall), [2, 4, 6, 8]),
|
||||
(Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Square), [4, 8, 12, 16]),
|
||||
(Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Wide), [5, 10, 15, 20]),
|
||||
(Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Tall), [6, 12, 18, 24]),
|
||||
(Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Square), [14, 28, 42, 56]),
|
||||
(Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Wide), [20, 40, 60, 80]),
|
||||
(Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Tall), [20, 40, 60, 80]),
|
||||
]
|
||||
}
|
||||
|
||||
fn text_to_image_num_image_cases(expected_by_count: [u64; 4]) -> [(GptImage1p5TextToImageNumImages, u64); 4] {
|
||||
@@ -78,7 +197,7 @@ mod tests {
|
||||
|
||||
fn assert_standard_cost_estimate(cost: ImageGenerationCostEstimate, expected_usd_cents: u64) {
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(expected_usd_cents));
|
||||
assert_eq!(cost.cost_in_credits, None);
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
assert!(!cost.is_free);
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
|
||||
+190
-138
@@ -1,8 +1,28 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
use fal_client::requests::api::image::edit::gpt_image_2_edit_image::api::{
|
||||
GptImage2EditImageNumImages, GptImage2EditImageQuality, GptImage2EditImageSize,
|
||||
};
|
||||
use fal_client::requests::api::image::text::gpt_image_2_text_to_image::api::{
|
||||
GptImage2TextToImageNumImages, GptImage2TextToImageQuality, GptImage2TextToImageSize,
|
||||
};
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::request::FalGptImage2RequestState;
|
||||
|
||||
/// Cost state for Fal GPT Image 2. Mirrors v1
|
||||
/// (`estimate_image_cost_fal_gpt_image_2`):
|
||||
///
|
||||
/// Output image cost (per output image), rounded up to whole cents:
|
||||
/// Low: 1¢ all sizes
|
||||
/// Medium: 6¢ square/squareHD/auto; 4¢ landscape/portrait (4:3 or 16:9)
|
||||
/// High: 22¢ square; 23¢ squareHD/auto;
|
||||
/// 15¢ landscape/portrait 4:3; 16¢ landscape/portrait 16:9
|
||||
///
|
||||
/// Quality defaults to High when unspecified. Resolution is NOT priced
|
||||
/// separately — v1's table only varies by quality + size.
|
||||
///
|
||||
/// The hand-rolled rates here intentionally override fal_client's trait,
|
||||
/// which uses a base+per-megapixel formula that scales with the resolution
|
||||
/// field. Keep in sync with v1.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalGptImage2CostState {
|
||||
request: FalGptImage2RequestState,
|
||||
@@ -15,12 +35,17 @@ impl FalGptImage2CostState {
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = match &self.request {
|
||||
FalGptImage2RequestState::TextToImage(req) => req.calculate_cost_in_cents(),
|
||||
FalGptImage2RequestState::EditImage(req) => req.calculate_cost_in_cents(),
|
||||
FalGptImage2RequestState::TextToImage(req) => {
|
||||
cost_t2i(req.quality, req.image_size, req.num_images)
|
||||
}
|
||||
FalGptImage2RequestState::EditImage(req) => {
|
||||
cost_edit(req.quality, req.image_size, req.num_images)
|
||||
}
|
||||
};
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
// v1 sets cost_in_credits to the same value as USD cents (1:1).
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
@@ -31,22 +56,115 @@ impl FalGptImage2CostState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum SizeBucket {
|
||||
Square,
|
||||
SquareHd,
|
||||
Landscape4x3,
|
||||
Landscape16x9,
|
||||
Portrait4x3,
|
||||
Portrait16x9,
|
||||
Auto,
|
||||
}
|
||||
|
||||
fn cost_t2i(
|
||||
quality: Option<GptImage2TextToImageQuality>,
|
||||
image_size: Option<GptImage2TextToImageSize>,
|
||||
num_images: GptImage2TextToImageNumImages,
|
||||
) -> u64 {
|
||||
use GptImage2TextToImageQuality as Q;
|
||||
use GptImage2TextToImageSize as S;
|
||||
let q = quality.unwrap_or(Q::High);
|
||||
// T2I size enum has no Auto variant — `None` is the only auto-equivalent.
|
||||
let bucket = match image_size {
|
||||
None => SizeBucket::Auto,
|
||||
Some(S::Square) => SizeBucket::Square,
|
||||
Some(S::SquareHd) => SizeBucket::SquareHd,
|
||||
Some(S::Landscape4x3) => SizeBucket::Landscape4x3,
|
||||
Some(S::Landscape16x9) => SizeBucket::Landscape16x9,
|
||||
Some(S::Portrait4x3) => SizeBucket::Portrait4x3,
|
||||
Some(S::Portrait16x9) => SizeBucket::Portrait16x9,
|
||||
};
|
||||
output_cost(matches!(q, Q::Low), matches!(q, Q::Medium), bucket) * t2i_num_images(num_images)
|
||||
}
|
||||
|
||||
fn cost_edit(
|
||||
quality: Option<GptImage2EditImageQuality>,
|
||||
image_size: Option<GptImage2EditImageSize>,
|
||||
num_images: GptImage2EditImageNumImages,
|
||||
) -> u64 {
|
||||
use GptImage2EditImageQuality as Q;
|
||||
use GptImage2EditImageSize as S;
|
||||
let q = quality.unwrap_or(Q::High);
|
||||
let bucket = match image_size {
|
||||
None | Some(S::Auto) => SizeBucket::Auto,
|
||||
Some(S::Square) => SizeBucket::Square,
|
||||
Some(S::SquareHd) => SizeBucket::SquareHd,
|
||||
Some(S::Landscape4x3) => SizeBucket::Landscape4x3,
|
||||
Some(S::Landscape16x9) => SizeBucket::Landscape16x9,
|
||||
Some(S::Portrait4x3) => SizeBucket::Portrait4x3,
|
||||
Some(S::Portrait16x9) => SizeBucket::Portrait16x9,
|
||||
};
|
||||
output_cost(matches!(q, Q::Low), matches!(q, Q::Medium), bucket) * edit_num_images(num_images)
|
||||
}
|
||||
|
||||
fn output_cost(is_low: bool, is_medium: bool, bucket: SizeBucket) -> u64 {
|
||||
if is_low {
|
||||
return 1;
|
||||
}
|
||||
if is_medium {
|
||||
return match bucket {
|
||||
SizeBucket::Square | SizeBucket::SquareHd | SizeBucket::Auto => 6,
|
||||
SizeBucket::Landscape4x3 | SizeBucket::Portrait4x3
|
||||
| SizeBucket::Landscape16x9 | SizeBucket::Portrait16x9 => 4,
|
||||
};
|
||||
}
|
||||
// High (default)
|
||||
match bucket {
|
||||
SizeBucket::Square => 22,
|
||||
SizeBucket::SquareHd | SizeBucket::Auto => 23,
|
||||
SizeBucket::Landscape4x3 | SizeBucket::Portrait4x3 => 15,
|
||||
SizeBucket::Landscape16x9 | SizeBucket::Portrait16x9 => 16,
|
||||
}
|
||||
}
|
||||
|
||||
fn t2i_num_images(n: GptImage2TextToImageNumImages) -> u64 {
|
||||
match n {
|
||||
GptImage2TextToImageNumImages::One => 1,
|
||||
GptImage2TextToImageNumImages::Two => 2,
|
||||
GptImage2TextToImageNumImages::Three => 3,
|
||||
GptImage2TextToImageNumImages::Four => 4,
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_num_images(n: GptImage2EditImageNumImages) -> u64 {
|
||||
match n {
|
||||
GptImage2EditImageNumImages::One => 1,
|
||||
GptImage2EditImageNumImages::Two => 2,
|
||||
GptImage2EditImageNumImages::Three => 3,
|
||||
GptImage2EditImageNumImages::Four => 4,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use fal_client::requests::api::image::common::gpt_image_2_resolution::GptImage2Resolution;
|
||||
use fal_client::requests::api::image::edit::gpt_image_2_edit_image::api::{GptImage2EditImageNumImages, GptImage2EditImageQuality, GptImage2EditImageRequest, GptImage2EditImageSize};
|
||||
use fal_client::requests::api::image::text::gpt_image_2_text_to_image::api::{GptImage2TextToImageNumImages, GptImage2TextToImageQuality, GptImage2TextToImageRequest, GptImage2TextToImageSize};
|
||||
|
||||
type TextPriceCase = (Option<GptImage2TextToImageQuality>, Option<GptImage2TextToImageSize>, Option<GptImage2Resolution>, [u64; 4]);
|
||||
type EditPriceCase = (Option<GptImage2EditImageQuality>, Option<GptImage2EditImageSize>, Option<GptImage2Resolution>, [u64; 4]);
|
||||
type TextPriceCase = (Option<GptImage2TextToImageQuality>, Option<GptImage2TextToImageSize>, [u64; 4]);
|
||||
type EditPriceCase = (Option<GptImage2EditImageQuality>, Option<GptImage2EditImageSize>, [u64; 4]);
|
||||
|
||||
// v1 doesn't price by resolution — only quality + size. The Fal request's
|
||||
// resolution field is sent to Fal for actual generation but doesn't enter
|
||||
// the cost calculation.
|
||||
|
||||
#[test]
|
||||
fn text_to_image_cost_table_covers_quality_size_resolution_and_count() {
|
||||
for (quality, image_size, resolution, expected_by_count) in text_to_image_price_cases() {
|
||||
fn text_to_image_cost_table_covers_quality_and_size() {
|
||||
for (quality, image_size, expected_by_count) in text_to_image_price_cases() {
|
||||
for (num_images, expected) in text_to_image_num_image_cases(expected_by_count) {
|
||||
let request = FalGptImage2RequestState::TextToImage(GptImage2TextToImageRequest { prompt: "test".to_string(), num_images, image_size, resolution, quality, output_format: None });
|
||||
let request = FalGptImage2RequestState::TextToImage(GptImage2TextToImageRequest { prompt: "test".to_string(), num_images, image_size, resolution: None, quality, output_format: None });
|
||||
let cost = FalGptImage2CostState::from_request(&request).estimate_cost();
|
||||
assert_standard_cost_estimate(cost, expected);
|
||||
}
|
||||
@@ -54,146 +172,80 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_image_cost_table_covers_quality_size_resolution_and_count() {
|
||||
for (quality, image_size, resolution, expected_by_count) in edit_image_price_cases() {
|
||||
fn edit_image_cost_table_covers_quality_and_size() {
|
||||
for (quality, image_size, expected_by_count) in edit_image_price_cases() {
|
||||
for (num_images, expected) in edit_image_num_image_cases(expected_by_count) {
|
||||
let request = FalGptImage2RequestState::EditImage(GptImage2EditImageRequest { prompt: "test".to_string(), image_urls: vec!["https://example.com/image.png".to_string()], num_images, mask_url: None, image_size, resolution, quality, output_format: None });
|
||||
let request = FalGptImage2RequestState::EditImage(GptImage2EditImageRequest { prompt: "test".to_string(), image_urls: vec!["https://example.com/image.png".to_string()], num_images, mask_url: None, image_size, resolution: None, quality, output_format: None });
|
||||
let cost = FalGptImage2CostState::from_request(&request).estimate_cost();
|
||||
assert_standard_cost_estimate(cost, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// v1 rates (independent of resolution):
|
||||
// Low: 1¢ all sizes
|
||||
// Medium: 6¢ Square/SquareHd/Auto; 4¢ Landscape/Portrait (4:3 or 16:9)
|
||||
// High: 22¢ Square; 23¢ SquareHd/Auto; 15¢ 4:3 (L/P); 16¢ 16:9 (L/P)
|
||||
// Default quality is High.
|
||||
fn text_to_image_price_cases() -> Vec<TextPriceCase> {
|
||||
let mut cases = vec![
|
||||
// Default: High quality, Square
|
||||
(None, None, None, [16, 32, 48, 64]),
|
||||
// Presets (no resolution)
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape4x3), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait4x3), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Square), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape16x9), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait16x9), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::SquareHd), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape4x3), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait4x3), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Square), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape16x9), None, [6, 12, 18, 24]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait16x9), None, [6, 12, 18, 24]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::SquareHd), None, [8, 16, 24, 32]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape4x3), None, [15, 30, 45, 60]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait4x3), None, [15, 30, 45, 60]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Square), None, [16, 32, 48, 64]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape16x9), None, [20, 40, 60, 80]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait16x9), None, [20, 40, 60, 80]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::SquareHd), None, [27, 54, 81, 108]),
|
||||
];
|
||||
|
||||
for (size, resolution, low, medium, high) in text_to_image_resolution_price_cases() {
|
||||
cases.push((Some(GptImage2TextToImageQuality::Low), Some(size), Some(resolution), low));
|
||||
cases.push((Some(GptImage2TextToImageQuality::Medium), Some(size), Some(resolution), medium));
|
||||
cases.push((Some(GptImage2TextToImageQuality::High), Some(size), Some(resolution), high));
|
||||
}
|
||||
|
||||
cases
|
||||
}
|
||||
|
||||
fn edit_image_price_cases() -> Vec<EditPriceCase> {
|
||||
let mut cases = vec![
|
||||
// Default: High quality, Square
|
||||
(None, None, None, [15, 30, 45, 60]),
|
||||
// Auto (with and without resolution — same cost, conservative max)
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto), None, [10, 20, 30, 40]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), None, [41, 82, 123, 164]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [2, 4, 6, 8]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [10, 20, 30, 40]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [41, 82, 123, 164]),
|
||||
// Presets (no resolution)
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape4x3), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait4x3), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Square), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape16x9), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait16x9), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::SquareHd), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape4x3), None, [4, 8, 12, 16]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait4x3), None, [4, 8, 12, 16]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Square), None, [4, 8, 12, 16]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape16x9), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait16x9), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::SquareHd), None, [7, 14, 21, 28]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape4x3), None, [14, 28, 42, 56]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait4x3), None, [14, 28, 42, 56]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Square), None, [15, 30, 45, 60]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape16x9), None, [19, 38, 57, 76]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait16x9), None, [19, 38, 57, 76]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::SquareHd), None, [26, 52, 78, 104]),
|
||||
];
|
||||
|
||||
for (size, resolution, low, medium, high) in edit_image_resolution_price_cases() {
|
||||
cases.push((Some(GptImage2EditImageQuality::Low), Some(size), Some(resolution), low));
|
||||
cases.push((Some(GptImage2EditImageQuality::Medium), Some(size), Some(resolution), medium));
|
||||
cases.push((Some(GptImage2EditImageQuality::High), Some(size), Some(resolution), high));
|
||||
}
|
||||
|
||||
cases
|
||||
}
|
||||
|
||||
fn text_to_image_resolution_price_cases() -> Vec<(GptImage2TextToImageSize, GptImage2Resolution, [u64; 4], [u64; 4], [u64; 4])> {
|
||||
vec![
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 48, 72, 96]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [11, 22, 33, 44], [37, 74, 111, 148]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [41, 82, 123, 164]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::TwoK, [2, 4, 6, 8], [6, 12, 18, 24], [21, 42, 63, 84]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [31, 62, 93, 124]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 48, 72, 96]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [11, 22, 33, 44], [37, 74, 111, 148]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [41, 82, 123, 164]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::TwoK, [2, 4, 6, 8], [6, 12, 18, 24], [21, 42, 63, 84]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [31, 62, 93, 124]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [16, 32, 48, 64]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::TwoK, [2, 4, 6, 8], [8, 16, 24, 32], [27, 54, 81, 108]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::ThreeK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [16, 32, 48, 64]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::TwoK, [2, 4, 6, 8], [8, 16, 24, 32], [27, 54, 81, 108]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::ThreeK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(None, None, [23, 46, 69, 92]),
|
||||
// Low
|
||||
// No Auto variant on t2i — None is the only auto-equivalent (tested above).
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Square), [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::SquareHd), [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape4x3), [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape16x9), [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait4x3), [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait16x9), [1, 2, 3, 4]),
|
||||
// Medium
|
||||
// No Auto variant on t2i.
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Square), [6, 12, 18, 24]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::SquareHd), [6, 12, 18, 24]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape4x3), [4, 8, 12, 16]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape16x9), [4, 8, 12, 16]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait4x3), [4, 8, 12, 16]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait16x9), [4, 8, 12, 16]),
|
||||
// High
|
||||
// No Auto variant on t2i.
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Square), [22, 44, 66, 88]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::SquareHd), [23, 46, 69, 92]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape4x3), [15, 30, 45, 60]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape16x9), [16, 32, 48, 64]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait4x3), [15, 30, 45, 60]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait16x9), [16, 32, 48, 64]),
|
||||
]
|
||||
}
|
||||
|
||||
fn edit_image_resolution_price_cases() -> Vec<(GptImage2EditImageSize, GptImage2Resolution, [u64; 4], [u64; 4], [u64; 4])> {
|
||||
// Edit-mode pricing is identical to t2i (v1 has no input-image fee for
|
||||
// gpt_image_2 — only gpt_image_1 has that fee).
|
||||
fn edit_image_price_cases() -> Vec<EditPriceCase> {
|
||||
vec![
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::TwoK, [1, 2, 3, 4], [6, 12, 18, 24], [23, 46, 69, 92]),
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [36, 72, 108, 144]),
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [40, 80, 120, 160]),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::TwoK, [1, 2, 3, 4], [5, 10, 15, 20], [20, 40, 60, 80]),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::ThreeK, [1, 2, 3, 4], [8, 16, 24, 32], [30, 60, 90, 120]),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::TwoK, [1, 2, 3, 4], [6, 12, 18, 24], [23, 46, 69, 92]),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [36, 72, 108, 144]),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [40, 80, 120, 160]),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::TwoK, [1, 2, 3, 4], [5, 10, 15, 20], [20, 40, 60, 80]),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::ThreeK, [1, 2, 3, 4], [8, 16, 24, 32], [30, 60, 90, 120]),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [15, 30, 45, 60]),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::TwoK, [1, 2, 3, 4], [7, 14, 21, 28], [26, 52, 78, 104]),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [15, 30, 45, 60]),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::TwoK, [1, 2, 3, 4], [7, 14, 21, 28], [26, 52, 78, 104]),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(None, None, [23, 46, 69, 92]),
|
||||
// Low
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto), [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Square), [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::SquareHd), [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape4x3), [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape16x9), [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait4x3), [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait16x9), [1, 2, 3, 4]),
|
||||
// Medium
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto), [6, 12, 18, 24]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Square), [6, 12, 18, 24]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::SquareHd), [6, 12, 18, 24]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape4x3), [4, 8, 12, 16]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape16x9), [4, 8, 12, 16]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait4x3), [4, 8, 12, 16]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait16x9), [4, 8, 12, 16]),
|
||||
// High
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), [23, 46, 69, 92]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Square), [22, 44, 66, 88]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::SquareHd), [23, 46, 69, 92]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape4x3), [15, 30, 45, 60]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape16x9), [16, 32, 48, 64]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait4x3), [15, 30, 45, 60]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait16x9), [16, 32, 48, 64]),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -207,7 +259,7 @@ mod tests {
|
||||
|
||||
fn assert_standard_cost_estimate(cost: ImageGenerationCostEstimate, expected_usd_cents: u64) {
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(expected_usd_cents));
|
||||
assert_eq!(cost.cost_in_credits, None);
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
assert!(!cost.is_free);
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
|
||||
+7
-3
@@ -122,7 +122,10 @@ fn plan_resolution(resolution: Option<CommonResolution>) -> Option<PlannedResolu
|
||||
CommonResolution::HalfK | CommonResolution::FourEightyP => PlannedResolution::HalfK,
|
||||
CommonResolution::OneK | CommonResolution::SevenTwentyP | CommonResolution::TenEightyP => PlannedResolution::OneK,
|
||||
CommonResolution::TwoK => PlannedResolution::TwoK,
|
||||
CommonResolution::ThreeK | CommonResolution::FourK => PlannedResolution::FourK,
|
||||
// 3K isn't natively supported on Fal nano_banana_2; v1 downgrades to 2K
|
||||
// pricing (and TwoK resolution). Match v1 here so cost+behavior agree.
|
||||
CommonResolution::ThreeK => PlannedResolution::TwoK,
|
||||
CommonResolution::FourK => PlannedResolution::FourK,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -416,13 +419,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_k_rounds_up_to_four_k() {
|
||||
fn three_k_rounds_down_to_two_k() {
|
||||
// Match v1's downgrade behavior: 3K → 2K (cheaper, no 3K native support).
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
resolution: Some(CommonResolution::ThreeK),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_t2i(build_fal_nano_banana_2(builder));
|
||||
assert!(matches!(req.resolution, Some(NanoBanana2TextToImageResolution::FourK)));
|
||||
assert!(matches!(req.resolution, Some(NanoBanana2TextToImageResolution::TwoK)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+74
-16
@@ -1,24 +1,44 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
use fal_client::requests::api::image::edit::nano_banana_2_edit_image::api::{
|
||||
NanoBanana2EditImageNumImages, NanoBanana2EditImageResolution,
|
||||
};
|
||||
use fal_client::requests::api::image::text::nano_banana_2_text_to_image::api::{
|
||||
NanoBanana2TextToImageNumImages, NanoBanana2TextToImageResolution,
|
||||
};
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::request::FalNanoBanana2RequestState;
|
||||
|
||||
/// Cost state for Fal Nano Banana 2. Mirrors v1
|
||||
/// (`estimate_image_cost_fal_nano_banana_2`):
|
||||
///
|
||||
/// $0.08/image base (1K). Multipliers: 0.5K = 0.75x, 2K = 1.5x, 4K = 2x.
|
||||
/// 0.5K → 6¢, 1K → 8¢ (also default), 2K → 12¢, 4K → 16¢
|
||||
///
|
||||
/// The hand-rolled rates here intentionally override fal_client's trait,
|
||||
/// whose rates are placeholder values from a `TODO(bt): Verify pricing` block
|
||||
/// (8/15/15/30 instead of v1's 6/8/12/16). Keep this in sync with the v1
|
||||
/// cost file.
|
||||
pub struct FalNanoBanana2CostState {
|
||||
cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl FalNanoBanana2CostState {
|
||||
pub fn from_request(request: &FalNanoBanana2RequestState) -> Self {
|
||||
let cost_in_usd_cents = match request {
|
||||
FalNanoBanana2RequestState::TextToImage(req) => req.calculate_cost_in_cents(),
|
||||
FalNanoBanana2RequestState::EditImage(req) => req.calculate_cost_in_cents(),
|
||||
let (cost_per_image, num_images) = match request {
|
||||
FalNanoBanana2RequestState::TextToImage(req) => {
|
||||
(cost_per_image_for_t2i_resolution(req.resolution), t2i_num_images(req.num_images))
|
||||
}
|
||||
FalNanoBanana2RequestState::EditImage(req) => {
|
||||
(cost_per_image_for_edit_resolution(req.resolution), edit_num_images(req.num_images))
|
||||
}
|
||||
};
|
||||
Self { cost_in_usd_cents }
|
||||
Self { cost_in_usd_cents: cost_per_image * num_images }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
// v1 sets cost_in_credits to the same value as USD cents (1:1).
|
||||
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,
|
||||
@@ -29,6 +49,42 @@ impl FalNanoBanana2CostState {
|
||||
}
|
||||
}
|
||||
|
||||
fn cost_per_image_for_t2i_resolution(r: Option<NanoBanana2TextToImageResolution>) -> u64 {
|
||||
match r {
|
||||
Some(NanoBanana2TextToImageResolution::HalfK) => 6,
|
||||
Some(NanoBanana2TextToImageResolution::OneK) | None => 8,
|
||||
Some(NanoBanana2TextToImageResolution::TwoK) => 12,
|
||||
Some(NanoBanana2TextToImageResolution::FourK) => 16,
|
||||
}
|
||||
}
|
||||
|
||||
fn cost_per_image_for_edit_resolution(r: Option<NanoBanana2EditImageResolution>) -> u64 {
|
||||
match r {
|
||||
Some(NanoBanana2EditImageResolution::HalfK) => 6,
|
||||
Some(NanoBanana2EditImageResolution::OneK) | None => 8,
|
||||
Some(NanoBanana2EditImageResolution::TwoK) => 12,
|
||||
Some(NanoBanana2EditImageResolution::FourK) => 16,
|
||||
}
|
||||
}
|
||||
|
||||
fn t2i_num_images(n: NanoBanana2TextToImageNumImages) -> u64 {
|
||||
match n {
|
||||
NanoBanana2TextToImageNumImages::One => 1,
|
||||
NanoBanana2TextToImageNumImages::Two => 2,
|
||||
NanoBanana2TextToImageNumImages::Three => 3,
|
||||
NanoBanana2TextToImageNumImages::Four => 4,
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_num_images(n: NanoBanana2EditImageNumImages) -> u64 {
|
||||
match n {
|
||||
NanoBanana2EditImageNumImages::One => 1,
|
||||
NanoBanana2EditImageNumImages::Two => 2,
|
||||
NanoBanana2EditImageNumImages::Three => 3,
|
||||
NanoBanana2EditImageNumImages::Four => 4,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -61,7 +117,8 @@ mod tests {
|
||||
let state = FalNanoBanana2CostState::from_request(
|
||||
&make_t2i(NanoBanana2TextToImageNumImages::One, None),
|
||||
);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(15));
|
||||
// None defaults to 1K = 8¢ per v1's rate table.
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -69,7 +126,8 @@ mod tests {
|
||||
let state = FalNanoBanana2CostState::from_request(
|
||||
&make_t2i(NanoBanana2TextToImageNumImages::One, Some(NanoBanana2TextToImageResolution::HalfK)),
|
||||
);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(8));
|
||||
// HalfK = 0.75x base = 6¢ per v1's rate table.
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -77,7 +135,7 @@ mod tests {
|
||||
let state = FalNanoBanana2CostState::from_request(
|
||||
&make_t2i(NanoBanana2TextToImageNumImages::Four, Some(NanoBanana2TextToImageResolution::OneK)),
|
||||
);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(60));
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(32));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -85,7 +143,7 @@ mod tests {
|
||||
let state = FalNanoBanana2CostState::from_request(
|
||||
&make_t2i(NanoBanana2TextToImageNumImages::One, Some(NanoBanana2TextToImageResolution::FourK)),
|
||||
);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(30));
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(16));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -93,7 +151,7 @@ mod tests {
|
||||
let state = FalNanoBanana2CostState::from_request(
|
||||
&make_t2i(NanoBanana2TextToImageNumImages::Two, Some(NanoBanana2TextToImageResolution::FourK)),
|
||||
);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(60));
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(32));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +176,7 @@ mod tests {
|
||||
let state = FalNanoBanana2CostState::from_request(
|
||||
&make_edit(NanoBanana2EditImageNumImages::One, None),
|
||||
);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(15));
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -126,7 +184,7 @@ mod tests {
|
||||
let state = FalNanoBanana2CostState::from_request(
|
||||
&make_edit(NanoBanana2EditImageNumImages::One, Some(NanoBanana2EditImageResolution::HalfK)),
|
||||
);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(8));
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -134,7 +192,7 @@ mod tests {
|
||||
let state = FalNanoBanana2CostState::from_request(
|
||||
&make_edit(NanoBanana2EditImageNumImages::Three, Some(NanoBanana2EditImageResolution::TwoK)),
|
||||
);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(45));
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(36));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -142,7 +200,7 @@ mod tests {
|
||||
let state = FalNanoBanana2CostState::from_request(
|
||||
&make_edit(NanoBanana2EditImageNumImages::One, Some(NanoBanana2EditImageResolution::FourK)),
|
||||
);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(30));
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +219,6 @@ mod tests {
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
assert!(!cost.has_watermark);
|
||||
assert!(cost.cost_in_credits.is_none());
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-3
@@ -121,7 +121,10 @@ fn plan_resolution(resolution: Option<CommonResolution>) -> Option<PlannedResolu
|
||||
CommonResolution::HalfK | CommonResolution::FourEightyP | CommonResolution::SevenTwentyP
|
||||
| CommonResolution::OneK | CommonResolution::TenEightyP => PlannedResolution::OneK,
|
||||
CommonResolution::TwoK => PlannedResolution::TwoK,
|
||||
CommonResolution::ThreeK | CommonResolution::FourK => PlannedResolution::FourK,
|
||||
// 3K isn't natively supported on Fal nano_banana_pro; v1 downgrades to 2K
|
||||
// pricing (and TwoK resolution). Match v1 here so cost+behavior agree.
|
||||
CommonResolution::ThreeK => PlannedResolution::TwoK,
|
||||
CommonResolution::FourK => PlannedResolution::FourK,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -393,13 +396,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_k_rounds_up_to_four_k() {
|
||||
fn three_k_rounds_down_to_two_k() {
|
||||
// Match v1's downgrade behavior: 3K → 2K (cheaper, no 3K native support).
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
resolution: Some(CommonResolution::ThreeK),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_t2i(build_fal_nano_banana_pro(builder));
|
||||
assert!(matches!(req.resolution, Some(NanoBananaProTextToImageResolution::FourK)));
|
||||
assert!(matches!(req.resolution, Some(NanoBananaProTextToImageResolution::TwoK)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+3
-2
@@ -18,7 +18,8 @@ impl FalNanoBananaProCostState {
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
// v1 sets cost_in_credits to the same value as USD cents (1:1).
|
||||
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,
|
||||
@@ -145,6 +146,6 @@ mod tests {
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
assert!(!cost.has_watermark);
|
||||
assert!(cost.cost_in_credits.is_none());
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user