From 189c5afc808a3b9e24306058f3d2418b7f212714 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 12 Jun 2026 19:52:58 -0400 Subject: [PATCH] updating pricing tests (#1636) --- .gitignore | 2 + .../providers/artcraft/midjourney_7/cost.rs | 64 +-- .../artcraft/midjourney_7_niji/cost.rs | 52 -- .../providers/artcraft/midjourney_8/cost.rs | 52 -- .../providers/kinovi/midjourney_7/cost.rs | 20 +- .../kinovi/midjourney_7_niji/cost.rs | 8 +- .../providers/kinovi/midjourney_8/cost.rs | 8 +- .../artcraft/grok_imagine_video/cost.rs | 170 ------- .../artcraft/grok_imagine_video_1p5/cost.rs | 179 ------- .../artcraft/happy_horse_1p0/cost.rs | 55 +-- .../providers/artcraft/seedance_2p0/cost.rs | 55 +-- .../artcraft/seedance_2p0_bp/cost.rs | 2 + .../artcraft/seedance_2p0_bp_fast/cost.rs | 2 + .../artcraft/seedance_2p0_bpu/cost.rs | 2 + .../artcraft/seedance_2p0_bpu_fast/cost.rs | 2 + .../artcraft/seedance_2p0_fast/cost.rs | 46 +- .../providers/artcraft/seedance_2p0_u/cost.rs | 2 + .../artcraft/seedance_2p0_u_fast/cost.rs | 2 + .../providers/kinovi/happy_horse_1p0/cost.rs | 16 +- .../providers/kinovi/seedance_2p0/cost.rs | 76 +-- .../kinovi/seedance_2p0_fast/cost.rs | 52 +- .../seedance2pro_client/costs_seedance2.md | 68 +++ .../seedance2pro_client/src/cost/constants.rs | 9 + .../src/cost/kinovi_generation_cost.rs | 90 ++++ .../cost/kinovi_seedance_generation_cost.rs | 111 +++++ .../seedance2pro_client/src/cost/mod.rs | 3 + .../seedance2pro_client/src/generate/cost.rs | 61 --- .../generate/image/generate_midjourney_v7.rs | 14 +- .../image/generate_midjourney_v7_niji.rs | 14 +- .../generate/image/generate_midjourney_v8.rs | 14 +- .../seedance2pro_client/src/generate/mod.rs | 1 - .../video/generate_happy_horse_1p0.rs | 34 +- .../generate/video/generate_seedance_2p0.rs | 452 +++++++++++++----- .../video/generate_seedance_2p0_fast.rs | 350 ++++++++++---- .../seedance2pro_client/src/lib.rs | 1 + .../requests/generate_image/generate_image.rs | 22 +- .../requests/generate_video/generate_video.rs | 36 +- .../workflow_run_task/workflow_run_task.rs | 40 +- 38 files changed, 1103 insertions(+), 1084 deletions(-) create mode 100644 crates/api_clients/seedance2pro_client/costs_seedance2.md create mode 100644 crates/api_clients/seedance2pro_client/src/cost/constants.rs create mode 100644 crates/api_clients/seedance2pro_client/src/cost/kinovi_generation_cost.rs create mode 100644 crates/api_clients/seedance2pro_client/src/cost/kinovi_seedance_generation_cost.rs create mode 100644 crates/api_clients/seedance2pro_client/src/cost/mod.rs delete mode 100644 crates/api_clients/seedance2pro_client/src/generate/cost.rs diff --git a/.gitignore b/.gitignore index 752bc2096f..986d68eaf9 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ script/socat_dev_database_connect.sh venv/ # LLM Testing +/external/ /prompts/ /reports/ /requests/ @@ -102,3 +103,4 @@ windows/sqlite/sqlite3.lib # ArtCraft Windows DLLs crates/desktop/src-tauri/*.dll windows_dlls/ + diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/cost.rs index 5370e732bc..f43dd52966 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/cost.rs @@ -11,9 +11,11 @@ use crate::generate::generate_image::providers::artcraft::midjourney_7::request: /// | 3 | 19 | 19¢ | (interpolated; Kinovi doesn't expose 3) /// | 4 | 25 | 25¢ | /// -/// Kinovi-side math: 12 Kinovi credits per Midjourney task; 193 Kinovi -/// credits per USD. Kinovi rounds to nearest cent, so batches 1/2/4 land -/// on 6/12/25. Batch 3 is `ceil((12 × 3 / 193) × 100) = 19`. +/// Kinovi-side math (historical, 22,000-credit package at 193 credits/$1): +/// 12 Kinovi credits per Midjourney task → batches 1/2/4 land on 6/12/25¢; +/// batch 3 is `ceil((12 × 3 / 193) × 100) = 19`. Under the current +/// 525,000-credit package (243 credits/$1) Kinovi costs only 5/10/20¢ — +/// user pricing is intentionally kept at the 193-derived values. #[derive(Clone, Debug)] pub struct ArtcraftMidjourney7CostState { pub num_images: u16, @@ -127,62 +129,6 @@ mod tests { } } - // ── 1:1 cost parity with Kinovi for batches 1/2/4 ── - // - // Kinovi midjourney_7 returns 6/12/25¢ for batches 1/2/4. The artcraft - // provider must match those values byte-for-byte so users pay the same. - - mod kinovi_parity { - use super::*; - use crate::generate::generate_image::providers::kinovi::midjourney_7::cost::KinoviMidjourney7CostState; - use seedance2pro_client::generate::image::generate_midjourney_v7::KinoviMidjourneyBatchCount; - - #[test] - fn batch_1_matches_kinovi() { - let kinovi = KinoviMidjourney7CostState { batch_count: KinoviMidjourneyBatchCount::One } - .estimate_cost(); - // NB: The kinovi provider estimate rounds fractional cents UP - // (KinoviGenerationCost) while this artcraft user price rounds to - // nearest — kinovi may exceed it by at most one cent. - let artcraft_cents = cost_cents(1); - let kinovi_cents = kinovi.cost_in_usd_cents.unwrap(); - assert!( - kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1, - "batch 1: artcraft={artcraft_cents}, kinovi={kinovi_cents}", - ); - } - - #[test] - fn batch_2_matches_kinovi() { - let kinovi = KinoviMidjourney7CostState { batch_count: KinoviMidjourneyBatchCount::Two } - .estimate_cost(); - // NB: The kinovi provider estimate rounds fractional cents UP - // (KinoviGenerationCost) while this artcraft user price rounds to - // nearest — kinovi may exceed it by at most one cent. - let artcraft_cents = cost_cents(2); - let kinovi_cents = kinovi.cost_in_usd_cents.unwrap(); - assert!( - kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1, - "batch 2: artcraft={artcraft_cents}, kinovi={kinovi_cents}", - ); - } - - #[test] - fn batch_4_matches_kinovi() { - let kinovi = KinoviMidjourney7CostState { batch_count: KinoviMidjourneyBatchCount::Four } - .estimate_cost(); - // NB: The kinovi provider estimate rounds fractional cents UP - // (KinoviGenerationCost) while this artcraft user price rounds to - // nearest — kinovi may exceed it by at most one cent. - let artcraft_cents = cost_cents(4); - let kinovi_cents = kinovi.cost_in_usd_cents.unwrap(); - assert!( - kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1, - "batch 4: artcraft={artcraft_cents}, kinovi={kinovi_cents}", - ); - } - } - // ── Monotonicity ── #[test] diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/cost.rs index 3694adf8c9..19dfcffe72 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/cost.rs @@ -82,56 +82,4 @@ mod tests { } } - // ── 1:1 cost parity with Kinovi 7-niji ── - - mod kinovi_parity { - use super::*; - use crate::generate::generate_image::providers::kinovi::midjourney_7_niji::cost::KinoviMidjourney7NijiCostState; - use seedance2pro_client::generate::image::generate_midjourney_v7_niji::KinoviMidjourneyBatchCount; - - #[test] - fn batch_1_matches_kinovi() { - let kinovi = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::One } - .estimate_cost(); - // NB: The kinovi provider estimate rounds fractional cents UP - // (KinoviGenerationCost) while this artcraft user price rounds to - // nearest — kinovi may exceed it by at most one cent. - let artcraft_cents = cost_cents(1); - let kinovi_cents = kinovi.cost_in_usd_cents.unwrap(); - assert!( - kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1, - "batch 1: artcraft={artcraft_cents}, kinovi={kinovi_cents}", - ); - } - - #[test] - fn batch_2_matches_kinovi() { - let kinovi = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::Two } - .estimate_cost(); - // NB: The kinovi provider estimate rounds fractional cents UP - // (KinoviGenerationCost) while this artcraft user price rounds to - // nearest — kinovi may exceed it by at most one cent. - let artcraft_cents = cost_cents(2); - let kinovi_cents = kinovi.cost_in_usd_cents.unwrap(); - assert!( - kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1, - "batch 2: artcraft={artcraft_cents}, kinovi={kinovi_cents}", - ); - } - - #[test] - fn batch_4_matches_kinovi() { - let kinovi = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::Four } - .estimate_cost(); - // NB: The kinovi provider estimate rounds fractional cents UP - // (KinoviGenerationCost) while this artcraft user price rounds to - // nearest — kinovi may exceed it by at most one cent. - let artcraft_cents = cost_cents(4); - let kinovi_cents = kinovi.cost_in_usd_cents.unwrap(); - assert!( - kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1, - "batch 4: artcraft={artcraft_cents}, kinovi={kinovi_cents}", - ); - } - } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/cost.rs index 59da779597..09cbaadbc8 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/cost.rs @@ -82,56 +82,4 @@ mod tests { } } - // ── 1:1 cost parity with Kinovi v8 ── - - mod kinovi_parity { - use super::*; - use crate::generate::generate_image::providers::kinovi::midjourney_8::cost::KinoviMidjourney8CostState; - use seedance2pro_client::generate::image::generate_midjourney_v8::KinoviMidjourneyBatchCount; - - #[test] - fn batch_1_matches_kinovi() { - let kinovi = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::One } - .estimate_cost(); - // NB: The kinovi provider estimate rounds fractional cents UP - // (KinoviGenerationCost) while this artcraft user price rounds to - // nearest — kinovi may exceed it by at most one cent. - let artcraft_cents = cost_cents(1); - let kinovi_cents = kinovi.cost_in_usd_cents.unwrap(); - assert!( - kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1, - "batch 1: artcraft={artcraft_cents}, kinovi={kinovi_cents}", - ); - } - - #[test] - fn batch_2_matches_kinovi() { - let kinovi = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::Two } - .estimate_cost(); - // NB: The kinovi provider estimate rounds fractional cents UP - // (KinoviGenerationCost) while this artcraft user price rounds to - // nearest — kinovi may exceed it by at most one cent. - let artcraft_cents = cost_cents(2); - let kinovi_cents = kinovi.cost_in_usd_cents.unwrap(); - assert!( - kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1, - "batch 2: artcraft={artcraft_cents}, kinovi={kinovi_cents}", - ); - } - - #[test] - fn batch_4_matches_kinovi() { - let kinovi = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::Four } - .estimate_cost(); - // NB: The kinovi provider estimate rounds fractional cents UP - // (KinoviGenerationCost) while this artcraft user price rounds to - // nearest — kinovi may exceed it by at most one cent. - let artcraft_cents = cost_cents(4); - let kinovi_cents = kinovi.cost_in_usd_cents.unwrap(); - assert!( - kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1, - "batch 4: artcraft={artcraft_cents}, kinovi={kinovi_cents}", - ); - } - } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/kinovi/midjourney_7/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/kinovi/midjourney_7/cost.rs index 0b49f22f48..fbc84f7f91 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/kinovi/midjourney_7/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/kinovi/midjourney_7/cost.rs @@ -86,27 +86,27 @@ mod tests { } } - // ── USD pricing (193 credits per dollar; rounded) ── + // ── USD pricing (243 credits per dollar; rounded up) ── mod usd { use super::*; #[test] - fn batch_one_is_six_cents() { - // 12 / 193 × 100 = 6.21 → 6¢ - assert_eq!(usd_cents_for(KinoviMidjourneyBatchCount::One), 7); // rounds UP + fn batch_one_is_five_cents() { + // 12/243 × 100 = 4.94 → 5¢ + assert_eq!(usd_cents_for(KinoviMidjourneyBatchCount::One), 5); // rounds UP } #[test] - fn batch_two_is_twelve_cents() { - // 24 / 193 × 100 = 12.44 → 12¢ - assert_eq!(usd_cents_for(KinoviMidjourneyBatchCount::Two), 13); // rounds UP + fn batch_two_is_ten_cents() { + // 24/243 × 100 = 9.88 → 10¢ + assert_eq!(usd_cents_for(KinoviMidjourneyBatchCount::Two), 10); // rounds UP } #[test] - fn batch_four_is_twentyfive_cents() { - // 48 / 193 × 100 = 24.87 → 25¢ - assert_eq!(usd_cents_for(KinoviMidjourneyBatchCount::Four), 25); + fn batch_four_is_twenty_cents() { + // 48/243 × 100 = 19.75 → 20¢ + assert_eq!(usd_cents_for(KinoviMidjourneyBatchCount::Four), 20); } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/kinovi/midjourney_7_niji/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/kinovi/midjourney_7_niji/cost.rs index bba08579ef..4aff6fe9bd 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/kinovi/midjourney_7_niji/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/kinovi/midjourney_7_niji/cost.rs @@ -56,11 +56,11 @@ mod tests { use super::*; #[test] - fn batch_one_is_twelve_credits_and_six_cents() { + fn batch_one_is_twelve_credits_and_five_cents() { let cost = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::One } .estimate_cost(); assert_eq!(cost.cost_in_credits, Some(12)); - assert_eq!(cost.cost_in_usd_cents, Some(7)); // 1200/193 = 6.22 -> rounds UP + assert_eq!(cost.cost_in_usd_cents, Some(5)); // 1200/243 = 4.94 -> rounds UP } #[test] @@ -68,7 +68,7 @@ mod tests { let cost = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::Two } .estimate_cost(); assert_eq!(cost.cost_in_credits, Some(24)); - assert_eq!(cost.cost_in_usd_cents, Some(13)); // 2400/193 = 12.44 -> rounds UP + assert_eq!(cost.cost_in_usd_cents, Some(10)); // 2400/243 = 9.88 -> rounds UP } #[test] @@ -76,7 +76,7 @@ mod tests { let cost = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::Four } .estimate_cost(); assert_eq!(cost.cost_in_credits, Some(48)); - assert_eq!(cost.cost_in_usd_cents, Some(25)); + assert_eq!(cost.cost_in_usd_cents, Some(20)); } #[test] diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/kinovi/midjourney_8/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/kinovi/midjourney_8/cost.rs index de40898354..58dae7eb8a 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/kinovi/midjourney_8/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/kinovi/midjourney_8/cost.rs @@ -56,11 +56,11 @@ mod tests { use super::*; #[test] - fn batch_one_is_twelve_credits_and_six_cents() { + fn batch_one_is_twelve_credits_and_five_cents() { let cost = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::One } .estimate_cost(); assert_eq!(cost.cost_in_credits, Some(12)); - assert_eq!(cost.cost_in_usd_cents, Some(7)); // 1200/193 = 6.22 -> rounds UP + assert_eq!(cost.cost_in_usd_cents, Some(5)); // 1200/243 = 4.94 -> rounds UP } #[test] @@ -68,7 +68,7 @@ mod tests { let cost = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::Two } .estimate_cost(); assert_eq!(cost.cost_in_credits, Some(24)); - assert_eq!(cost.cost_in_usd_cents, Some(13)); // 2400/193 = 12.44 -> rounds UP + assert_eq!(cost.cost_in_usd_cents, Some(10)); // 2400/243 = 9.88 -> rounds UP } #[test] @@ -76,6 +76,6 @@ mod tests { let cost = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::Four } .estimate_cost(); assert_eq!(cost.cost_in_credits, Some(48)); - assert_eq!(cost.cost_in_usd_cents, Some(25)); + assert_eq!(cost.cost_in_usd_cents, Some(20)); } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/grok_imagine_video/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/grok_imagine_video/cost.rs index 722485b5fd..273cb77f58 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/grok_imagine_video/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/grok_imagine_video/cost.rs @@ -231,176 +231,6 @@ mod tests { } } - // ── Cross-provider margin: ArtCraft is consistently ~30% above GrokApi ── - - mod small_margin { - use super::*; - use crate::api::router_aspect_ratio::RouterAspectRatio; - - #[test] - fn small_margin() { - let resolutions = [ - None, - Some(RouterResolution::FourEightyP), - Some(RouterResolution::SevenTwentyP), - Some(RouterResolution::TenEightyP), // clamps to 720p on both sides - ]; - let durations: &[u16] = &[1, 4, 5, 8, 10, 12, 15]; - let aspect_ratios = [ - None, - Some(RouterAspectRatio::Auto), - Some(RouterAspectRatio::Square), - Some(RouterAspectRatio::WideSixteenByNine), - Some(RouterAspectRatio::TallNineBySixteen), - Some(RouterAspectRatio::WideFourByThree), - Some(RouterAspectRatio::TallThreeByFour), - Some(RouterAspectRatio::WideThreeByTwo), - Some(RouterAspectRatio::TallTwoByThree), - ]; - // (has_start_frame, num_reference_images). At most one of the two is - // non-default — start_frame and reference_images are mutually exclusive - // on the GrokApi wire (image-to-video vs reference-to-video), so we - // don't mix them here to keep the comparison apples-to-apples. - let image_combos: &[(bool, usize)] = &[ - (false, 0), // text-to-video - (true, 0), // image-to-video, 1 source image - (false, 1), // reference-to-video, 1 ref image - (false, 2), - (false, 3), - ]; - - let mut total_cases = 0; - let mut failures = Vec::new(); - - for &res in &resolutions { - for &dur in durations { - for &ar in &aspect_ratios { - for &(has_start, num_refs) in image_combos { - total_cases += 1; - let artcraft = artcraft_cents(res, dur, ar, has_start, num_refs); - let grokapi = grokapi_cents(res, dur, ar, has_start, num_refs); - - // Margin requirement: artcraft / max(grokapi - 1, 1) >= 1.29. - // The (-1, 1) floor accounts for one cent of GrokApi-side - // ceiling-rounding noise (see test doc comment above). - let grokapi_compare = grokapi.saturating_sub(1).max(1); - let lhs = artcraft.saturating_mul(100); - let rhs = grokapi_compare.saturating_mul(129); - - if lhs < rhs { - let pct = if grokapi == 0 { 0.0 } - else { (artcraft as f64 / grokapi as f64 - 1.0) * 100.0 }; - failures.push(format!( - "res={:?} dur={}s ar={:?} start={} refs={} → \ - artcraft={}¢ vs grokapi={}¢ (margin {:.1}%, want ≥ 29% after 1¢ tolerance)", - res, dur, ar, has_start, num_refs, artcraft, grokapi, pct, - )); - } - - // Defense in depth: ArtCraft must always cost at least as - // much as GrokApi, regardless of rounding noise. - assert!( - artcraft >= grokapi, - "ArtCraft ({artcraft}¢) cheaper than GrokApi ({grokapi}¢) for \ - res={res:?} dur={dur}s ar={ar:?} start={has_start} refs={num_refs}", - ); - } - } - } - } - - if !failures.is_empty() { - let summary = failures.iter().take(10).cloned().collect::>().join("\n "); - panic!( - "{} / {} (res × duration × aspect × image_combo) tuples failed \ - the ≥29% (with 1¢ tolerance) margin check.\nFirst 10:\n {}", - failures.len(), total_cases, summary, - ); - } - } - - // ── Cross-provider helpers ── - - fn artcraft_cents( - resolution: Option, - duration_seconds: u16, - aspect_ratio: Option, - has_start_frame: bool, - num_reference_images: usize, - ) -> u64 { - let start_frame = if has_start_frame { - // ArtCraft's omni endpoint takes MediaFileTokens for image refs. - Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_start".to_string()))) - } else { None }; - let reference_images = if num_reference_images > 0 { - Some(ImageListRef::MediaFileTokens( - (0..num_reference_images) - .map(|i| MediaFileToken::new(format!("mf_ref_{i}"))) - .collect(), - )) - } else { None }; - - let builder = GenerateVideoRequestBuilder { - model: RouterVideoModel::GrokImagineVideo, - provider: RouterProvider::Artcraft, - resolution, - aspect_ratio, - duration_seconds: Some(duration_seconds), - // Batch is fixed at 1 because GrokApi's per-request cost calculator - // doesn't multiply by batch (Grok returns one video per call); the - // markup test would compare apples to oranges otherwise. - video_batch_count: Some(1), - start_frame, - reference_images, - ..Default::default() - }; - builder.build2() - .expect("build2 (artcraft) should succeed") - .estimate_cost() - .expect("estimate_cost (artcraft) should succeed") - .cost_in_usd_cents - .unwrap() - } - - fn grokapi_cents( - resolution: Option, - duration_seconds: u16, - aspect_ratio: Option, - has_start_frame: bool, - num_reference_images: usize, - ) -> u64 { - let start_frame = if has_start_frame { - // GrokApi (direct xAI) takes URLs for image refs. - Some(ImageRef::Url("https://example.com/start.png".to_string())) - } else { None }; - let reference_images = if num_reference_images > 0 { - Some(ImageListRef::Urls( - (0..num_reference_images) - .map(|i| format!("https://example.com/ref_{i}.png")) - .collect(), - )) - } else { None }; - - let builder = GenerateVideoRequestBuilder { - model: RouterVideoModel::GrokImagineVideo, - provider: RouterProvider::GrokApi, - resolution, - aspect_ratio, - duration_seconds: Some(duration_seconds), - video_batch_count: Some(1), - start_frame, - reference_images, - ..Default::default() - }; - builder.build2() - .expect("build2 (grokapi) should succeed") - .estimate_cost() - .expect("estimate_cost (grokapi) should succeed") - .cost_in_usd_cents - .unwrap() - } - } - // ── Helpers ── fn build_cost( diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/grok_imagine_video_1p5/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/grok_imagine_video_1p5/cost.rs index f897408f07..327de87ed2 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/grok_imagine_video_1p5/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/grok_imagine_video_1p5/cost.rs @@ -144,113 +144,6 @@ mod tests { use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; - // ── Base cost parity with grok_api_client ───────────────────────────── - // - // The base cost (before ArtCraft markup, batch=1) must equal exactly what - // grok_api_client's calculator returns for an equivalent direct-Grok - // request. This protects against the artcraft and grok-api implementations - // silently drifting apart. - - mod grok_api_parity { - use super::*; - - // (resolution, duration, image_count) - // - // Durations are confined to [4, 15] because the ArtCraft pipeline clamps - // outside that range (see `plan_duration` in artcraft/build_common.rs). - // Every case carries exactly one image. v1.5 rejects text-to-video, AND - // the GrokApi build step collapses multi-image reference-to-video - // requests down to one `image` (promote-first; see grok_api side - // build.rs). So the only request shape where both sides agree on the - // input image count is the single-image case. - const PARITY_CASES: &[(Option, u16, u64)] = &[ - // 480p variations - (Some(RouterResolution::FourEightyP), 4, 1), - (Some(RouterResolution::FourEightyP), 5, 1), - (Some(RouterResolution::FourEightyP), 8, 1), - (Some(RouterResolution::FourEightyP), 15, 1), - // 720p variations - (Some(RouterResolution::SevenTwentyP), 4, 1), - (Some(RouterResolution::SevenTwentyP), 5, 1), - (Some(RouterResolution::SevenTwentyP), 8, 1), - (Some(RouterResolution::SevenTwentyP), 15, 1), - // None defaults to 720p in our `from_request`, which matches the - // assumption Grok's calculator makes when fed a 720p request. - (None, 5, 1), - ]; - - #[test] - fn base_cost_for_one_video_matches_grok_api_client() { - for &(router_res, duration, image_count) in PARITY_CASES { - let artcraft = artcraft_state(router_res, duration, image_count, 1); - let grok_cents = grok_cents(router_res, duration, image_count); - - assert_eq!( - artcraft.base_cost_in_cents_for_one_video(), - grok_cents, - "parity mismatch at res={router_res:?} dur={duration} images={image_count}: \ - artcraft_base={} grok={}", - artcraft.base_cost_in_cents_for_one_video(), - grok_cents, - ); - } - } - - #[test] - fn batch_one_base_total_equals_one_video_base() { - let s = artcraft_state(Some(RouterResolution::SevenTwentyP), 5, 1, 1); - assert_eq!(s.base_cost_in_cents_for_batch(), s.base_cost_in_cents_for_one_video()); - } - - #[test] - fn batch_scales_base_linearly() { - // The ArtCraft pipeline only accepts batch counts of 1, 2, or 4 (see - // `plan_batch_count` in artcraft/build_common.rs); anything else gets - // clamped, so we can only meaningfully test those values. - let per_one = artcraft_state(Some(RouterResolution::SevenTwentyP), 5, 1, 1) - .base_cost_in_cents_for_one_video(); - for batch in [1u16, 2, 4] { - let s = artcraft_state(Some(RouterResolution::SevenTwentyP), 5, 1, batch); - assert_eq!( - s.base_cost_in_cents_for_batch(), - per_one * batch as u64, - "batch={batch}", - ); - } - } - - /// Builder-driven parity — exercises the full pipeline (build2 → - /// estimate_cost) on both providers and compares ArtCraft's base cost - /// (final / 1.05, accounting for ceiling) against GrokApi's direct - /// cost. - /// - /// We verify the markup rather than the base here because the public - /// `estimate_cost()` returns the marked-up number; the explicit - /// `base_cost_in_cents_*` API is tested above. - #[test] - fn end_to_end_markup_is_at_least_five_percent_over_grok() { - // Single-image cases only — see PARITY_CASES comment above for why - // multi-image rows can't be apples-to-apples between the two sides. - let cases: &[(Option, u16, bool, usize)] = &[ - (Some(RouterResolution::FourEightyP), 5, true, 0), - (Some(RouterResolution::FourEightyP), 10, true, 0), - (Some(RouterResolution::SevenTwentyP), 5, true, 0), - (Some(RouterResolution::SevenTwentyP), 8, true, 0), - (Some(RouterResolution::SevenTwentyP),15, true, 0), - ]; - for &(res, duration, has_start, refs) in cases { - let artcraft_cents = artcraft_end_to_end_cents(res, duration, 1, has_start, refs); - let grok_cents = grok_cents(res, duration, has_start as u64 + refs as u64); - let want_minimum = grok_cents.saturating_mul(MARKUP_NUMERATOR).div_ceil(MARKUP_DENOMINATOR); - assert_eq!( - artcraft_cents, want_minimum, - "res={res:?} dur={duration} start={has_start} refs={refs}: \ - artcraft={artcraft_cents}¢ vs ceil(grok({grok_cents})×1.05)={want_minimum}¢", - ); - } - } - } - // ── Markup correctness ───────────────────────────────────────────────── mod markup { @@ -484,42 +377,6 @@ mod tests { } } - fn artcraft_end_to_end_cents( - resolution: Option, - duration_seconds: u16, - batch_count: u16, - has_start_frame: bool, - num_reference_images: usize, - ) -> u64 { - let start_frame = if has_start_frame { - Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_start".to_string()))) - } else { None }; - let reference_images = if num_reference_images > 0 { - Some(ImageListRef::MediaFileTokens( - (0..num_reference_images) - .map(|i| MediaFileToken::new(format!("mf_ref_{i}"))) - .collect(), - )) - } else { None }; - - let builder = GenerateVideoRequestBuilder { - model: RouterVideoModel::GrokImagineVideo1p5, - provider: RouterProvider::Artcraft, - resolution, - duration_seconds: Some(duration_seconds), - video_batch_count: Some(batch_count), - start_frame, - reference_images, - ..Default::default() - }; - builder.build2() - .expect("build2 should succeed") - .estimate_cost() - .expect("estimate_cost should succeed") - .cost_in_usd_cents - .unwrap() - } - fn build_cost_via_builder( resolution: Option, duration_seconds: u16, @@ -542,40 +399,4 @@ mod tests { .expect("estimate_cost should succeed") } - /// Reference value: what GrokApi's own pipeline would charge for one - /// equivalent video. Used by parity tests. - /// - /// The Grok build step prefers `start_frame` over `reference_images` when - /// both are supplied and drops the latter (see grok_api/grok_imagine_video_1p5/build.rs). - /// To keep parity with the ArtCraft input-image *count*, we put all source - /// images in `reference_images` only — the cost calculator counts the total - /// either way. - fn grok_cents( - resolution: Option, - duration_seconds: u16, - image_count: u64, - ) -> u64 { - let reference_images = if image_count >= 1 { - Some(ImageListRef::Urls( - (0..image_count).map(|i| format!("https://example.com/ref_{i}.png")).collect(), - )) - } else { None }; - - let builder = GenerateVideoRequestBuilder { - model: RouterVideoModel::GrokImagineVideo1p5, - provider: RouterProvider::GrokApi, - resolution, - duration_seconds: Some(duration_seconds), - video_batch_count: Some(1), - start_frame: None, - reference_images, - ..Default::default() - }; - builder.build2() - .expect("build2 (grokapi) should succeed") - .estimate_cost() - .expect("estimate_cost (grokapi) should succeed") - .cost_in_usd_cents - .unwrap() - } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/happy_horse_1p0/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/happy_horse_1p0/cost.rs index 32c8c8dec4..548825f91f 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/happy_horse_1p0/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/happy_horse_1p0/cost.rs @@ -14,13 +14,16 @@ use crate::generate::generate_video::providers::artcraft::happy_horse_1p0::reque // // Default resolution (None) is 720p. // Batch count multiplies the total cost. -// Credit package: 22,000 credits for $114 (~192.98 credits/$1, rounded to 193). +// Credit package (historical): 22,000 credits for $114 (~193 credits/$1). +// The upstream package is now 525,000 credits for $2,159.0909 (~243 credits/$1), +// but user pricing is intentionally kept at the 193-derived rates. // // ArtCraft credits: 100 credits = $1.00, so ArtCraft credits = USD cents. // We compute USD cents from upstream credits ÷ credits_per_dollar × 100. const UPSTREAM_CREDITS_PER_SECOND_720P: u32 = 33; const UPSTREAM_CREDITS_PER_SECOND_1080P: u32 = 66; +// Frozen at the historical rate so user pricing doesn't shift with the package deal. const UPSTREAM_CREDITS_PER_DOLLAR: f64 = 193.0; pub struct ArtcraftHappyHorse1p0CostState { @@ -201,56 +204,6 @@ mod tests { } } - // ── Cross-check with Kinovi ── - - mod cross_check_with_kinovi { - use super::*; - - #[test] - fn artcraft_matches_kinovi_all_combos() { - let resolutions = [ - Some(RouterResolution::SevenTwentyP), - None, - Some(RouterResolution::TenEightyP), - ]; - let durations: [u16; 4] = [4, 5, 10, 15]; - let batches: [u16; 3] = [1, 2, 4]; - - for res in &resolutions { - for dur in &durations { - for batch in &batches { - let artcraft_cost = build_cost(*res, *dur, *batch); - - let kinovi = GenerateVideoRequestBuilder { - model: RouterVideoModel::HappyHorse1p0, - provider: RouterProvider::Seedance2Pro, - resolution: *res, - duration_seconds: Some(*dur), - video_batch_count: Some(*batch), - ..Default::default() - }; - let kinovi_cost = kinovi.build2() - .expect("kinovi build2") - .estimate_cost() - .expect("kinovi estimate_cost"); - - // NB: The kinovi provider estimate rounds fractional cents UP - // (KinoviGenerationCost), while the artcraft user-facing price - // here rounds to nearest — so kinovi may exceed artcraft by at - // most one cent. - let artcraft_cents = artcraft_cost.cost_in_usd_cents.expect("artcraft cents"); - let kinovi_cents = kinovi_cost.cost_in_usd_cents.expect("kinovi cents"); - assert!( - kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1, - "USD cents mismatch: res={:?} dur={}s batch={} (artcraft={}, kinovi={})", - res, dur, batch, artcraft_cents, kinovi_cents, - ); - } - } - } - } - } - // ── Helpers ── fn build_cost( diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0/cost.rs index 892e35483e..7b4585955f 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0/cost.rs @@ -18,6 +18,10 @@ use crate::generate::generate_video::providers::artcraft::seedance_2p0::request: /// /// We keep these as f64 because per-second rates are fractional; rounding /// happens once at the end after multiplying by duration × batch. +/// +/// NB: Derivation is historical (193 credits/$1 package). The upstream +/// package is now 525,000 credits for $2,159.0909 (~243 credits/$1), but +/// user pricing is intentionally kept at these 193-derived rates. const CENTS_PER_SECOND_480P: f64 = 7.772; const CENTS_PER_SECOND_720P: f64 = 16.0; const CENTS_PER_SECOND_1080P: f64 = 46.632; @@ -214,57 +218,6 @@ mod tests { } } - // -- Cross-check with Kinovi via builder -- - - mod cross_check_with_kinovi { - use super::*; - - #[test] - fn artcraft_matches_kinovi_all_combos() { - let resolutions = [ - Some(RouterResolution::FourEightyP), - Some(RouterResolution::TenEightyP), - // NB: These are underpriced (for now) - //Some(RouterResolution::SevenTwentyP), - //None, - ]; - let durations: [u16; 4] = [4, 5, 10, 15]; - let batches: [u16; 3] = [1, 2, 4]; - - for res in &resolutions { - for dur in &durations { - for batch in &batches { - let artcraft_cost = build_cost(*res, *dur, *batch); - - let kinovi = GenerateVideoRequestBuilder { - provider: RouterProvider::Seedance2Pro, - resolution: *res, - duration_seconds: Some(*dur), - video_batch_count: Some(*batch), - ..Default::default() - }; - let kinovi_cost = kinovi.build2() - .expect("kinovi build2") - .estimate_cost() - .expect("kinovi estimate_cost"); - - // NB: The kinovi provider estimate rounds fractional cents UP - // (KinoviGenerationCost), while the artcraft user-facing price - // here rounds to nearest — so kinovi may exceed artcraft by at - // most one cent. - let artcraft_cents = artcraft_cost.cost_in_usd_cents.expect("artcraft cents"); - let kinovi_cents = kinovi_cost.cost_in_usd_cents.expect("kinovi cents"); - assert!( - kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1, - "USD cents mismatch: res={:?} dur={}s batch={} (artcraft={}, kinovi={})", - res, dur, batch, artcraft_cents, kinovi_cents, - ); - } - } - } - } - } - // -- Helpers -- /// Build a cost estimate end-to-end: GenerateVideoRequestBuilder → build2 → estimate_cost. diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bp/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bp/cost.rs index b10f8358af..f02b7a85fc 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bp/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bp/cost.rs @@ -154,6 +154,8 @@ mod tests { } } + // -- Helpers -- + fn build_cost( resolution: Option, duration_seconds: u16, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bp_fast/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bp_fast/cost.rs index 53f12c6bf4..3fc8452ffc 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bp_fast/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bp_fast/cost.rs @@ -125,6 +125,8 @@ mod tests { } } + // -- Helpers -- + fn build_cost( resolution: Option, duration_seconds: u16, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bpu/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bpu/cost.rs index 6965815b17..d1818fc540 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bpu/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bpu/cost.rs @@ -154,6 +154,8 @@ mod tests { } } + // -- Helpers -- + fn build_cost( resolution: Option, duration_seconds: u16, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bpu_fast/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bpu_fast/cost.rs index 6242e50ca2..8bc8c92729 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bpu_fast/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_bpu_fast/cost.rs @@ -125,6 +125,8 @@ mod tests { } } + // -- Helpers -- + fn build_cost( resolution: Option, duration_seconds: u16, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_fast/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_fast/cost.rs index bb37e1ccbb..f21f73f423 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_fast/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_fast/cost.rs @@ -13,6 +13,8 @@ use crate::generate::generate_video::providers::artcraft::seedance_2p0_fast::req /// USD cents per second by resolution, derived from upstream Fast rates: /// 480p: 10 upstream-credits/sec / 193 upstream-credits/$1 * 100 ~= 5.181 c/s +/// (historical 193 credits/$1 derivation; the upstream package is now +/// ~243 credits/$1 but user pricing is intentionally unchanged) /// 720p: 28 upstream-credits/sec / 220 upstream-credits/$1 * 100 ~= 12.727 c/s /// /// We keep these as f64 because per-second rates are fractional; rounding @@ -184,50 +186,6 @@ mod tests { } } - // -- Cross-check with Kinovi Fast via builder -- - - //mod cross_check_with_kinovi { - // use super::*; - // - // #[test] - // fn artcraft_fast_matches_kinovi_fast_all_combos() { - // let resolutions = [ - // Some(RouterResolution::FourEightyP), - // Some(RouterResolution::SevenTwentyP), - // None, - // ]; - // let durations: [u16; 4] = [4, 5, 10, 15]; - // let batches: [u16; 3] = [1, 2, 4]; - // - // for res in &resolutions { - // for dur in &durations { - // for batch in &batches { - // let artcraft_cost = build_cost(*res, *dur, *batch); - // - // let kinovi = GenerateVideoRequestBuilder { - // model: RouterVideoModel::Seedance2p0Fast, - // provider: RouterProvider::Seedance2Pro, - // resolution: *res, - // duration_seconds: Some(*dur), - // video_batch_count: Some(*batch), - // ..Default::default() - // }; - // let kinovi_cost = kinovi.build2() - // .expect("kinovi build2") - // .estimate_cost() - // .expect("kinovi estimate_cost"); - // - // assert_eq!( - // artcraft_cost.cost_in_usd_cents, kinovi_cost.cost_in_usd_cents, - // "USD cents mismatch: res={:?} dur={}s batch={}", - // res, dur, batch, - // ); - // } - // } - // } - // } - //} - // -- Helpers -- fn build_cost( diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_u/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_u/cost.rs index d216150245..cd47b05d9d 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_u/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_u/cost.rs @@ -159,6 +159,8 @@ mod tests { } } + // -- Helpers -- + fn build_cost( resolution: Option, duration_seconds: u16, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_u_fast/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_u_fast/cost.rs index ffd65d6aae..4352468c9c 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_u_fast/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_2p0_u_fast/cost.rs @@ -144,6 +144,8 @@ mod tests { } } + // -- Helpers -- + fn build_cost( resolution: Option, duration_seconds: u16, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/happy_horse_1p0/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/happy_horse_1p0/cost.rs index eae84f559c..649ede0160 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/happy_horse_1p0/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/happy_horse_1p0/cost.rs @@ -98,10 +98,8 @@ mod tests { #[test] fn usd_cents_720p() { - // 165 credits / 193 * 100 = 85.49 → 85¢ - assert_eq!(usd_cents(None, 5, None), 86); // 16500/193 = 85.49 -> rounds UP - // 495 credits / 193 * 100 = 256.48 → 256¢ - assert_eq!(usd_cents(None, 15, None), 257); // 49500/193 = 256.48 -> rounds UP + assert_eq!(usd_cents(None, 5, None), 68); // 16500/243 = 67.90 -> rounds UP + assert_eq!(usd_cents(None, 15, None), 204); // 49500/243 = 203.70 -> rounds UP } } @@ -131,10 +129,10 @@ mod tests { #[test] fn usd_cents_1080p() { let r = Some(KinoviHappyHorse1p0OutputResolution::TenEightyP); - // 330 credits / 193 * 100 = 170.98 → 171¢ - assert_eq!(usd_cents(r, 5, None), 171); - // 990 credits / 193 * 100 = 512.95 → 513¢ - assert_eq!(usd_cents(r, 15, None), 513); + // 330 credits; 33000/243 = 135.80 → rounds UP to 136¢ + assert_eq!(usd_cents(r, 5, None), 136); + // 990 credits; 99000/243 = 407.41 → rounds UP to 408¢ + assert_eq!(usd_cents(r, 15, None), 408); } } @@ -238,7 +236,7 @@ mod tests { ); let cost = KinoviHappyHorse1p0CostState::from_request(&req); assert_eq!(cost.estimate_cost().cost_in_credits, Some(330)); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(171)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(136)); } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0/cost.rs index 9549b89d03..94e66049dd 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0/cost.rs @@ -66,8 +66,8 @@ impl KinoviSeedance2p0CostState { }; let costs = pricing_request.calculate_costs(); - let cost_in_credits = costs.kinovi_credits; - let cost_in_usd_cents = costs.usd_cents_rounded_up; + let cost_in_credits = costs.total_cost.kinovi_credits; + let cost_in_usd_cents = costs.total_cost.usd_cents_rounded_up; VideoGenerationCostEstimate { cost_in_credits: Some(cost_in_credits), @@ -103,26 +103,26 @@ mod tests { #[test] fn cost_720p_batch_1() { - assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One), 83); - assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 104); - assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 6, KinoviBatchCount::One), 125); - assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 7, KinoviBatchCount::One), 146); - assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One), 208); - assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One), 311); + assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One), 66); + assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 83); + assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 6, KinoviBatchCount::One), 99); + assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 7, KinoviBatchCount::One), 116); + assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One), 165); + assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One), 247); } #[test] fn cost_720p_batch_2() { - assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::Two), 166); - assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two), 208); - assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::Two), 622); + assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::Two), 132); + assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two), 165); + assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::Two), 494); } #[test] fn cost_720p_batch_4() { - assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::Four), 332); - assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Four), 415); - assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::Four), 1244); + assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::Four), 264); + assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Four), 330); + assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::Four), 988); } } @@ -133,20 +133,20 @@ mod tests { #[test] fn cost_480p_batch_1() { - assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One), 32); - assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 39); - assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One), 78); - assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One), 117); + assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One), 25); + assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 31); + assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One), 62); + assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One), 93); } #[test] fn cost_480p_batch_2() { - assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Two), 78); + assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Two), 62); } #[test] fn cost_480p_batch_4() { - assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 156); + assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 124); } } @@ -157,20 +157,20 @@ mod tests { #[test] fn cost_1080p_batch_1() { - assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 4, KinoviBatchCount::One), 187); - assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One), 234); - assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 10, KinoviBatchCount::One), 467); - assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 15, KinoviBatchCount::One), 700); + assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 4, KinoviBatchCount::One), 149); + assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One), 186); + assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 10, KinoviBatchCount::One), 371); + assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 15, KinoviBatchCount::One), 556); } #[test] fn cost_1080p_batch_2() { - assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Two), 467); + assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Two), 371); } #[test] fn cost_1080p_batch_4() { - assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Four), 933); + assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Four), 741); } } @@ -219,10 +219,10 @@ mod tests { }; let without = base.estimate_cost(); let with = KinoviSeedance2p0CostState { has_video_reference: true, ..base }.estimate_cost(); - // 720p surcharge is +8 credits/s: 200 -> 240 credits (24000/193 = 124.35 -> 125 cents). + // 720p surcharge is +8 credits/s: 200 -> 240 credits (24000/243 = 98.77 -> 99 cents). assert_eq!(without.cost_in_credits, Some(200)); assert_eq!(with.cost_in_credits, Some(240)); - assert_eq!(with.cost_in_usd_cents, Some(125)); + assert_eq!(with.cost_in_usd_cents, Some(99)); } // ── from_request() tests ── @@ -238,7 +238,7 @@ mod tests { assert_eq!(cost.duration_seconds, 5); assert!(matches!(cost.batch_count, Some(KinoviBatchCount::One))); assert!(!cost.has_video_reference); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(104)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(83)); } #[test] @@ -246,21 +246,21 @@ mod tests { let req = make_request_state(None, 5, KinoviBatchCount::One, false); let cost = KinoviSeedance2p0CostState::from_request(&req); assert!(cost.resolution.is_none()); // None defaults to 720p pricing - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(104)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(83)); } #[test] fn from_request_480p() { let req = make_request_state(Some(KinoviOutputResolution::FourEightyP), 5, KinoviBatchCount::One, false); let cost = KinoviSeedance2p0CostState::from_request(&req); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(39)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(31)); } #[test] fn from_request_1080p_batch_2() { let req = make_request_state(Some(KinoviOutputResolution::TenEightyP), 5, KinoviBatchCount::Two, false); let cost = KinoviSeedance2p0CostState::from_request(&req); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(467)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(371)); } #[test] @@ -270,7 +270,7 @@ mod tests { assert!(cost.has_video_reference); // Video refs add a +8 credits/s surcharge at 720p: 240 credits -> 125 cents. assert_eq!(cost.estimate_cost().cost_in_credits, Some(240)); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(125)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(99)); } #[test] @@ -294,7 +294,7 @@ mod tests { assert_eq!(cost.duration_seconds, 5); assert!(matches!(cost.batch_count, Some(KinoviBatchCount::One))); assert!(!cost.has_video_reference); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(104)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(83)); } #[test] @@ -302,7 +302,7 @@ mod tests { let draft = make_draft(5, 1, Some(RouterResolution::FourEightyP), false); let cost = KinoviSeedance2p0CostState::from_draft(&draft); assert!(matches!(cost.resolution, Some(KinoviOutputResolution::FourEightyP))); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(39)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(31)); } #[test] @@ -311,7 +311,7 @@ mod tests { let cost = KinoviSeedance2p0CostState::from_draft(&draft); assert!(matches!(cost.resolution, Some(KinoviOutputResolution::TenEightyP))); assert!(matches!(cost.batch_count, Some(KinoviBatchCount::Four))); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(933)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(741)); } #[test] @@ -321,7 +321,7 @@ mod tests { assert!(cost.has_video_reference); // Video refs add a +8 credits/s surcharge at 720p: 240 credits -> 125 cents. assert_eq!(cost.estimate_cost().cost_in_credits, Some(240)); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(125)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(99)); } #[test] @@ -337,7 +337,7 @@ mod tests { let cost = KinoviSeedance2p0CostState::from_draft(&draft); assert_eq!(cost.duration_seconds, 15); assert!(matches!(cost.batch_count, Some(KinoviBatchCount::Two))); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(622)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(494)); } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0_fast/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0_fast/cost.rs index 05b57181c5..10b616839c 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0_fast/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/kinovi/seedance_2p0_fast/cost.rs @@ -66,8 +66,8 @@ impl KinoviSeedance2p0FastCostState { }; let costs = pricing_request.calculate_costs(); - let cost_in_credits = costs.kinovi_credits; - let cost_in_usd_cents = costs.usd_cents_rounded_up; + let cost_in_credits = costs.total_cost.kinovi_credits; + let cost_in_usd_cents = costs.total_cost.usd_cents_rounded_up; VideoGenerationCostEstimate { cost_in_credits: Some(cost_in_credits), @@ -100,51 +100,51 @@ mod tests { // ── Direct estimate_cost() tests ── // These must match the reference implementation values exactly. - // -- 720p (28 credits/sec, 193 credits/$1) -- + // -- 720p (28 credits/sec, 243 credits/$1) -- mod pricing_720p { use super::*; #[test] fn cost_720p_batch_1() { - assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One), 59); - assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 73); - assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One), 146); - assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One), 218); + assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One), 47); + assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 58); + assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One), 116); + assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One), 173); } #[test] fn cost_720p_batch_2() { - assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two), 146); + assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two), 116); } #[test] fn cost_720p_batch_4() { - assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Four), 291); + assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Four), 231); } } - // -- 480p (14 credits/sec, 193 credits/$1) -- + // -- 480p (14 credits/sec, 243 credits/$1) -- mod pricing_480p { use super::*; #[test] fn cost_480p_batch_1() { - assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One), 30); - assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 37); - assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One), 73); - assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One), 109); + assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One), 24); + assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 29); + assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One), 58); + assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One), 87); } #[test] fn cost_480p_batch_2() { - assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Two), 73); + assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Two), 58); } #[test] fn cost_480p_batch_4() { - assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 146); + assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 116); } } @@ -191,10 +191,10 @@ mod tests { }; let without = base.estimate_cost(); let with = KinoviSeedance2p0FastCostState { has_video_reference: true, ..base }.estimate_cost(); - // Fast 720p surcharge is +6 credits/s: 140 -> 170 credits (17000/193 = 88.08 -> 89 cents). + // Fast 720p surcharge is +6 credits/s: 140 -> 170 credits (17000/243 = 69.96 -> 70 cents). assert_eq!(without.cost_in_credits, Some(140)); assert_eq!(with.cost_in_credits, Some(170)); - assert_eq!(with.cost_in_usd_cents, Some(89)); + assert_eq!(with.cost_in_usd_cents, Some(70)); } // ── from_request() tests ── @@ -210,7 +210,7 @@ mod tests { assert_eq!(cost.duration_seconds, 5); assert!(matches!(cost.batch_count, Some(KinoviBatchCount::One))); assert!(!cost.has_video_reference); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(73)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(58)); } #[test] @@ -218,14 +218,14 @@ mod tests { let req = make_request_state(None, 5, KinoviBatchCount::One, false); let cost = KinoviSeedance2p0FastCostState::from_request(&req); assert!(cost.resolution.is_none()); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(73)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(58)); } #[test] fn from_request_480p() { let req = make_request_state(Some(KinoviOutputResolution::FourEightyP), 5, KinoviBatchCount::One, false); let cost = KinoviSeedance2p0FastCostState::from_request(&req); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(37)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(29)); } #[test] @@ -235,7 +235,7 @@ mod tests { assert!(cost.has_video_reference); // Video refs add a +6 credits/s surcharge at 720p: 170 credits -> 89 cents. assert_eq!(cost.estimate_cost().cost_in_credits, Some(170)); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(89)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(70)); } #[test] @@ -259,7 +259,7 @@ mod tests { assert_eq!(cost.duration_seconds, 5); assert!(matches!(cost.batch_count, Some(KinoviBatchCount::One))); assert!(!cost.has_video_reference); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(73)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(58)); } #[test] @@ -267,7 +267,7 @@ mod tests { let draft = make_draft(5, 1, Some(RouterResolution::FourEightyP), false); let cost = KinoviSeedance2p0FastCostState::from_draft(&draft); assert!(matches!(cost.resolution, Some(KinoviOutputResolution::FourEightyP))); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(37)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(29)); } #[test] @@ -277,7 +277,7 @@ mod tests { assert!(cost.has_video_reference); // Video refs add a +6 credits/s surcharge at 720p: 170 credits -> 89 cents. assert_eq!(cost.estimate_cost().cost_in_credits, Some(170)); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(89)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(70)); } #[test] @@ -293,7 +293,7 @@ mod tests { let cost = KinoviSeedance2p0FastCostState::from_draft(&draft); assert_eq!(cost.duration_seconds, 15); assert!(matches!(cost.batch_count, Some(KinoviBatchCount::Two))); - assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(436)); + assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(346)); } } diff --git a/crates/api_clients/seedance2pro_client/costs_seedance2.md b/crates/api_clients/seedance2pro_client/costs_seedance2.md new file mode 100644 index 0000000000..2f603df96e --- /dev/null +++ b/crates/api_clients/seedance2pro_client/costs_seedance2.md @@ -0,0 +1,68 @@ +Cost table built on 2026-06-11 + +Seedance 2.0 (~$0.19/sec) +Seedance 2.0 Fast (~$0.13/sec) + +Without Video Uploads +===================== + +### Seedance 2.0 + +4 sec 480p = 60 credits +5 sec 480p = 75 credits +10 sec 480p = 150 credits +15 sec 480p = 225 credits + +4 sec 720p = 160 credits +5 sec 720p = 200 credits +10 sec 720p = 400 credits +15 sec 720p = 600 credits + +4 sec 1080p = 360 credits +5 sec 1080p = 450 credits +10 sec 1080p = 900 credits +15 sec 1080p = 1350 credits + +### Seedance 2.0 Fast + +fast 4 sec 480p = 56 credits +fast 5 sec 480p = 70 credits +fast 10 sec 480p = 140 credits +fast 15 sec 480p = 210 credits + +fast 4 sec 720p = 112 credits +fast 5 sec 720p = 140 credits +fast 10 sec 720p = 280 credits +fast 15 sec 720p = 420 credits + +With Video Uploads +================== + +### Seedance 2.0 With Video Refs + +4 sec 480p + 10 sec video ref = 60 credits + 16 credits +5 sec 480p + 10 sec video ref = 75 credits + 20 credits +10 sec 480p + 10 sec video ref = 150 credits + 40 credits +15 sec 480p + 10 sec video ref = 225 credits + 60 credits + +4 sec 720p + 10 sec video ref = 160 credits + 32 credits +5 sec 720p + 10 sec video ref = 200 credits + 40 credits +10 sec 720p + 10 sec video ref = 400 credits + 80 credits +15 sec 720p + 10 sec video ref = 600 credits + 120 credits + +4 sec 1080p + 10 sec video ref = 360 credits + 72 credits +5 sec 1080p + 10 sec video ref = 450 credits + 90 credits +10 sec 1080p + 10 sec video ref = 900 credits + 180 credits +15 sec 1080p + 10 sec video ref = 1350 credits + 270 credits + +### Seedance 2.0 Fast With Video Refs + +fast 4 sec 480p + 10 sec video ref = 56 credits + 16 credits +fast 5 sec 480p + 10 sec video ref = 70 credits + 20 credits +fast 10 sec 480p + 10 sec video ref = 140 credits + 40 credits +fast 15 sec 480p + 10 sec video ref = 210 credits + 60 credits + +fast 4 sec 720p + 10 sec video ref = 112 credits + 24 credits +fast 5 sec 720p + 10 sec video ref = 140 credits + 30 credits +fast 10 sec 720p + 10 sec video ref = 280 credits + 60 credits +fast 15 sec 720p + 10 sec video ref = 420 credits + 90 credits diff --git a/crates/api_clients/seedance2pro_client/src/cost/constants.rs b/crates/api_clients/seedance2pro_client/src/cost/constants.rs new file mode 100644 index 0000000000..da46086787 --- /dev/null +++ b/crates/api_clients/seedance2pro_client/src/cost/constants.rs @@ -0,0 +1,9 @@ +/// Kinovi credits per US dollar. +/// +/// Current package: 525,000 credits for $2,159.0909 (~243.16 credits/$1, +/// rounded down to 243). +/// +/// Historical packages, for reference: +/// - 500,000 credits for $2,159.09 (~231.58 credits/$1, rounded down to 231) +/// - 22,000 credits for $114 (~192.98 credits/$1, rounded to 193) +pub const CREDITS_PER_DOLLAR: u64 = 243; diff --git a/crates/api_clients/seedance2pro_client/src/cost/kinovi_generation_cost.rs b/crates/api_clients/seedance2pro_client/src/cost/kinovi_generation_cost.rs new file mode 100644 index 0000000000..a8c3b07deb --- /dev/null +++ b/crates/api_clients/seedance2pro_client/src/cost/kinovi_generation_cost.rs @@ -0,0 +1,90 @@ +use crate::cost::constants::CREDITS_PER_DOLLAR; + +/// The cost of a Kinovi generation, in both native credits and USD cents. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct KinoviGenerationCost { + /// The cost of the generation in Kinovi credits. + pub kinovi_credits: u64, + + /// Estimated cost in cents, rounded up to the nearest whole cent. + /// This does not account for discounts, prorations, etc. + /// This is always rounded up when fractional. + pub usd_cents_rounded_up: u64, + + /// Estimated cost in cents, rounded down to the nearest whole cent. + pub usd_cents_rounded_down: u64, + + /// Estimated cost in cents without rounding (the exact fractional value). + pub usd_cents_fractional: f64, +} + +impl KinoviGenerationCost { + /// Build a cost from a Kinovi credit amount, deriving the USD cents via + /// the credit-package rate. + pub fn from_kinovi_credits(kinovi_credits: u64) -> Self { + let total_hundredths = kinovi_credits * 100; + Self { + kinovi_credits, + // Integer ceiling / floor of (credits * 100 / CREDITS_PER_DOLLAR). + usd_cents_rounded_up: total_hundredths.div_ceil(CREDITS_PER_DOLLAR), + usd_cents_rounded_down: total_hundredths / CREDITS_PER_DOLLAR, + usd_cents_fractional: total_hundredths as f64 / CREDITS_PER_DOLLAR as f64, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const FLOAT_TOLERANCE: f64 = 1e-9; + + #[test] + fn zero_credits_is_zero_cents() { + let cost = KinoviGenerationCost::from_kinovi_credits(0); + assert_eq!(cost.kinovi_credits, 0); + assert_eq!(cost.usd_cents_rounded_up, 0); + assert_eq!(cost.usd_cents_rounded_down, 0); + assert!((cost.usd_cents_fractional - 0.0).abs() < FLOAT_TOLERANCE); + } + + #[test] + fn exact_dollar_boundaries_do_not_round() { + // 243 credits = exactly $1.00 — all three representations agree. + let cost = KinoviGenerationCost::from_kinovi_credits(243); + assert_eq!(cost.kinovi_credits, 243); + assert_eq!(cost.usd_cents_rounded_up, 100); + assert_eq!(cost.usd_cents_rounded_down, 100); + assert!((cost.usd_cents_fractional - 100.0).abs() < FLOAT_TOLERANCE); + + let cost = KinoviGenerationCost::from_kinovi_credits(486); + assert_eq!(cost.usd_cents_rounded_up, 200); + assert_eq!(cost.usd_cents_rounded_down, 200); + assert!((cost.usd_cents_fractional - 200.0).abs() < FLOAT_TOLERANCE); + } + + #[test] + fn fractional_cents_round_in_both_directions() { + // 244 credits = 100.411... cents. + let cost = KinoviGenerationCost::from_kinovi_credits(244); + assert_eq!(cost.usd_cents_rounded_up, 101); + assert_eq!(cost.usd_cents_rounded_down, 100); + assert!((cost.usd_cents_fractional - (24400.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // 1 credit = 0.432... cents. + let cost = KinoviGenerationCost::from_kinovi_credits(1); + assert_eq!(cost.usd_cents_rounded_up, 1); + assert_eq!(cost.usd_cents_rounded_down, 0); + assert!((cost.usd_cents_fractional - (100.0 / 243.0)).abs() < FLOAT_TOLERANCE); + } + + #[test] + fn rounded_bounds_bracket_the_fractional_value() { + for credits in [0, 1, 12, 75, 200, 243, 244, 450, 1350] { + let cost = KinoviGenerationCost::from_kinovi_credits(credits); + assert!(cost.usd_cents_rounded_down as f64 <= cost.usd_cents_fractional); + assert!(cost.usd_cents_fractional <= cost.usd_cents_rounded_up as f64); + assert!(cost.usd_cents_rounded_up - cost.usd_cents_rounded_down <= 1); + } + } +} diff --git a/crates/api_clients/seedance2pro_client/src/cost/kinovi_seedance_generation_cost.rs b/crates/api_clients/seedance2pro_client/src/cost/kinovi_seedance_generation_cost.rs new file mode 100644 index 0000000000..353c6aa2c5 --- /dev/null +++ b/crates/api_clients/seedance2pro_client/src/cost/kinovi_seedance_generation_cost.rs @@ -0,0 +1,111 @@ +use crate::cost::kinovi_generation_cost::KinoviGenerationCost; + +/// The cost of a Kinovi Seedance generation, with the video-reference +/// surcharge broken out from the base price. +/// +/// `total_cost` covers base + surcharge. NB: the total's USD conversions are +/// computed from the SUMMED credits (rounded once), so they may differ by a +/// cent from adding the parts' rounded USD values together. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct KinoviSeedanceGenerationCost { + /// The full cost (base + any surcharge), in credits and USD cents. + pub total_cost: KinoviGenerationCost, + + /// The base generation cost (resolution rate × output duration × batch + /// count), excluding any surcharges. + pub base_cost: KinoviGenerationCost, + + /// The video-reference surcharge, when one or more reference videos are + /// attached. `None` when no reference videos are attached. + pub video_reference_surcharge_cost: Option, +} + +impl KinoviSeedanceGenerationCost { + /// Build a cost from base credits plus an optional video-reference + /// surcharge (both in Kinovi credits). The per-part and total USD + /// conversions are derived. + pub fn from_base_and_surcharge( + base_credits: u64, + maybe_video_reference_surcharge_credits: Option, + ) -> Self { + let total_credits = base_credits + maybe_video_reference_surcharge_credits.unwrap_or(0); + Self { + total_cost: KinoviGenerationCost::from_kinovi_credits(total_credits), + base_cost: KinoviGenerationCost::from_kinovi_credits(base_credits), + video_reference_surcharge_cost: maybe_video_reference_surcharge_credits + .map(KinoviGenerationCost::from_kinovi_credits), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const FLOAT_TOLERANCE: f64 = 1e-9; + + #[test] + fn without_surcharge() { + let cost = KinoviSeedanceGenerationCost::from_base_and_surcharge(200, None); + + assert!(cost.video_reference_surcharge_cost.is_none()); + + // Base: 200 credits; 20000/243 = 82.30. + assert_eq!(cost.base_cost.kinovi_credits, 200); + assert_eq!(cost.base_cost.usd_cents_rounded_up, 83); + assert_eq!(cost.base_cost.usd_cents_rounded_down, 82); + assert!((cost.base_cost.usd_cents_fractional - (20000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // Total equals base when there's no surcharge. + assert_eq!(cost.total_cost, cost.base_cost); + } + + #[test] + fn with_surcharge() { + let cost = KinoviSeedanceGenerationCost::from_base_and_surcharge(200, Some(40)); + + // Base: 200 credits; 20000/243 = 82.30. + assert_eq!(cost.base_cost.kinovi_credits, 200); + assert_eq!(cost.base_cost.usd_cents_rounded_up, 83); + assert_eq!(cost.base_cost.usd_cents_rounded_down, 82); + assert!((cost.base_cost.usd_cents_fractional - (20000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // Surcharge: 40 credits; 4000/243 = 16.46. + let surcharge = cost.video_reference_surcharge_cost.expect("should have surcharge"); + assert_eq!(surcharge.kinovi_credits, 40); + assert_eq!(surcharge.usd_cents_rounded_up, 17); + assert_eq!(surcharge.usd_cents_rounded_down, 16); + assert!((surcharge.usd_cents_fractional - (4000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // Total: 240 credits; 24000/243 = 98.77. + assert_eq!(cost.total_cost.kinovi_credits, 240); + assert_eq!(cost.total_cost.usd_cents_rounded_up, 99); + assert_eq!(cost.total_cost.usd_cents_rounded_down, 98); + assert!((cost.total_cost.usd_cents_fractional - (24000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + } + + #[test] + fn total_credits_are_base_plus_surcharge() { + let cost = KinoviSeedanceGenerationCost::from_base_and_surcharge(450, Some(90)); + assert_eq!( + cost.total_cost.kinovi_credits, + cost.base_cost.kinovi_credits + cost.video_reference_surcharge_cost.unwrap().kinovi_credits); + } + + /// The total's USD is rounded ONCE on the summed credits, so it can be a + /// cent under the sum of the parts' rounded-up values. + #[test] + fn total_usd_is_rounded_once_not_per_part() { + let cost = KinoviSeedanceGenerationCost::from_base_and_surcharge(200, Some(40)); + let surcharge = cost.video_reference_surcharge_cost.unwrap(); + + // Parts round up to 83 + 17 = 100, but the total (240 credits) rounds + // once to 99 — one cent less than summing the rounded parts. + assert_eq!(cost.base_cost.usd_cents_rounded_up + surcharge.usd_cents_rounded_up, 100); + assert_eq!(cost.total_cost.usd_cents_rounded_up, 99); + + // …but the fractional parts always sum exactly to the fractional total. + let parts = cost.base_cost.usd_cents_fractional + surcharge.usd_cents_fractional; + assert!((parts - cost.total_cost.usd_cents_fractional).abs() < FLOAT_TOLERANCE); + } +} diff --git a/crates/api_clients/seedance2pro_client/src/cost/mod.rs b/crates/api_clients/seedance2pro_client/src/cost/mod.rs new file mode 100644 index 0000000000..0ce4b61b38 --- /dev/null +++ b/crates/api_clients/seedance2pro_client/src/cost/mod.rs @@ -0,0 +1,3 @@ +pub mod constants; +pub mod kinovi_generation_cost; +pub mod kinovi_seedance_generation_cost; diff --git a/crates/api_clients/seedance2pro_client/src/generate/cost.rs b/crates/api_clients/seedance2pro_client/src/generate/cost.rs deleted file mode 100644 index 58f53bb2d0..0000000000 --- a/crates/api_clients/seedance2pro_client/src/generate/cost.rs +++ /dev/null @@ -1,61 +0,0 @@ -/// Kinovi credits per US dollar. -/// Credit package: 22,000 credits for $114 (~192.98 credits/$1, rounded to 193). -const CREDITS_PER_DOLLAR: u64 = 193; - -/// The cost of a Kinovi generation, in both native credits and USD cents. -pub struct KinoviGenerationCost { - /// The cost of the generation in Kinovi credits. - pub kinovi_credits: u64, - - /// Estimated cost in cents, rounded up to the nearest whole cent. - /// This does not account for discounts, prorations, etc. - /// This is always rounded up when fractional. - pub usd_cents_rounded_up: u64, -} - -impl KinoviGenerationCost { - /// Build a cost from a Kinovi credit amount, deriving the USD cents via - /// the credit-package rate (always rounding up fractional cents). - pub fn from_kinovi_credits(kinovi_credits: u64) -> Self { - // Integer ceiling of (credits * 100 / CREDITS_PER_DOLLAR). - let usd_cents_rounded_up = (kinovi_credits * 100).div_ceil(CREDITS_PER_DOLLAR); - Self { - kinovi_credits, - usd_cents_rounded_up, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn zero_credits_is_zero_cents() { - let cost = KinoviGenerationCost::from_kinovi_credits(0); - assert_eq!(cost.kinovi_credits, 0); - assert_eq!(cost.usd_cents_rounded_up, 0); - } - - #[test] - fn exact_dollar_boundaries_do_not_round() { - // 193 credits = exactly $1.00. - let cost = KinoviGenerationCost::from_kinovi_credits(193); - assert_eq!(cost.kinovi_credits, 193); - assert_eq!(cost.usd_cents_rounded_up, 100); - - let cost = KinoviGenerationCost::from_kinovi_credits(386); - assert_eq!(cost.usd_cents_rounded_up, 200); - } - - #[test] - fn fractional_cents_round_up() { - // 194 credits = 100.518... cents -> 101. - let cost = KinoviGenerationCost::from_kinovi_credits(194); - assert_eq!(cost.usd_cents_rounded_up, 101); - - // 1 credit = 0.518... cents -> 1. - let cost = KinoviGenerationCost::from_kinovi_credits(1); - assert_eq!(cost.usd_cents_rounded_up, 1); - } -} diff --git a/crates/api_clients/seedance2pro_client/src/generate/image/generate_midjourney_v7.rs b/crates/api_clients/seedance2pro_client/src/generate/image/generate_midjourney_v7.rs index 23b5d0f683..1f449bdf3e 100644 --- a/crates/api_clients/seedance2pro_client/src/generate/image/generate_midjourney_v7.rs +++ b/crates/api_clients/seedance2pro_client/src/generate/image/generate_midjourney_v7.rs @@ -1,4 +1,4 @@ -use crate::generate::cost::KinoviGenerationCost; +use crate::cost::kinovi_generation_cost::KinoviGenerationCost; use crate::creds::seedance2pro_session::Seedance2ProSession; use crate::error::seedance2pro_error::Seedance2ProError; use crate::requests::generate_image::generate_image::{ @@ -288,13 +288,17 @@ mod tests { } #[test] - fn usd_cents_batch_one_is_six() { - assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 7); // 1200/193 = 6.22 -> rounds UP + fn usd_cents_batch_one_is_five() { + assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 5); // 1200/243 = 4.94 -> rounds UP + assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_down, 4); + assert!((make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_fractional - (1200.0 / 243.0)).abs() < 1e-9); } #[test] - fn usd_cents_batch_four_is_twentyfive() { - assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 25); // 4800/193 = 24.87 -> rounds UP + fn usd_cents_batch_four_is_twenty() { + assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 20); // 4800/243 = 19.75 -> rounds UP + assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_down, 19); + assert!((make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_fractional - (4800.0 / 243.0)).abs() < 1e-9); } /// Pricing must match the inner module byte-for-byte (sanity that no diff --git a/crates/api_clients/seedance2pro_client/src/generate/image/generate_midjourney_v7_niji.rs b/crates/api_clients/seedance2pro_client/src/generate/image/generate_midjourney_v7_niji.rs index 3e242cd132..724871b619 100644 --- a/crates/api_clients/seedance2pro_client/src/generate/image/generate_midjourney_v7_niji.rs +++ b/crates/api_clients/seedance2pro_client/src/generate/image/generate_midjourney_v7_niji.rs @@ -1,4 +1,4 @@ -use crate::generate::cost::KinoviGenerationCost; +use crate::cost::kinovi_generation_cost::KinoviGenerationCost; use crate::creds::seedance2pro_session::Seedance2ProSession; use crate::error::seedance2pro_error::Seedance2ProError; use crate::requests::generate_image::generate_image::{ @@ -286,13 +286,17 @@ mod tests { } #[test] - fn usd_cents_batch_one_is_six() { - assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 7); // 1200/193 = 6.22 -> rounds UP + fn usd_cents_batch_one_is_five() { + assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 5); // 1200/243 = 4.94 -> rounds UP + assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_down, 4); + assert!((make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_fractional - (1200.0 / 243.0)).abs() < 1e-9); } #[test] - fn usd_cents_batch_four_is_twentyfive() { - assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 25); // 4800/193 = 24.87 -> rounds UP + fn usd_cents_batch_four_is_twenty() { + assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 20); // 4800/243 = 19.75 -> rounds UP + assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_down, 19); + assert!((make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_fractional - (4800.0 / 243.0)).abs() < 1e-9); } /// Pricing must match the inner module exactly. diff --git a/crates/api_clients/seedance2pro_client/src/generate/image/generate_midjourney_v8.rs b/crates/api_clients/seedance2pro_client/src/generate/image/generate_midjourney_v8.rs index 7805aea485..cf4b68e4b7 100644 --- a/crates/api_clients/seedance2pro_client/src/generate/image/generate_midjourney_v8.rs +++ b/crates/api_clients/seedance2pro_client/src/generate/image/generate_midjourney_v8.rs @@ -1,4 +1,4 @@ -use crate::generate::cost::KinoviGenerationCost; +use crate::cost::kinovi_generation_cost::KinoviGenerationCost; use crate::creds::seedance2pro_session::Seedance2ProSession; use crate::error::seedance2pro_error::Seedance2ProError; use crate::requests::generate_image::generate_image::{ @@ -283,13 +283,17 @@ mod tests { } #[test] - fn usd_cents_batch_one_is_six() { - assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 7); // 1200/193 = 6.22 -> rounds UP + fn usd_cents_batch_one_is_five() { + assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 5); // 1200/243 = 4.94 -> rounds UP + assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_down, 4); + assert!((make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_fractional - (1200.0 / 243.0)).abs() < 1e-9); } #[test] - fn usd_cents_batch_four_is_twentyfive() { - assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 25); // 4800/193 = 24.87 -> rounds UP + fn usd_cents_batch_four_is_twenty() { + assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 20); // 4800/243 = 19.75 -> rounds UP + assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_down, 19); + assert!((make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_fractional - (4800.0 / 243.0)).abs() < 1e-9); } /// Pricing must match the inner module exactly. diff --git a/crates/api_clients/seedance2pro_client/src/generate/mod.rs b/crates/api_clients/seedance2pro_client/src/generate/mod.rs index c8ce6b1f6f..c10a6ca15e 100644 --- a/crates/api_clients/seedance2pro_client/src/generate/mod.rs +++ b/crates/api_clients/seedance2pro_client/src/generate/mod.rs @@ -1,3 +1,2 @@ -pub mod cost; pub mod image; pub mod video; diff --git a/crates/api_clients/seedance2pro_client/src/generate/video/generate_happy_horse_1p0.rs b/crates/api_clients/seedance2pro_client/src/generate/video/generate_happy_horse_1p0.rs index 489a360b11..69895cea02 100644 --- a/crates/api_clients/seedance2pro_client/src/generate/video/generate_happy_horse_1p0.rs +++ b/crates/api_clients/seedance2pro_client/src/generate/video/generate_happy_horse_1p0.rs @@ -1,6 +1,6 @@ use crate::creds::seedance2pro_session::Seedance2ProSession; use crate::error::seedance2pro_error::Seedance2ProError; -use crate::generate::cost::KinoviGenerationCost; +use crate::cost::kinovi_generation_cost::KinoviGenerationCost; use crate::requests::kinovi_host::KinoviHost; use crate::requests::workflow_run_task::workflow_run_task::{ workflow_run_task, KinoviAspectRatioRaw, KinoviBatchCountRaw, @@ -63,7 +63,7 @@ pub enum KinoviHappyHorse1p0BatchCount { // // Default resolution (None) is 720p. // Batch count multiplies the total cost. -// Credit package: 22,000 credits for $114 (~192.98 credits/$1, rounded to 193). +// Credit package: 525,000 credits for $2,159.0909 (~243.16 credits/$1, rounded down to 243). impl GenerateHappyHorse1p0Request { /// Estimate the credit cost for this generation request. @@ -334,42 +334,52 @@ mod tests { /// (USD cents are always rounded UP when fractional.) #[test] fn costs_720p_5s() { - // 33 credits/s × 5s = 165 credits; 16500/193 = 85.49 → rounds UP to 86¢ + // 33 credits/s × 5s = 165 credits; 16500/243 = 67.90 → rounds UP to 68¢ let costs = r720(5).calculate_costs(); assert_eq!(costs.kinovi_credits, 165); - assert_eq!(costs.usd_cents_rounded_up, 86); + assert_eq!(costs.usd_cents_rounded_up, 68); + assert_eq!(costs.usd_cents_rounded_down, 67); + assert!((costs.usd_cents_fractional - (16500.0 / 243.0)).abs() < 1e-9); } #[test] fn costs_1080p_5s() { - // 66 credits/s × 5s = 330 credits; 33000/193 = 170.98 → 171¢ + // 66 credits/s × 5s = 330 credits; 33000/243 = 135.80 → 136¢ let costs = r1080(5).calculate_costs(); assert_eq!(costs.kinovi_credits, 330); - assert_eq!(costs.usd_cents_rounded_up, 171); + assert_eq!(costs.usd_cents_rounded_up, 136); + assert_eq!(costs.usd_cents_rounded_down, 135); + assert!((costs.usd_cents_fractional - (33000.0 / 243.0)).abs() < 1e-9); } #[test] fn costs_720p_15s() { - // 33 credits/s × 15s = 495 credits; 49500/193 = 256.48 → rounds UP to 257¢ + // 33 credits/s × 15s = 495 credits; 49500/243 = 203.70 → rounds UP to 204¢ let costs = r720(15).calculate_costs(); assert_eq!(costs.kinovi_credits, 495); - assert_eq!(costs.usd_cents_rounded_up, 257); + assert_eq!(costs.usd_cents_rounded_up, 204); + assert_eq!(costs.usd_cents_rounded_down, 203); + assert!((costs.usd_cents_fractional - (49500.0 / 243.0)).abs() < 1e-9); } #[test] fn costs_1080p_15s() { - // 66 credits/s × 15s = 990 credits; 99000/193 = 512.95 → 513¢ + // 66 credits/s × 15s = 990 credits; 99000/243 = 407.41 → 408¢ let costs = r1080(15).calculate_costs(); assert_eq!(costs.kinovi_credits, 990); - assert_eq!(costs.usd_cents_rounded_up, 513); + assert_eq!(costs.usd_cents_rounded_up, 408); + assert_eq!(costs.usd_cents_rounded_down, 407); + assert!((costs.usd_cents_fractional - (99000.0 / 243.0)).abs() < 1e-9); } #[test] fn costs_batch_2_720p_5s() { - // 165 credits × 2 = 330 credits; 33000/193 = 170.98 → 171¢ + // 165 credits × 2 = 330 credits; 33000/243 = 135.80 → 136¢ let costs = make_request(5, None, Some(KinoviHappyHorse1p0BatchCount::Two)).calculate_costs(); assert_eq!(costs.kinovi_credits, 330); - assert_eq!(costs.usd_cents_rounded_up, 171); + assert_eq!(costs.usd_cents_rounded_up, 136); + assert_eq!(costs.usd_cents_rounded_down, 135); + assert!((costs.usd_cents_fractional - (33000.0 / 243.0)).abs() < 1e-9); } /// The deprecated shims return the corresponding struct fields. diff --git a/crates/api_clients/seedance2pro_client/src/generate/video/generate_seedance_2p0.rs b/crates/api_clients/seedance2pro_client/src/generate/video/generate_seedance_2p0.rs index 8892a1127c..8a846e0168 100644 --- a/crates/api_clients/seedance2pro_client/src/generate/video/generate_seedance_2p0.rs +++ b/crates/api_clients/seedance2pro_client/src/generate/video/generate_seedance_2p0.rs @@ -1,6 +1,6 @@ use crate::creds::seedance2pro_session::Seedance2ProSession; use crate::error::seedance2pro_error::Seedance2ProError; -use crate::generate::cost::KinoviGenerationCost; +use crate::cost::kinovi_seedance_generation_cost::KinoviSeedanceGenerationCost; use crate::requests::kinovi_host::KinoviHost; use crate::requests::workflow_run_task::workflow_run_task::{ workflow_run_task, KinoviAspectRatioRaw, KinoviBatchCountRaw, @@ -72,7 +72,7 @@ pub enum KinoviSeedance2p0BatchCount { // // Default resolution (None) is 720p. // Batch count multiplies the total cost. -// Credit package: 22,000 credits for $114 (~192.98 credits/$1, rounded to 193). +// Credit package: 525,000 credits for $2,159.0909 (~243.16 credits/$1, rounded down to 243). impl GenerateSeedance2p0Request { /// Calculate the cost of this generation request, in Kinovi credits and @@ -80,7 +80,7 @@ impl GenerateSeedance2p0Request { /// /// Attaching reference VIDEOS adds a per-output-second surcharge (see the /// pricing table below). Reference images and audio are free. - pub fn calculate_costs(&self) -> KinoviGenerationCost { + pub fn calculate_costs(&self) -> KinoviSeedanceGenerationCost { let credits_per_second: u64 = match self.output_resolution { Some(KinoviSeedance2p0OutputResolution::FourEightyP) => 15, Some(KinoviSeedance2p0OutputResolution::SevenTwentyP) | None => 40, @@ -108,15 +108,24 @@ impl GenerateSeedance2p0Request { 0 }; - let per_video = u64::from(self.duration_seconds) - * (credits_per_second + video_reference_surcharge_per_second); let batch_multiplier: u64 = match self.batch_count { None | Some(KinoviSeedance2p0BatchCount::One) => 1, Some(KinoviSeedance2p0BatchCount::Two) => 2, Some(KinoviSeedance2p0BatchCount::Four) => 4, }; - KinoviGenerationCost::from_kinovi_credits(per_video * batch_multiplier) + let duration = u64::from(self.duration_seconds); + let base_credits = duration * credits_per_second * batch_multiplier; + let maybe_video_reference_surcharge_credits = if self.has_video_reference() { + Some(duration * video_reference_surcharge_per_second * batch_multiplier) + } else { + None + }; + + KinoviSeedanceGenerationCost::from_base_and_surcharge( + base_credits, + maybe_video_reference_surcharge_credits, + ) } fn has_video_reference(&self) -> bool { @@ -128,14 +137,14 @@ impl GenerateSeedance2p0Request { /// Estimate the credit cost for this generation request. #[deprecated(note = "Use calculate_costs() instead")] pub fn estimate_credits(&self) -> u32 { - self.calculate_costs().kinovi_credits as u32 + self.calculate_costs().total_cost.kinovi_credits as u32 } /// Estimate the USD cost in cents for this generation request. /// NB: Rounds UP fractional cents (the historical behavior rounded to nearest). #[deprecated(note = "Use calculate_costs() instead")] pub fn estimate_cost_in_usd_cents(&self) -> u64 { - self.calculate_costs().usd_cents_rounded_up + self.calculate_costs().total_cost.usd_cents_rounded_up } } @@ -262,6 +271,171 @@ mod tests { build_request(dur, Some(KinoviSeedance2p0OutputResolution::TenEightyP), None) } + // ── Comprehensive per-resolution coverage ── + // + // Seedance 2.0: 480p 15/s (+4/s video ref), 720p 40/s (+8/s), 1080p 90/s (+18/s). + // Every field and subfield is asserted: total_cost, base_cost, + // video_reference_surcharge_cost × (kinovi_credits, usd_cents_rounded_up, + // usd_cents_rounded_down, usd_cents_fractional). All cases use a 5 second + // duration with no batching. + + mod comprehensive { + use super::*; + + const FLOAT_TOLERANCE: f64 = 1e-9; + + + mod resolution_480p { + use super::*; + + #[test] + fn test_without_reference_video() { + let costs = r480(5).calculate_costs(); + + // Base: 15 credits/s × 5s = 75 credits; 7500/243 = 30.8642. + assert_eq!(costs.base_cost.kinovi_credits, 75); + assert_eq!(costs.base_cost.usd_cents_rounded_up, 31); + assert_eq!(costs.base_cost.usd_cents_rounded_down, 30); + assert!((costs.base_cost.usd_cents_fractional - (7500.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // No reference video: no surcharge, and the total equals the base. + assert!(costs.video_reference_surcharge_cost.is_none()); + + assert_eq!(costs.total_cost.kinovi_credits, 75); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 31); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 30); + assert!((costs.total_cost.usd_cents_fractional - (7500.0 / 243.0)).abs() < FLOAT_TOLERANCE); + } + + #[test] + fn test_with_reference_video() { + let mut request = r480(5); + request.reference_video_urls = Some(vec!["https://example.com/ref.mp4".to_string()]); + let costs = request.calculate_costs(); + + // Base: 15 credits/s × 5s = 75 credits; 7500/243 = 30.8642. + assert_eq!(costs.base_cost.kinovi_credits, 75); + assert_eq!(costs.base_cost.usd_cents_rounded_up, 31); + assert_eq!(costs.base_cost.usd_cents_rounded_down, 30); + assert!((costs.base_cost.usd_cents_fractional - (7500.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // Surcharge: 4 credits/s × 5s = 20 credits; 2000/243 = 8.2305. + let surcharge = costs.video_reference_surcharge_cost.expect("should have surcharge"); + assert_eq!(surcharge.kinovi_credits, 20); + assert_eq!(surcharge.usd_cents_rounded_up, 9); + assert_eq!(surcharge.usd_cents_rounded_down, 8); + assert!((surcharge.usd_cents_fractional - (2000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // Total: 75 + 20 = 95 credits; 9500/243 = 39.0947. + assert_eq!(costs.total_cost.kinovi_credits, 95); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 40); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 39); + assert!((costs.total_cost.usd_cents_fractional - (9500.0 / 243.0)).abs() < FLOAT_TOLERANCE); + } + } + + + mod resolution_720p { + use super::*; + + #[test] + fn test_without_reference_video() { + let costs = r720(5).calculate_costs(); + + // Base: 40 credits/s × 5s = 200 credits; 20000/243 = 82.3045. + assert_eq!(costs.base_cost.kinovi_credits, 200); + assert_eq!(costs.base_cost.usd_cents_rounded_up, 83); + assert_eq!(costs.base_cost.usd_cents_rounded_down, 82); + assert!((costs.base_cost.usd_cents_fractional - (20000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // No reference video: no surcharge, and the total equals the base. + assert!(costs.video_reference_surcharge_cost.is_none()); + + assert_eq!(costs.total_cost.kinovi_credits, 200); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 83); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 82); + assert!((costs.total_cost.usd_cents_fractional - (20000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + } + + #[test] + fn test_with_reference_video() { + let mut request = r720(5); + request.reference_video_urls = Some(vec!["https://example.com/ref.mp4".to_string()]); + let costs = request.calculate_costs(); + + // Base: 40 credits/s × 5s = 200 credits; 20000/243 = 82.3045. + assert_eq!(costs.base_cost.kinovi_credits, 200); + assert_eq!(costs.base_cost.usd_cents_rounded_up, 83); + assert_eq!(costs.base_cost.usd_cents_rounded_down, 82); + assert!((costs.base_cost.usd_cents_fractional - (20000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // Surcharge: 8 credits/s × 5s = 40 credits; 4000/243 = 16.4609. + let surcharge = costs.video_reference_surcharge_cost.expect("should have surcharge"); + assert_eq!(surcharge.kinovi_credits, 40); + assert_eq!(surcharge.usd_cents_rounded_up, 17); + assert_eq!(surcharge.usd_cents_rounded_down, 16); + assert!((surcharge.usd_cents_fractional - (4000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // Total: 200 + 40 = 240 credits; 24000/243 = 98.7654. + assert_eq!(costs.total_cost.kinovi_credits, 240); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 99); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 98); + assert!((costs.total_cost.usd_cents_fractional - (24000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + } + } + + + mod resolution_1080p { + use super::*; + + #[test] + fn test_without_reference_video() { + let costs = r1080(5).calculate_costs(); + + // Base: 90 credits/s × 5s = 450 credits; 45000/243 = 185.1852. + assert_eq!(costs.base_cost.kinovi_credits, 450); + assert_eq!(costs.base_cost.usd_cents_rounded_up, 186); + assert_eq!(costs.base_cost.usd_cents_rounded_down, 185); + assert!((costs.base_cost.usd_cents_fractional - (45000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // No reference video: no surcharge, and the total equals the base. + assert!(costs.video_reference_surcharge_cost.is_none()); + + assert_eq!(costs.total_cost.kinovi_credits, 450); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 186); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 185); + assert!((costs.total_cost.usd_cents_fractional - (45000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + } + + #[test] + fn test_with_reference_video() { + let mut request = r1080(5); + request.reference_video_urls = Some(vec!["https://example.com/ref.mp4".to_string()]); + let costs = request.calculate_costs(); + + // Base: 90 credits/s × 5s = 450 credits; 45000/243 = 185.1852. + assert_eq!(costs.base_cost.kinovi_credits, 450); + assert_eq!(costs.base_cost.usd_cents_rounded_up, 186); + assert_eq!(costs.base_cost.usd_cents_rounded_down, 185); + assert!((costs.base_cost.usd_cents_fractional - (45000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // Surcharge: 18 credits/s × 5s = 90 credits; 9000/243 = 37.0370. + let surcharge = costs.video_reference_surcharge_cost.expect("should have surcharge"); + assert_eq!(surcharge.kinovi_credits, 90); + assert_eq!(surcharge.usd_cents_rounded_up, 38); + assert_eq!(surcharge.usd_cents_rounded_down, 37); + assert!((surcharge.usd_cents_fractional - (9000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // Total: 450 + 90 = 540 credits; 54000/243 = 222.2222. + assert_eq!(costs.total_cost.kinovi_credits, 540); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 223); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 222); + assert!((costs.total_cost.usd_cents_fractional - (54000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + } + } + + } + // ── 480p credits (15 credits/sec) ── mod credits_480p { @@ -269,19 +443,19 @@ mod tests { #[test] fn every_duration() { - assert_eq!(r480(3).calculate_costs().kinovi_credits, 45); - assert_eq!(r480(4).calculate_costs().kinovi_credits, 60); - assert_eq!(r480(5).calculate_costs().kinovi_credits, 75); - assert_eq!(r480(6).calculate_costs().kinovi_credits, 90); - assert_eq!(r480(7).calculate_costs().kinovi_credits, 105); - assert_eq!(r480(8).calculate_costs().kinovi_credits, 120); - assert_eq!(r480(9).calculate_costs().kinovi_credits, 135); - assert_eq!(r480(10).calculate_costs().kinovi_credits, 150); - assert_eq!(r480(11).calculate_costs().kinovi_credits, 165); - assert_eq!(r480(12).calculate_costs().kinovi_credits, 180); - assert_eq!(r480(13).calculate_costs().kinovi_credits, 195); - assert_eq!(r480(14).calculate_costs().kinovi_credits, 210); - assert_eq!(r480(15).calculate_costs().kinovi_credits, 225); + assert_eq!(r480(3).calculate_costs().total_cost.kinovi_credits, 45); + assert_eq!(r480(4).calculate_costs().total_cost.kinovi_credits, 60); + assert_eq!(r480(5).calculate_costs().total_cost.kinovi_credits, 75); + assert_eq!(r480(6).calculate_costs().total_cost.kinovi_credits, 90); + assert_eq!(r480(7).calculate_costs().total_cost.kinovi_credits, 105); + assert_eq!(r480(8).calculate_costs().total_cost.kinovi_credits, 120); + assert_eq!(r480(9).calculate_costs().total_cost.kinovi_credits, 135); + assert_eq!(r480(10).calculate_costs().total_cost.kinovi_credits, 150); + assert_eq!(r480(11).calculate_costs().total_cost.kinovi_credits, 165); + assert_eq!(r480(12).calculate_costs().total_cost.kinovi_credits, 180); + assert_eq!(r480(13).calculate_costs().total_cost.kinovi_credits, 195); + assert_eq!(r480(14).calculate_costs().total_cost.kinovi_credits, 210); + assert_eq!(r480(15).calculate_costs().total_cost.kinovi_credits, 225); } } @@ -292,25 +466,25 @@ mod tests { #[test] fn every_duration() { - assert_eq!(r720(3).calculate_costs().kinovi_credits, 120); - assert_eq!(r720(4).calculate_costs().kinovi_credits, 160); - assert_eq!(r720(5).calculate_costs().kinovi_credits, 200); - assert_eq!(r720(6).calculate_costs().kinovi_credits, 240); - assert_eq!(r720(7).calculate_costs().kinovi_credits, 280); - assert_eq!(r720(8).calculate_costs().kinovi_credits, 320); - assert_eq!(r720(9).calculate_costs().kinovi_credits, 360); - assert_eq!(r720(10).calculate_costs().kinovi_credits, 400); - assert_eq!(r720(11).calculate_costs().kinovi_credits, 440); - assert_eq!(r720(12).calculate_costs().kinovi_credits, 480); - assert_eq!(r720(13).calculate_costs().kinovi_credits, 520); - assert_eq!(r720(14).calculate_costs().kinovi_credits, 560); - assert_eq!(r720(15).calculate_costs().kinovi_credits, 600); + assert_eq!(r720(3).calculate_costs().total_cost.kinovi_credits, 120); + assert_eq!(r720(4).calculate_costs().total_cost.kinovi_credits, 160); + assert_eq!(r720(5).calculate_costs().total_cost.kinovi_credits, 200); + assert_eq!(r720(6).calculate_costs().total_cost.kinovi_credits, 240); + assert_eq!(r720(7).calculate_costs().total_cost.kinovi_credits, 280); + assert_eq!(r720(8).calculate_costs().total_cost.kinovi_credits, 320); + assert_eq!(r720(9).calculate_costs().total_cost.kinovi_credits, 360); + assert_eq!(r720(10).calculate_costs().total_cost.kinovi_credits, 400); + assert_eq!(r720(11).calculate_costs().total_cost.kinovi_credits, 440); + assert_eq!(r720(12).calculate_costs().total_cost.kinovi_credits, 480); + assert_eq!(r720(13).calculate_costs().total_cost.kinovi_credits, 520); + assert_eq!(r720(14).calculate_costs().total_cost.kinovi_credits, 560); + assert_eq!(r720(15).calculate_costs().total_cost.kinovi_credits, 600); } #[test] fn explicit_720p_same_as_default() { - let default = r720(5).calculate_costs().kinovi_credits; - let explicit = build_request(5, Some(KinoviSeedance2p0OutputResolution::SevenTwentyP), None).calculate_costs().kinovi_credits; + let default = r720(5).calculate_costs().total_cost.kinovi_credits; + let explicit = build_request(5, Some(KinoviSeedance2p0OutputResolution::SevenTwentyP), None).calculate_costs().total_cost.kinovi_credits; assert_eq!(default, explicit); } } @@ -322,19 +496,19 @@ mod tests { #[test] fn every_duration() { - assert_eq!(r1080(3).calculate_costs().kinovi_credits, 270); - assert_eq!(r1080(4).calculate_costs().kinovi_credits, 360); - assert_eq!(r1080(5).calculate_costs().kinovi_credits, 450); - assert_eq!(r1080(6).calculate_costs().kinovi_credits, 540); - assert_eq!(r1080(7).calculate_costs().kinovi_credits, 630); - assert_eq!(r1080(8).calculate_costs().kinovi_credits, 720); - assert_eq!(r1080(9).calculate_costs().kinovi_credits, 810); - assert_eq!(r1080(10).calculate_costs().kinovi_credits, 900); - assert_eq!(r1080(11).calculate_costs().kinovi_credits, 990); - assert_eq!(r1080(12).calculate_costs().kinovi_credits, 1080); - assert_eq!(r1080(13).calculate_costs().kinovi_credits, 1170); - assert_eq!(r1080(14).calculate_costs().kinovi_credits, 1260); - assert_eq!(r1080(15).calculate_costs().kinovi_credits, 1350); + assert_eq!(r1080(3).calculate_costs().total_cost.kinovi_credits, 270); + assert_eq!(r1080(4).calculate_costs().total_cost.kinovi_credits, 360); + assert_eq!(r1080(5).calculate_costs().total_cost.kinovi_credits, 450); + assert_eq!(r1080(6).calculate_costs().total_cost.kinovi_credits, 540); + assert_eq!(r1080(7).calculate_costs().total_cost.kinovi_credits, 630); + assert_eq!(r1080(8).calculate_costs().total_cost.kinovi_credits, 720); + assert_eq!(r1080(9).calculate_costs().total_cost.kinovi_credits, 810); + assert_eq!(r1080(10).calculate_costs().total_cost.kinovi_credits, 900); + assert_eq!(r1080(11).calculate_costs().total_cost.kinovi_credits, 990); + assert_eq!(r1080(12).calculate_costs().total_cost.kinovi_credits, 1080); + assert_eq!(r1080(13).calculate_costs().total_cost.kinovi_credits, 1170); + assert_eq!(r1080(14).calculate_costs().total_cost.kinovi_credits, 1260); + assert_eq!(r1080(15).calculate_costs().total_cost.kinovi_credits, 1350); } } @@ -345,39 +519,39 @@ mod tests { #[test] fn batch_1_is_base() { - let base = r720(5).calculate_costs().kinovi_credits; - let explicit = build_request(5, None, Some(KinoviSeedance2p0BatchCount::One)).calculate_costs().kinovi_credits; + let base = r720(5).calculate_costs().total_cost.kinovi_credits; + let explicit = build_request(5, None, Some(KinoviSeedance2p0BatchCount::One)).calculate_costs().total_cost.kinovi_credits; assert_eq!(base, explicit); } #[test] fn batch_2_doubles() { - let base = r720(5).calculate_costs().kinovi_credits; - let batch2 = build_request(5, None, Some(KinoviSeedance2p0BatchCount::Two)).calculate_costs().kinovi_credits; + let base = r720(5).calculate_costs().total_cost.kinovi_credits; + let batch2 = build_request(5, None, Some(KinoviSeedance2p0BatchCount::Two)).calculate_costs().total_cost.kinovi_credits; assert_eq!(batch2, base * 2); } #[test] fn batch_4_quadruples() { - let base = r720(5).calculate_costs().kinovi_credits; - let batch4 = build_request(5, None, Some(KinoviSeedance2p0BatchCount::Four)).calculate_costs().kinovi_credits; + let base = r720(5).calculate_costs().total_cost.kinovi_credits; + let batch4 = build_request(5, None, Some(KinoviSeedance2p0BatchCount::Four)).calculate_costs().total_cost.kinovi_credits; assert_eq!(batch4, base * 4); } #[test] fn batch_multiplier_applies_to_1080p() { - let base = r1080(5).calculate_costs().kinovi_credits; - let batch2 = build_request(5, Some(KinoviSeedance2p0OutputResolution::TenEightyP), Some(KinoviSeedance2p0BatchCount::Two)).calculate_costs().kinovi_credits; - let batch4 = build_request(5, Some(KinoviSeedance2p0OutputResolution::TenEightyP), Some(KinoviSeedance2p0BatchCount::Four)).calculate_costs().kinovi_credits; + let base = r1080(5).calculate_costs().total_cost.kinovi_credits; + let batch2 = build_request(5, Some(KinoviSeedance2p0OutputResolution::TenEightyP), Some(KinoviSeedance2p0BatchCount::Two)).calculate_costs().total_cost.kinovi_credits; + let batch4 = build_request(5, Some(KinoviSeedance2p0OutputResolution::TenEightyP), Some(KinoviSeedance2p0BatchCount::Four)).calculate_costs().total_cost.kinovi_credits; assert_eq!(batch2, base * 2); assert_eq!(batch4, base * 4); } #[test] fn batch_multiplier_applies_to_480p() { - let base = r480(5).calculate_costs().kinovi_credits; - let batch2 = build_request(5, Some(KinoviSeedance2p0OutputResolution::FourEightyP), Some(KinoviSeedance2p0BatchCount::Two)).calculate_costs().kinovi_credits; - let batch4 = build_request(5, Some(KinoviSeedance2p0OutputResolution::FourEightyP), Some(KinoviSeedance2p0BatchCount::Four)).calculate_costs().kinovi_credits; + let base = r480(5).calculate_costs().total_cost.kinovi_credits; + let batch2 = build_request(5, Some(KinoviSeedance2p0OutputResolution::FourEightyP), Some(KinoviSeedance2p0BatchCount::Two)).calculate_costs().total_cost.kinovi_credits; + let batch4 = build_request(5, Some(KinoviSeedance2p0OutputResolution::FourEightyP), Some(KinoviSeedance2p0BatchCount::Four)).calculate_costs().total_cost.kinovi_credits; assert_eq!(batch2, base * 2); assert_eq!(batch4, base * 4); } @@ -390,9 +564,9 @@ mod tests { #[test] fn cost_scales_with_duration() { - let c3 = r720(3).calculate_costs().kinovi_credits; - let c10 = r720(10).calculate_costs().kinovi_credits; - let c15 = r720(15).calculate_costs().kinovi_credits; + let c3 = r720(3).calculate_costs().total_cost.kinovi_credits; + let c10 = r720(10).calculate_costs().total_cost.kinovi_credits; + let c15 = r720(15).calculate_costs().total_cost.kinovi_credits; assert!(c3 < c10); assert!(c10 < c15); } @@ -400,9 +574,9 @@ mod tests { #[test] fn resolution_ordering() { for dur in 3..=15u8 { - let c480 = build_request(dur, Some(KinoviSeedance2p0OutputResolution::FourEightyP), None).calculate_costs().kinovi_credits; - let c720 = build_request(dur, None, None).calculate_costs().kinovi_credits; - let c1080 = build_request(dur, Some(KinoviSeedance2p0OutputResolution::TenEightyP), None).calculate_costs().kinovi_credits; + let c480 = build_request(dur, Some(KinoviSeedance2p0OutputResolution::FourEightyP), None).calculate_costs().total_cost.kinovi_credits; + let c720 = build_request(dur, None, None).calculate_costs().total_cost.kinovi_credits; + let c1080 = build_request(dur, Some(KinoviSeedance2p0OutputResolution::TenEightyP), None).calculate_costs().total_cost.kinovi_credits; assert!(c480 < c720, "480p should be cheaper than 720p at {}s", dur); assert!(c720 < c1080, "720p should be cheaper than 1080p at {}s", dur); } @@ -418,50 +592,74 @@ mod tests { /// (USD cents are always rounded UP when fractional.) #[test] fn costs_480p_5s() { - // 15 credits/s × 5s = 75 credits; 7500/193 = 38.86 → 39¢ + // 15 credits/s × 5s = 75 credits; 7500/243 = 30.86 → 31¢ let costs = r480(5).calculate_costs(); - assert_eq!(costs.kinovi_credits, 75); - assert_eq!(costs.usd_cents_rounded_up, 39); + assert_eq!(costs.total_cost.kinovi_credits, 75); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 31); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 30); + assert!((costs.total_cost.usd_cents_fractional - (7500.0 / 243.0)).abs() < 1e-9); + assert_eq!(costs.base_cost.kinovi_credits, 75); + assert!(costs.video_reference_surcharge_cost.is_none()); } #[test] fn costs_720p_5s() { - // 40 credits/s × 5s = 200 credits; 20000/193 = 103.63 → 104¢ + // 40 credits/s × 5s = 200 credits; 20000/243 = 82.30 → 83¢ let costs = r720(5).calculate_costs(); - assert_eq!(costs.kinovi_credits, 200); - assert_eq!(costs.usd_cents_rounded_up, 104); + assert_eq!(costs.total_cost.kinovi_credits, 200); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 83); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 82); + assert!((costs.total_cost.usd_cents_fractional - (20000.0 / 243.0)).abs() < 1e-9); + assert_eq!(costs.base_cost.kinovi_credits, 200); + assert!(costs.video_reference_surcharge_cost.is_none()); } #[test] fn costs_1080p_5s() { - // 90 credits/s × 5s = 450 credits; 45000/193 = 233.16 → rounds UP to 234¢ + // 90 credits/s × 5s = 450 credits; 45000/243 = 185.19 → rounds UP to 186¢ let costs = r1080(5).calculate_costs(); - assert_eq!(costs.kinovi_credits, 450); - assert_eq!(costs.usd_cents_rounded_up, 234); + assert_eq!(costs.total_cost.kinovi_credits, 450); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 186); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 185); + assert!((costs.total_cost.usd_cents_fractional - (45000.0 / 243.0)).abs() < 1e-9); + assert_eq!(costs.base_cost.kinovi_credits, 450); + assert!(costs.video_reference_surcharge_cost.is_none()); } #[test] fn costs_720p_15s() { - // 40 credits/s × 15s = 600 credits; 60000/193 = 310.88 → 311¢ + // 40 credits/s × 15s = 600 credits; 60000/243 = 246.91 → 247¢ let costs = r720(15).calculate_costs(); - assert_eq!(costs.kinovi_credits, 600); - assert_eq!(costs.usd_cents_rounded_up, 311); + assert_eq!(costs.total_cost.kinovi_credits, 600); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 247); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 246); + assert!((costs.total_cost.usd_cents_fractional - (60000.0 / 243.0)).abs() < 1e-9); + assert_eq!(costs.base_cost.kinovi_credits, 600); + assert!(costs.video_reference_surcharge_cost.is_none()); } #[test] fn costs_1080p_15s() { - // 90 credits/s × 15s = 1350 credits; 135000/193 = 699.48 → rounds UP to 700¢ + // 90 credits/s × 15s = 1350 credits; 135000/243 = 555.56 → rounds UP to 556¢ let costs = r1080(15).calculate_costs(); - assert_eq!(costs.kinovi_credits, 1350); - assert_eq!(costs.usd_cents_rounded_up, 700); + assert_eq!(costs.total_cost.kinovi_credits, 1350); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 556); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 555); + assert!((costs.total_cost.usd_cents_fractional - (135000.0 / 243.0)).abs() < 1e-9); + assert_eq!(costs.base_cost.kinovi_credits, 1350); + assert!(costs.video_reference_surcharge_cost.is_none()); } #[test] fn costs_batch_4_720p_5s() { - // 200 credits × 4 = 800 credits; 80000/193 = 414.51 → 415¢ + // 200 credits × 4 = 800 credits; 80000/243 = 329.22 → 330¢ let costs = build_request(5, None, Some(KinoviSeedance2p0BatchCount::Four)).calculate_costs(); - assert_eq!(costs.kinovi_credits, 800); - assert_eq!(costs.usd_cents_rounded_up, 415); + assert_eq!(costs.total_cost.kinovi_credits, 800); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 330); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 329); + assert!((costs.total_cost.usd_cents_fractional - (80000.0 / 243.0)).abs() < 1e-9); + assert_eq!(costs.base_cost.kinovi_credits, 800); + assert!(costs.video_reference_surcharge_cost.is_none()); } /// The deprecated shims return the corresponding struct fields. @@ -470,8 +668,8 @@ mod tests { fn deprecated_methods_delegate() { let request = r720(5); let costs = request.calculate_costs(); - assert_eq!(u64::from(request.estimate_credits()), costs.kinovi_credits); - assert_eq!(request.estimate_cost_in_usd_cents(), costs.usd_cents_rounded_up); + assert_eq!(u64::from(request.estimate_credits()), costs.total_cost.kinovi_credits); + assert_eq!(request.estimate_cost_in_usd_cents(), costs.total_cost.usd_cents_rounded_up); } } @@ -489,47 +687,74 @@ mod tests { } /// The full base + surcharge table from Kinovi's pricing page - /// ("With Video Uploads", 10 sec video ref). + /// ("With Video Uploads", 10 sec video ref). Asserts every field: + /// base, surcharge, and the derived total. #[test] fn kinovi_pricing_table_with_video_reference() { - let cases: &[(fn(u8) -> GenerateSeedance2p0Request, u8, u64)] = &[ + // (request, duration, base credits, surcharge credits) + let cases: &[(fn(u8) -> GenerateSeedance2p0Request, u8, u64, u64)] = &[ // 480p: base + 4/s - (r480, 4, 60 + 16), - (r480, 5, 75 + 20), - (r480, 10, 150 + 40), - (r480, 15, 225 + 60), + (r480, 4, 60, 16), + (r480, 5, 75, 20), + (r480, 10, 150, 40), + (r480, 15, 225, 60), // 720p: base + 8/s - (r720, 4, 160 + 32), - (r720, 5, 200 + 40), - (r720, 10, 400 + 80), - (r720, 15, 600 + 120), + (r720, 4, 160, 32), + (r720, 5, 200, 40), + (r720, 10, 400, 80), + (r720, 15, 600, 120), // 1080p: base + 18/s - (r1080, 4, 360 + 72), - (r1080, 5, 450 + 90), - (r1080, 10, 900 + 180), - (r1080, 15, 1350 + 270), + (r1080, 4, 360, 72), + (r1080, 5, 450, 90), + (r1080, 10, 900, 180), + (r1080, 15, 1350, 270), ]; - for (make, duration, expected_credits) in cases { + for (make, duration, base, surcharge) in cases { let costs = with_video_ref(make(*duration)).calculate_costs(); - assert_eq!(costs.kinovi_credits, *expected_credits, - "duration {duration}s should cost {expected_credits} credits with a video reference"); + assert_eq!(costs.base_cost.kinovi_credits, *base, "base for {duration}s"); + assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(*surcharge), "surcharge for {duration}s"); + assert_eq!(costs.total_cost.kinovi_credits, base + surcharge, "total for {duration}s"); } } #[test] fn surcharge_includes_usd_cents() { - // 720p 5s + video ref = 240 credits; 24000/193 = 124.35 → 125¢ + // 720p 5s + video ref = 200 + 40 = 240 credits; 24000/243 = 98.77 → 99¢ let costs = with_video_ref(r720(5)).calculate_costs(); - assert_eq!(costs.kinovi_credits, 240); - assert_eq!(costs.usd_cents_rounded_up, 125); + assert_eq!(costs.base_cost.kinovi_credits, 200); + assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(40)); + assert_eq!(costs.total_cost.kinovi_credits, 240); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 99); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 98); + assert!((costs.total_cost.usd_cents_fractional - (24000.0 / 243.0)).abs() < 1e-9); + } + + /// The base and surcharge parts each carry their own USD conversions. + #[test] + fn parts_have_their_own_usd_conversions() { + let costs = with_video_ref(r720(5)).calculate_costs(); + + // Base: 200 credits; 20000/243 = 82.30. + assert_eq!(costs.base_cost.usd_cents_rounded_up, 83); + assert_eq!(costs.base_cost.usd_cents_rounded_down, 82); + assert!((costs.base_cost.usd_cents_fractional - (20000.0 / 243.0)).abs() < 1e-9); + + // Surcharge: 40 credits; 4000/243 = 16.46. + let surcharge = costs.video_reference_surcharge_cost.expect("should have surcharge"); + assert_eq!(surcharge.kinovi_credits, 40); + assert_eq!(surcharge.usd_cents_rounded_up, 17); + assert_eq!(surcharge.usd_cents_rounded_down, 16); + assert!((surcharge.usd_cents_fractional - (4000.0 / 243.0)).abs() < 1e-9); } #[test] fn empty_video_reference_list_has_no_surcharge() { let mut request = r720(5); request.reference_video_urls = Some(vec![]); - assert_eq!(request.calculate_costs().kinovi_credits, 200); + let costs = request.calculate_costs(); + assert_eq!(costs.total_cost.kinovi_credits, 200); + assert!(costs.video_reference_surcharge_cost.is_none()); } /// Surcharge is flat per generation regardless of how many reference @@ -541,7 +766,7 @@ mod tests { "https://example.com/a.mp4".to_string(), "https://example.com/b.mp4".to_string(), ]); - assert_eq!(request.calculate_costs().kinovi_credits, 240); + assert_eq!(request.calculate_costs().total_cost.kinovi_credits, 240); } /// The surcharge applies per generated video, so batches multiply it. @@ -549,7 +774,10 @@ mod tests { fn batch_multiplies_surcharge() { let request = with_video_ref(build_request(5, None, Some(KinoviSeedance2p0BatchCount::Two))); // (200 base + 40 surcharge) × 2 = 480 credits - assert_eq!(request.calculate_costs().kinovi_credits, 480); + let costs = request.calculate_costs(); + assert_eq!(costs.base_cost.kinovi_credits, 400); + assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(80)); + assert_eq!(costs.total_cost.kinovi_credits, 480); } } @@ -557,7 +785,7 @@ mod tests { #[test] fn aspect_ratio_does_not_affect_credits() { - let baseline = r720(5).calculate_costs().kinovi_credits; + let baseline = r720(5).calculate_costs().total_cost.kinovi_credits; let ratios = [ KinoviSeedance2p0AspectRatio::Landscape16x9, @@ -584,7 +812,7 @@ mod tests { use_face_blur_hack: None, }; assert_eq!( - req.calculate_costs().kinovi_credits, baseline, + req.calculate_costs().total_cost.kinovi_credits, baseline, "Aspect ratio {:?} should not change credits from baseline {}", ar, baseline, ); } diff --git a/crates/api_clients/seedance2pro_client/src/generate/video/generate_seedance_2p0_fast.rs b/crates/api_clients/seedance2pro_client/src/generate/video/generate_seedance_2p0_fast.rs index 17735724bd..b12d9b05cd 100644 --- a/crates/api_clients/seedance2pro_client/src/generate/video/generate_seedance_2p0_fast.rs +++ b/crates/api_clients/seedance2pro_client/src/generate/video/generate_seedance_2p0_fast.rs @@ -1,6 +1,6 @@ use crate::creds::seedance2pro_session::Seedance2ProSession; use crate::error::seedance2pro_error::Seedance2ProError; -use crate::generate::cost::KinoviGenerationCost; +use crate::cost::kinovi_seedance_generation_cost::KinoviSeedanceGenerationCost; use crate::requests::kinovi_host::KinoviHost; use crate::requests::workflow_run_task::workflow_run_task::{ workflow_run_task, KinoviAspectRatioRaw, KinoviBatchCountRaw, @@ -70,7 +70,7 @@ pub enum KinoviSeedance2p0FastBatchCount { // // Default resolution (None) is 720p. // Batch count multiplies the total cost. -// Credit package: 22,000 credits for $114 (~192.98 credits/$1, rounded to 193). +// Credit package: 525,000 credits for $2,159.0909 (~243.16 credits/$1, rounded down to 243). impl GenerateSeedance2p0FastRequest { /// Calculate the cost of this generation request, in Kinovi credits and @@ -78,7 +78,7 @@ impl GenerateSeedance2p0FastRequest { /// /// Attaching reference VIDEOS adds a per-output-second surcharge (see the /// pricing table below). Reference images and audio are free. - pub fn calculate_costs(&self) -> KinoviGenerationCost { + pub fn calculate_costs(&self) -> KinoviSeedanceGenerationCost { let credits_per_second: u64 = match self.output_resolution { Some(KinoviSeedance2p0FastOutputResolution::FourEightyP) => 14, Some(KinoviSeedance2p0FastOutputResolution::SevenTwentyP) | None => 28, @@ -103,15 +103,24 @@ impl GenerateSeedance2p0FastRequest { 0 }; - let per_video = u64::from(self.duration_seconds) - * (credits_per_second + video_reference_surcharge_per_second); let batch_multiplier: u64 = match self.batch_count { None | Some(KinoviSeedance2p0FastBatchCount::One) => 1, Some(KinoviSeedance2p0FastBatchCount::Two) => 2, Some(KinoviSeedance2p0FastBatchCount::Four) => 4, }; - KinoviGenerationCost::from_kinovi_credits(per_video * batch_multiplier) + let duration = u64::from(self.duration_seconds); + let base_credits = duration * credits_per_second * batch_multiplier; + let maybe_video_reference_surcharge_credits = if self.has_video_reference() { + Some(duration * video_reference_surcharge_per_second * batch_multiplier) + } else { + None + }; + + KinoviSeedanceGenerationCost::from_base_and_surcharge( + base_credits, + maybe_video_reference_surcharge_credits, + ) } fn has_video_reference(&self) -> bool { @@ -123,14 +132,14 @@ impl GenerateSeedance2p0FastRequest { /// Estimate the credit cost for this generation request. #[deprecated(note = "Use calculate_costs() instead")] pub fn estimate_credits(&self) -> u32 { - self.calculate_costs().kinovi_credits as u32 + self.calculate_costs().total_cost.kinovi_credits as u32 } /// Estimate the USD cost in cents for this generation request. /// NB: Rounds UP fractional cents (the historical behavior rounded to nearest). #[deprecated(note = "Use calculate_costs() instead")] pub fn estimate_cost_in_usd_cents(&self) -> u64 { - self.calculate_costs().usd_cents_rounded_up + self.calculate_costs().total_cost.usd_cents_rounded_up } } @@ -252,6 +261,121 @@ mod tests { build_request(dur, None, None) } + // ── Comprehensive per-resolution coverage ── + // + // Seedance 2.0 Fast: 480p 14/s (+4/s video ref), 720p 28/s (+6/s). No 1080p. + // Every field and subfield is asserted: total_cost, base_cost, + // video_reference_surcharge_cost × (kinovi_credits, usd_cents_rounded_up, + // usd_cents_rounded_down, usd_cents_fractional). All cases use a 5 second + // duration with no batching. + + mod comprehensive { + use super::*; + + const FLOAT_TOLERANCE: f64 = 1e-9; + + + mod resolution_480p { + use super::*; + + #[test] + fn test_without_reference_video() { + let costs = r480(5).calculate_costs(); + + // Base: 14 credits/s × 5s = 70 credits; 7000/243 = 28.8066. + assert_eq!(costs.base_cost.kinovi_credits, 70); + assert_eq!(costs.base_cost.usd_cents_rounded_up, 29); + assert_eq!(costs.base_cost.usd_cents_rounded_down, 28); + assert!((costs.base_cost.usd_cents_fractional - (7000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // No reference video: no surcharge, and the total equals the base. + assert!(costs.video_reference_surcharge_cost.is_none()); + + assert_eq!(costs.total_cost.kinovi_credits, 70); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 29); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 28); + assert!((costs.total_cost.usd_cents_fractional - (7000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + } + + #[test] + fn test_with_reference_video() { + let mut request = r480(5); + request.reference_video_urls = Some(vec!["https://example.com/ref.mp4".to_string()]); + let costs = request.calculate_costs(); + + // Base: 14 credits/s × 5s = 70 credits; 7000/243 = 28.8066. + assert_eq!(costs.base_cost.kinovi_credits, 70); + assert_eq!(costs.base_cost.usd_cents_rounded_up, 29); + assert_eq!(costs.base_cost.usd_cents_rounded_down, 28); + assert!((costs.base_cost.usd_cents_fractional - (7000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // Surcharge: 4 credits/s × 5s = 20 credits; 2000/243 = 8.2305. + let surcharge = costs.video_reference_surcharge_cost.expect("should have surcharge"); + assert_eq!(surcharge.kinovi_credits, 20); + assert_eq!(surcharge.usd_cents_rounded_up, 9); + assert_eq!(surcharge.usd_cents_rounded_down, 8); + assert!((surcharge.usd_cents_fractional - (2000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // Total: 70 + 20 = 90 credits; 9000/243 = 37.0370. + assert_eq!(costs.total_cost.kinovi_credits, 90); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 38); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 37); + assert!((costs.total_cost.usd_cents_fractional - (9000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + } + } + + + mod resolution_720p { + use super::*; + + #[test] + fn test_without_reference_video() { + let costs = r720(5).calculate_costs(); + + // Base: 28 credits/s × 5s = 140 credits; 14000/243 = 57.6132. + assert_eq!(costs.base_cost.kinovi_credits, 140); + assert_eq!(costs.base_cost.usd_cents_rounded_up, 58); + assert_eq!(costs.base_cost.usd_cents_rounded_down, 57); + assert!((costs.base_cost.usd_cents_fractional - (14000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // No reference video: no surcharge, and the total equals the base. + assert!(costs.video_reference_surcharge_cost.is_none()); + + assert_eq!(costs.total_cost.kinovi_credits, 140); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 58); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 57); + assert!((costs.total_cost.usd_cents_fractional - (14000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + } + + #[test] + fn test_with_reference_video() { + let mut request = r720(5); + request.reference_video_urls = Some(vec!["https://example.com/ref.mp4".to_string()]); + let costs = request.calculate_costs(); + + // Base: 28 credits/s × 5s = 140 credits; 14000/243 = 57.6132. + assert_eq!(costs.base_cost.kinovi_credits, 140); + assert_eq!(costs.base_cost.usd_cents_rounded_up, 58); + assert_eq!(costs.base_cost.usd_cents_rounded_down, 57); + assert!((costs.base_cost.usd_cents_fractional - (14000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // Surcharge: 6 credits/s × 5s = 30 credits; 3000/243 = 12.3457. + let surcharge = costs.video_reference_surcharge_cost.expect("should have surcharge"); + assert_eq!(surcharge.kinovi_credits, 30); + assert_eq!(surcharge.usd_cents_rounded_up, 13); + assert_eq!(surcharge.usd_cents_rounded_down, 12); + assert!((surcharge.usd_cents_fractional - (3000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + + // Total: 140 + 30 = 170 credits; 17000/243 = 69.9588. + assert_eq!(costs.total_cost.kinovi_credits, 170); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 70); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 69); + assert!((costs.total_cost.usd_cents_fractional - (17000.0 / 243.0)).abs() < FLOAT_TOLERANCE); + } + } + + } + // ── 480p credits (14 credits/sec) ── mod credits_480p { @@ -259,19 +383,19 @@ mod tests { #[test] fn every_duration() { - assert_eq!(r480(3).calculate_costs().kinovi_credits, 42); - assert_eq!(r480(4).calculate_costs().kinovi_credits, 56); - assert_eq!(r480(5).calculate_costs().kinovi_credits, 70); - assert_eq!(r480(6).calculate_costs().kinovi_credits, 84); - assert_eq!(r480(7).calculate_costs().kinovi_credits, 98); - assert_eq!(r480(8).calculate_costs().kinovi_credits, 112); - assert_eq!(r480(9).calculate_costs().kinovi_credits, 126); - assert_eq!(r480(10).calculate_costs().kinovi_credits, 140); - assert_eq!(r480(11).calculate_costs().kinovi_credits, 154); - assert_eq!(r480(12).calculate_costs().kinovi_credits, 168); - assert_eq!(r480(13).calculate_costs().kinovi_credits, 182); - assert_eq!(r480(14).calculate_costs().kinovi_credits, 196); - assert_eq!(r480(15).calculate_costs().kinovi_credits, 210); + assert_eq!(r480(3).calculate_costs().total_cost.kinovi_credits, 42); + assert_eq!(r480(4).calculate_costs().total_cost.kinovi_credits, 56); + assert_eq!(r480(5).calculate_costs().total_cost.kinovi_credits, 70); + assert_eq!(r480(6).calculate_costs().total_cost.kinovi_credits, 84); + assert_eq!(r480(7).calculate_costs().total_cost.kinovi_credits, 98); + assert_eq!(r480(8).calculate_costs().total_cost.kinovi_credits, 112); + assert_eq!(r480(9).calculate_costs().total_cost.kinovi_credits, 126); + assert_eq!(r480(10).calculate_costs().total_cost.kinovi_credits, 140); + assert_eq!(r480(11).calculate_costs().total_cost.kinovi_credits, 154); + assert_eq!(r480(12).calculate_costs().total_cost.kinovi_credits, 168); + assert_eq!(r480(13).calculate_costs().total_cost.kinovi_credits, 182); + assert_eq!(r480(14).calculate_costs().total_cost.kinovi_credits, 196); + assert_eq!(r480(15).calculate_costs().total_cost.kinovi_credits, 210); } } @@ -282,25 +406,25 @@ mod tests { #[test] fn every_duration() { - assert_eq!(r720(3).calculate_costs().kinovi_credits, 84); - assert_eq!(r720(4).calculate_costs().kinovi_credits, 112); - assert_eq!(r720(5).calculate_costs().kinovi_credits, 140); - assert_eq!(r720(6).calculate_costs().kinovi_credits, 168); - assert_eq!(r720(7).calculate_costs().kinovi_credits, 196); - assert_eq!(r720(8).calculate_costs().kinovi_credits, 224); - assert_eq!(r720(9).calculate_costs().kinovi_credits, 252); - assert_eq!(r720(10).calculate_costs().kinovi_credits, 280); - assert_eq!(r720(11).calculate_costs().kinovi_credits, 308); - assert_eq!(r720(12).calculate_costs().kinovi_credits, 336); - assert_eq!(r720(13).calculate_costs().kinovi_credits, 364); - assert_eq!(r720(14).calculate_costs().kinovi_credits, 392); - assert_eq!(r720(15).calculate_costs().kinovi_credits, 420); + assert_eq!(r720(3).calculate_costs().total_cost.kinovi_credits, 84); + assert_eq!(r720(4).calculate_costs().total_cost.kinovi_credits, 112); + assert_eq!(r720(5).calculate_costs().total_cost.kinovi_credits, 140); + assert_eq!(r720(6).calculate_costs().total_cost.kinovi_credits, 168); + assert_eq!(r720(7).calculate_costs().total_cost.kinovi_credits, 196); + assert_eq!(r720(8).calculate_costs().total_cost.kinovi_credits, 224); + assert_eq!(r720(9).calculate_costs().total_cost.kinovi_credits, 252); + assert_eq!(r720(10).calculate_costs().total_cost.kinovi_credits, 280); + assert_eq!(r720(11).calculate_costs().total_cost.kinovi_credits, 308); + assert_eq!(r720(12).calculate_costs().total_cost.kinovi_credits, 336); + assert_eq!(r720(13).calculate_costs().total_cost.kinovi_credits, 364); + assert_eq!(r720(14).calculate_costs().total_cost.kinovi_credits, 392); + assert_eq!(r720(15).calculate_costs().total_cost.kinovi_credits, 420); } #[test] fn explicit_720p_same_as_default() { - let default = r720(5).calculate_costs().kinovi_credits; - let explicit = build_request(5, Some(KinoviSeedance2p0FastOutputResolution::SevenTwentyP), None).calculate_costs().kinovi_credits; + let default = r720(5).calculate_costs().total_cost.kinovi_credits; + let explicit = build_request(5, Some(KinoviSeedance2p0FastOutputResolution::SevenTwentyP), None).calculate_costs().total_cost.kinovi_credits; assert_eq!(default, explicit); } } @@ -312,30 +436,30 @@ mod tests { #[test] fn batch_1_is_base() { - let base = r720(5).calculate_costs().kinovi_credits; - let explicit = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::One)).calculate_costs().kinovi_credits; + let base = r720(5).calculate_costs().total_cost.kinovi_credits; + let explicit = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::One)).calculate_costs().total_cost.kinovi_credits; assert_eq!(base, explicit); } #[test] fn batch_2_doubles() { - let base = r720(5).calculate_costs().kinovi_credits; - let batch2 = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Two)).calculate_costs().kinovi_credits; + let base = r720(5).calculate_costs().total_cost.kinovi_credits; + let batch2 = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Two)).calculate_costs().total_cost.kinovi_credits; assert_eq!(batch2, base * 2); } #[test] fn batch_4_quadruples() { - let base = r720(5).calculate_costs().kinovi_credits; - let batch4 = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Four)).calculate_costs().kinovi_credits; + let base = r720(5).calculate_costs().total_cost.kinovi_credits; + let batch4 = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Four)).calculate_costs().total_cost.kinovi_credits; assert_eq!(batch4, base * 4); } #[test] fn batch_multiplier_applies_to_480p() { - let base = r480(5).calculate_costs().kinovi_credits; - let batch2 = build_request(5, Some(KinoviSeedance2p0FastOutputResolution::FourEightyP), Some(KinoviSeedance2p0FastBatchCount::Two)).calculate_costs().kinovi_credits; - let batch4 = build_request(5, Some(KinoviSeedance2p0FastOutputResolution::FourEightyP), Some(KinoviSeedance2p0FastBatchCount::Four)).calculate_costs().kinovi_credits; + let base = r480(5).calculate_costs().total_cost.kinovi_credits; + let batch2 = build_request(5, Some(KinoviSeedance2p0FastOutputResolution::FourEightyP), Some(KinoviSeedance2p0FastBatchCount::Two)).calculate_costs().total_cost.kinovi_credits; + let batch4 = build_request(5, Some(KinoviSeedance2p0FastOutputResolution::FourEightyP), Some(KinoviSeedance2p0FastBatchCount::Four)).calculate_costs().total_cost.kinovi_credits; assert_eq!(batch2, base * 2); assert_eq!(batch4, base * 4); } @@ -348,9 +472,9 @@ mod tests { #[test] fn cost_scales_with_duration() { - let c3 = r720(3).calculate_costs().kinovi_credits; - let c10 = r720(10).calculate_costs().kinovi_credits; - let c15 = r720(15).calculate_costs().kinovi_credits; + let c3 = r720(3).calculate_costs().total_cost.kinovi_credits; + let c10 = r720(10).calculate_costs().total_cost.kinovi_credits; + let c15 = r720(15).calculate_costs().total_cost.kinovi_credits; assert!(c3 < c10); assert!(c10 < c15); } @@ -358,8 +482,8 @@ mod tests { #[test] fn resolution_ordering() { for dur in 3..=15u8 { - let c480 = build_request(dur, Some(KinoviSeedance2p0FastOutputResolution::FourEightyP), None).calculate_costs().kinovi_credits; - let c720 = build_request(dur, None, None).calculate_costs().kinovi_credits; + let c480 = build_request(dur, Some(KinoviSeedance2p0FastOutputResolution::FourEightyP), None).calculate_costs().total_cost.kinovi_credits; + let c720 = build_request(dur, None, None).calculate_costs().total_cost.kinovi_credits; assert!(c480 < c720, "480p should be cheaper than 720p at {}s", dur); } } @@ -374,42 +498,62 @@ mod tests { /// (USD cents are always rounded UP when fractional.) #[test] fn costs_480p_5s() { - // 14 credits/s × 5s = 70 credits; 7000/193 = 36.27 → rounds UP to 37¢ + // 14 credits/s × 5s = 70 credits; 7000/243 = 28.81 → rounds UP to 29¢ let costs = r480(5).calculate_costs(); - assert_eq!(costs.kinovi_credits, 70); - assert_eq!(costs.usd_cents_rounded_up, 37); + assert_eq!(costs.total_cost.kinovi_credits, 70); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 29); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 28); + assert!((costs.total_cost.usd_cents_fractional - (7000.0 / 243.0)).abs() < 1e-9); + assert_eq!(costs.base_cost.kinovi_credits, 70); + assert!(costs.video_reference_surcharge_cost.is_none()); } #[test] fn costs_720p_5s() { - // 28 credits/s × 5s = 140 credits; 14000/193 = 72.54 → 73¢ + // 28 credits/s × 5s = 140 credits; 14000/243 = 57.61 → 58¢ let costs = r720(5).calculate_costs(); - assert_eq!(costs.kinovi_credits, 140); - assert_eq!(costs.usd_cents_rounded_up, 73); + assert_eq!(costs.total_cost.kinovi_credits, 140); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 58); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 57); + assert!((costs.total_cost.usd_cents_fractional - (14000.0 / 243.0)).abs() < 1e-9); + assert_eq!(costs.base_cost.kinovi_credits, 140); + assert!(costs.video_reference_surcharge_cost.is_none()); } #[test] fn costs_720p_15s() { - // 28 credits/s × 15s = 420 credits; 42000/193 = 217.62 → 218¢ + // 28 credits/s × 15s = 420 credits; 42000/243 = 172.84 → 173¢ let costs = r720(15).calculate_costs(); - assert_eq!(costs.kinovi_credits, 420); - assert_eq!(costs.usd_cents_rounded_up, 218); + assert_eq!(costs.total_cost.kinovi_credits, 420); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 173); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 172); + assert!((costs.total_cost.usd_cents_fractional - (42000.0 / 243.0)).abs() < 1e-9); + assert_eq!(costs.base_cost.kinovi_credits, 420); + assert!(costs.video_reference_surcharge_cost.is_none()); } #[test] fn costs_480p_15s() { - // 14 credits/s × 15s = 210 credits; 21000/193 = 108.81 → 109¢ + // 14 credits/s × 15s = 210 credits; 21000/243 = 86.42 → 87¢ let costs = r480(15).calculate_costs(); - assert_eq!(costs.kinovi_credits, 210); - assert_eq!(costs.usd_cents_rounded_up, 109); + assert_eq!(costs.total_cost.kinovi_credits, 210); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 87); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 86); + assert!((costs.total_cost.usd_cents_fractional - (21000.0 / 243.0)).abs() < 1e-9); + assert_eq!(costs.base_cost.kinovi_credits, 210); + assert!(costs.video_reference_surcharge_cost.is_none()); } #[test] fn costs_batch_2_720p_5s() { - // 140 credits × 2 = 280 credits; 28000/193 = 145.07 → 146¢ + // 140 credits × 2 = 280 credits; 28000/243 = 115.23 → 116¢ let costs = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Two)).calculate_costs(); - assert_eq!(costs.kinovi_credits, 280); - assert_eq!(costs.usd_cents_rounded_up, 146); + assert_eq!(costs.total_cost.kinovi_credits, 280); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 116); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 115); + assert!((costs.total_cost.usd_cents_fractional - (28000.0 / 243.0)).abs() < 1e-9); + assert_eq!(costs.base_cost.kinovi_credits, 280); + assert!(costs.video_reference_surcharge_cost.is_none()); } /// The deprecated shims return the corresponding struct fields. @@ -418,8 +562,8 @@ mod tests { fn deprecated_methods_delegate() { let request = r720(5); let costs = request.calculate_costs(); - assert_eq!(u64::from(request.estimate_credits()), costs.kinovi_credits); - assert_eq!(request.estimate_cost_in_usd_cents(), costs.usd_cents_rounded_up); + assert_eq!(u64::from(request.estimate_credits()), costs.total_cost.kinovi_credits); + assert_eq!(request.estimate_cost_in_usd_cents(), costs.total_cost.usd_cents_rounded_up); } } @@ -438,42 +582,69 @@ mod tests { } /// The full base + surcharge table from Kinovi's pricing page - /// ("Seedance 2.0 Fast With Video Ref", 10 sec video ref). + /// ("Seedance 2.0 Fast With Video Ref", 10 sec video ref). Asserts + /// every field: base, surcharge, and the derived total. #[test] fn kinovi_pricing_table_with_video_reference() { - let cases: &[(fn(u8) -> GenerateSeedance2p0FastRequest, u8, u64)] = &[ + // (request, duration, base credits, surcharge credits) + let cases: &[(fn(u8) -> GenerateSeedance2p0FastRequest, u8, u64, u64)] = &[ // 480p: base + 4/s - (r480, 4, 56 + 16), - (r480, 5, 70 + 20), - (r480, 10, 140 + 40), - (r480, 15, 210 + 60), + (r480, 4, 56, 16), + (r480, 5, 70, 20), + (r480, 10, 140, 40), + (r480, 15, 210, 60), // 720p: base + 6/s - (r720, 4, 112 + 24), - (r720, 5, 140 + 30), - (r720, 10, 280 + 60), - (r720, 15, 420 + 90), + (r720, 4, 112, 24), + (r720, 5, 140, 30), + (r720, 10, 280, 60), + (r720, 15, 420, 90), ]; - for (make, duration, expected_credits) in cases { + for (make, duration, base, surcharge) in cases { let costs = with_video_ref(make(*duration)).calculate_costs(); - assert_eq!(costs.kinovi_credits, *expected_credits, - "duration {duration}s should cost {expected_credits} credits with a video reference"); + assert_eq!(costs.base_cost.kinovi_credits, *base, "base for {duration}s"); + assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(*surcharge), "surcharge for {duration}s"); + assert_eq!(costs.total_cost.kinovi_credits, base + surcharge, "total for {duration}s"); } } #[test] fn surcharge_includes_usd_cents() { - // 720p 5s + video ref = 170 credits; 17000/193 = 88.08 → 89¢ + // 720p 5s + video ref = 140 + 30 = 170 credits; 17000/243 = 69.96 → 70¢ let costs = with_video_ref(r720(5)).calculate_costs(); - assert_eq!(costs.kinovi_credits, 170); - assert_eq!(costs.usd_cents_rounded_up, 89); + assert_eq!(costs.base_cost.kinovi_credits, 140); + assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(30)); + assert_eq!(costs.total_cost.kinovi_credits, 170); + assert_eq!(costs.total_cost.usd_cents_rounded_up, 70); + assert_eq!(costs.total_cost.usd_cents_rounded_down, 69); + assert!((costs.total_cost.usd_cents_fractional - (17000.0 / 243.0)).abs() < 1e-9); + } + + /// The base and surcharge parts each carry their own USD conversions. + #[test] + fn parts_have_their_own_usd_conversions() { + let costs = with_video_ref(r720(5)).calculate_costs(); + + // Base: 140 credits; 14000/243 = 57.61. + assert_eq!(costs.base_cost.usd_cents_rounded_up, 58); + assert_eq!(costs.base_cost.usd_cents_rounded_down, 57); + assert!((costs.base_cost.usd_cents_fractional - (14000.0 / 243.0)).abs() < 1e-9); + + // Surcharge: 30 credits; 3000/243 = 12.35. + let surcharge = costs.video_reference_surcharge_cost.expect("should have surcharge"); + assert_eq!(surcharge.kinovi_credits, 30); + assert_eq!(surcharge.usd_cents_rounded_up, 13); + assert_eq!(surcharge.usd_cents_rounded_down, 12); + assert!((surcharge.usd_cents_fractional - (3000.0 / 243.0)).abs() < 1e-9); } #[test] fn empty_video_reference_list_has_no_surcharge() { let mut request = r720(5); request.reference_video_urls = Some(vec![]); - assert_eq!(request.calculate_costs().kinovi_credits, 140); + let costs = request.calculate_costs(); + assert_eq!(costs.total_cost.kinovi_credits, 140); + assert!(costs.video_reference_surcharge_cost.is_none()); } /// Surcharge is flat per generation regardless of how many reference @@ -485,7 +656,7 @@ mod tests { "https://example.com/a.mp4".to_string(), "https://example.com/b.mp4".to_string(), ]); - assert_eq!(request.calculate_costs().kinovi_credits, 170); + assert_eq!(request.calculate_costs().total_cost.kinovi_credits, 170); } /// The surcharge applies per generated video, so batches multiply it. @@ -493,7 +664,10 @@ mod tests { fn batch_multiplies_surcharge() { let request = with_video_ref(build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Two))); // (140 base + 30 surcharge) × 2 = 340 credits - assert_eq!(request.calculate_costs().kinovi_credits, 340); + let costs = request.calculate_costs(); + assert_eq!(costs.base_cost.kinovi_credits, 280); + assert_eq!(costs.video_reference_surcharge_cost.map(|c| c.kinovi_credits), Some(60)); + assert_eq!(costs.total_cost.kinovi_credits, 340); } } @@ -501,7 +675,7 @@ mod tests { #[test] fn aspect_ratio_does_not_affect_credits() { - let baseline = r720(5).calculate_costs().kinovi_credits; + let baseline = r720(5).calculate_costs().total_cost.kinovi_credits; let ratios = [ KinoviSeedance2p0FastAspectRatio::Landscape16x9, @@ -528,7 +702,7 @@ mod tests { use_face_blur_hack: None, }; assert_eq!( - req.calculate_costs().kinovi_credits, baseline, + req.calculate_costs().total_cost.kinovi_credits, baseline, "Aspect ratio {:?} should not change credits from baseline {}", ar, baseline, ); } diff --git a/crates/api_clients/seedance2pro_client/src/lib.rs b/crates/api_clients/seedance2pro_client/src/lib.rs index 2da70d8c5a..8e47c24bdf 100644 --- a/crates/api_clients/seedance2pro_client/src/lib.rs +++ b/crates/api_clients/seedance2pro_client/src/lib.rs @@ -2,6 +2,7 @@ pub (crate) mod test_utils; // User API +pub mod cost; pub mod creds; pub mod error; pub mod generate; diff --git a/crates/api_clients/seedance2pro_client/src/requests/generate_image/generate_image.rs b/crates/api_clients/seedance2pro_client/src/requests/generate_image/generate_image.rs index 2f04883610..83c968e697 100644 --- a/crates/api_clients/seedance2pro_client/src/requests/generate_image/generate_image.rs +++ b/crates/api_clients/seedance2pro_client/src/requests/generate_image/generate_image.rs @@ -1,4 +1,4 @@ -use crate::generate::cost::KinoviGenerationCost; +use crate::cost::kinovi_generation_cost::KinoviGenerationCost; use crate::creds::seedance2pro_session::Seedance2ProSession; use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError; use crate::error::seedance2pro_client_error::Seedance2ProClientError; @@ -22,7 +22,7 @@ const CREDITS_PER_MIDJOURNEY_TASK: u32 = 12; /// Newer Kinovi credit-package rate. Used to convert credits to USD for the /// Midjourney image generation flow. /// -/// 22,000 credits / $114 = 192.98 ≈ 193 credits per dollar. +/// 500,000 credits / $2,159.09 = 231.58 ≈ 243 credits per dollar (rounded down). /// Kinovi only currently exposes a single Midjourney resolution preset. const MIDJOURNEY_RESOLUTION: &str = "1k"; @@ -416,20 +416,26 @@ mod tests { #[test] fn usd_cents_batch_one() { - // 12 / 193 × 100 = 6.21¢ → 6¢ - assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 7); // 1200/193 = 6.22 -> rounds UP + // 12/243 × 100 = 4.94¢ → 5¢ + assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 5); // 1200/243 = 4.94 -> rounds UP + assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_down, 4); + assert!((make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_fractional - (1200.0 / 243.0)).abs() < 1e-9); } #[test] fn usd_cents_batch_two() { - // 24 / 193 × 100 = 12.43¢ → 12¢ - assert_eq!(make_request(KinoviMidjourneyBatchCount::Two).calculate_costs().usd_cents_rounded_up, 13); // 2400/193 = 12.44 -> rounds UP + // 24/243 × 100 = 9.88¢ → 10¢ + assert_eq!(make_request(KinoviMidjourneyBatchCount::Two).calculate_costs().usd_cents_rounded_up, 10); // 2400/243 = 9.88 -> rounds UP + assert_eq!(make_request(KinoviMidjourneyBatchCount::Two).calculate_costs().usd_cents_rounded_down, 9); + assert!((make_request(KinoviMidjourneyBatchCount::Two).calculate_costs().usd_cents_fractional - (2400.0 / 243.0)).abs() < 1e-9); } #[test] fn usd_cents_batch_four() { - // 48 / 193 × 100 = 24.87¢ → 25¢ - assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 25); // 4800/193 = 24.87 -> rounds UP + // 48/243 × 100 = 19.75¢ → 20¢ + assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 20); // 4800/243 = 19.75 -> rounds UP + assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_down, 19); + assert!((make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_fractional - (4800.0 / 243.0)).abs() < 1e-9); } /// Batch 4 should be exactly 4× the credit cost of batch 1, and the USD diff --git a/crates/api_clients/seedance2pro_client/src/requests/generate_video/generate_video.rs b/crates/api_clients/seedance2pro_client/src/requests/generate_video/generate_video.rs index eff36d109f..5661e2be43 100644 --- a/crates/api_clients/seedance2pro_client/src/requests/generate_video/generate_video.rs +++ b/crates/api_clients/seedance2pro_client/src/requests/generate_video/generate_video.rs @@ -141,7 +141,7 @@ impl KinoviGenerateVideoRequest { /// Credits per dollar for billing conversion. /// /// Legacy 720p pricing uses the original Kinovi credit package rates. - /// All other model/resolution combos use the newer rate: 22,000 credits / $114. + /// All other model/resolution combos use the newer rate: 525,000 credits / $2,159.0909. fn credits_per_dollar(&self) -> f64 { match (self.model_type, self.output_resolution) { // Legacy: Seedance 2.0 Pro @ 720p — 25,000 credits for $99.99 @@ -152,8 +152,8 @@ impl KinoviGenerateVideoRequest { (KinoviModelType::Seedance2Fast, None) | (KinoviModelType::Seedance2Fast, Some(KinoviOutputResolution::SevenTwentyP)) => 220.0, - // New pricing: 22,000 credits for $114 (~192.98 credits/$1) - _ => 193.0, + // New pricing: 525,000 credits for $2,159.0909 (~243.16 credits/$1) + _ => 243.0, } } @@ -669,7 +669,7 @@ mod tests { #[test] fn usd_cents_legacy_fast_720p() { - // 220 credits/$1: 140 credits = 63.6 → 64¢ + // 220 credits/$1: 140 credits = 57.61 → 58¢ assert_eq!(fast(5, KinoviBatchCount::One).estimate_cost_in_usd_cents(), 64); assert_eq!(fast(10, KinoviBatchCount::One).estimate_cost_in_usd_cents(), 127); } @@ -678,24 +678,24 @@ mod tests { #[test] fn usd_cents_new_pro_480p() { - // 192.98 credits/$1: 75 credits = 38.86 → 39¢ - assert_eq!(pro_res(5, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).estimate_cost_in_usd_cents(), 39); - assert_eq!(pro_res(10, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).estimate_cost_in_usd_cents(), 78); + // 243 credits/$1: 75 credits = 30.86 → 31¢ + assert_eq!(pro_res(5, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).estimate_cost_in_usd_cents(), 31); + assert_eq!(pro_res(10, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).estimate_cost_in_usd_cents(), 62); } #[test] fn usd_cents_new_pro_1080p() { - // 192.98 credits/$1: 450 credits = 233.18 → 233¢ - assert_eq!(pro_res(5, KinoviBatchCount::One, KinoviOutputResolution::TenEightyP).estimate_cost_in_usd_cents(), 233); - // 900 credits = 466.36 → 466¢ - assert_eq!(pro_res(10, KinoviBatchCount::One, KinoviOutputResolution::TenEightyP).estimate_cost_in_usd_cents(), 466); + // 243 credits/$1: 450 credits = 185.19 → 185¢ + assert_eq!(pro_res(5, KinoviBatchCount::One, KinoviOutputResolution::TenEightyP).estimate_cost_in_usd_cents(), 185); + // 900 credits = 370.37 → 370¢ + assert_eq!(pro_res(10, KinoviBatchCount::One, KinoviOutputResolution::TenEightyP).estimate_cost_in_usd_cents(), 370); } #[test] fn usd_cents_new_fast_480p() { - // 192.98 credits/$1: 50 credits = 25.91 → 26¢ - assert_eq!(fast_res(5, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).estimate_cost_in_usd_cents(), 26); - assert_eq!(fast_res(10, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).estimate_cost_in_usd_cents(), 52); + // 243 credits/$1: 50 credits = 20.58 → 21¢ + assert_eq!(fast_res(5, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).estimate_cost_in_usd_cents(), 21); + assert_eq!(fast_res(10, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).estimate_cost_in_usd_cents(), 41); } // ── credits_per_dollar: verify the rates directly ── @@ -711,11 +711,11 @@ mod tests { #[test] fn credits_per_dollar_new_rate() { - assert_eq!(pro_res(5, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).credits_per_dollar(), 193.0); - assert_eq!(pro_res(5, KinoviBatchCount::One, KinoviOutputResolution::TenEightyP).credits_per_dollar(), 193.0); - assert_eq!(fast_res(5, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).credits_per_dollar(), 193.0); + assert_eq!(pro_res(5, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).credits_per_dollar(), 243.0); + assert_eq!(pro_res(5, KinoviBatchCount::One, KinoviOutputResolution::TenEightyP).credits_per_dollar(), 243.0); + assert_eq!(fast_res(5, KinoviBatchCount::One, KinoviOutputResolution::FourEightyP).credits_per_dollar(), 243.0); // Fast 1080p (not officially supported) also uses new rate - assert_eq!(fast_res(5, KinoviBatchCount::One, KinoviOutputResolution::TenEightyP).credits_per_dollar(), 193.0); + assert_eq!(fast_res(5, KinoviBatchCount::One, KinoviOutputResolution::TenEightyP).credits_per_dollar(), 243.0); } } diff --git a/crates/api_clients/seedance2pro_client/src/requests/workflow_run_task/workflow_run_task.rs b/crates/api_clients/seedance2pro_client/src/requests/workflow_run_task/workflow_run_task.rs index 42d0a1e51c..a481a197c4 100644 --- a/crates/api_clients/seedance2pro_client/src/requests/workflow_run_task/workflow_run_task.rs +++ b/crates/api_clients/seedance2pro_client/src/requests/workflow_run_task/workflow_run_task.rs @@ -150,7 +150,7 @@ impl WorkflowRunTaskRequest { /// Credits per dollar for billing conversion. /// /// Legacy 720p pricing uses the original Kinovi credit package rates. - /// All other model/resolution combos use the newer rate: 22,000 credits / $114. + /// All other model/resolution combos use the newer rate: 525,000 credits / $2,159.0909. fn credits_per_dollar(&self) -> f64 { match (self.model_type, self.output_resolution) { // Legacy: Seedance 2.0 Pro @ 720p — 25,000 credits for $99.99 @@ -161,8 +161,8 @@ impl WorkflowRunTaskRequest { (KinoviModelTypeRaw::Seedance2Fast, None) | (KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::SevenTwentyP)) => 220.0, - // New pricing: 22,000 credits for $114 (~192.98 credits/$1) - _ => 193.0, + // New pricing: 525,000 credits for $2,159.0909 (~243.16 credits/$1) + _ => 243.0, } } @@ -661,20 +661,20 @@ mod tests { #[test] fn usd_cents_new_pro_480p() { - assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 39); - assert_eq!(pro_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 78); + assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 31); + assert_eq!(pro_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 62); } #[test] fn usd_cents_new_pro_1080p() { - assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_cost_in_usd_cents(), 233); - assert_eq!(pro_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_cost_in_usd_cents(), 466); + assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_cost_in_usd_cents(), 185); + assert_eq!(pro_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_cost_in_usd_cents(), 370); } #[test] fn usd_cents_new_fast_480p() { - assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 26); - assert_eq!(fast_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 52); + assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 21); + assert_eq!(fast_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 41); } #[test] @@ -687,10 +687,10 @@ mod tests { #[test] fn credits_per_dollar_new_rate() { - assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).credits_per_dollar(), 193.0); - assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).credits_per_dollar(), 193.0); - assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).credits_per_dollar(), 193.0); - assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).credits_per_dollar(), 193.0); + assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).credits_per_dollar(), 243.0); + assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).credits_per_dollar(), 243.0); + assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).credits_per_dollar(), 243.0); + assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).credits_per_dollar(), 243.0); } // ── Happy Horse 1.0 ── @@ -724,17 +724,17 @@ mod tests { #[test] fn happy_horse_uses_new_pricing_rate() { - // All Happy Horse resolutions use the new 193 credits/$1 rate (not legacy) - assert_eq!(happy_horse(5, KinoviBatchCountRaw::One).credits_per_dollar(), 193.0); - assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::SevenTwentyP).credits_per_dollar(), 193.0); - assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).credits_per_dollar(), 193.0); - assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).credits_per_dollar(), 193.0); + // All Happy Horse resolutions use the new 243 credits/$1 rate (not legacy) + assert_eq!(happy_horse(5, KinoviBatchCountRaw::One).credits_per_dollar(), 243.0); + assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::SevenTwentyP).credits_per_dollar(), 243.0); + assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).credits_per_dollar(), 243.0); + assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).credits_per_dollar(), 243.0); } #[test] fn happy_horse_usd_cents_1080p() { - // 193 credits/$1: 360 credits (4s×90) = 186.53 → 187¢ - assert_eq!(happy_horse_res(4, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_cost_in_usd_cents(), 187); + // 243 credits/$1: 360 credits (4s×90) = 148.15 → 148¢ + assert_eq!(happy_horse_res(4, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_cost_in_usd_cents(), 148); } }