Compare commits

...
Author SHA1 Message Date
Brandon Thomas dfe71177ce pricing 2026-06-24 17:17:20 -04:00
Brandon Thomas 43086c9071 margin pricing 2026-06-24 17:01:19 -04:00
Brandon Thomas f92e4f4836 more tests 2026-06-24 16:23:39 -04:00
Brandon Thomas be3f3ec5f9 pricing alignment 2026-06-24 16:12:25 -04:00
Brandon Thomas 5b4b392416 pricing 2026-06-24 16:02:18 -04:00
Brandon Thomas 3c24333c7b enable 4k in router (artcraft) 2026-06-24 15:30:23 -04:00
Brandon Thomas 88492018dc model list 4k 2026-06-24 13:58:23 -04:00
16 changed files with 569 additions and 411 deletions
@@ -21,6 +21,8 @@ use crate::generate::generate_video::providers::artcraft::resolve::{
pub enum SupportedResolutions {
/// 480p, 720p, 1080p
Full,
/// 480p, 720p, 1080p, 4K
FullWith4k,
/// 480p, 720p only (1080p downgrades to 720p)
Fast,
/// 720p, 1080p only (480p upgrades)
@@ -157,7 +159,7 @@ fn plan_output_resolution(
None => Ok(None),
Some(RouterResolution::FourEightyP) => match supported {
SupportedResolutions::Full | SupportedResolutions::Fast => {
SupportedResolutions::Full | SupportedResolutions::FullWith4k | SupportedResolutions::Fast => {
Ok(Some(CommonResolutionEnum::FourEightyP))
}
SupportedResolutions::NoFourEightyP => match strategy {
@@ -175,7 +177,7 @@ fn plan_output_resolution(
Some(RouterResolution::SevenTwentyP) => Ok(Some(CommonResolutionEnum::SevenTwentyP)),
Some(RouterResolution::TenEightyP) => match supported {
SupportedResolutions::Full | SupportedResolutions::NoFourEightyP => {
SupportedResolutions::Full | SupportedResolutions::FullWith4k | SupportedResolutions::NoFourEightyP => {
Ok(Some(CommonResolutionEnum::TenEightyP))
}
SupportedResolutions::Fast => match strategy {
@@ -189,6 +191,20 @@ fn plan_output_resolution(
},
},
// 4K is only available on models flagged `FullWith4k` (the non-Fast Seedance
// 2.0 family). Other models error out or fall back to their best resolution.
Some(RouterResolution::FourK) => match (supported, strategy) {
(SupportedResolutions::FullWith4k, _) => Ok(Some(CommonResolutionEnum::FourK)),
(_, RequestMismatchMitigationStrategy::ErrorOut) => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "resolution",
value: format!("{:?}", RouterResolution::FourK),
}))
}
(SupportedResolutions::Fast, _) => Ok(Some(CommonResolutionEnum::SevenTwentyP)),
(_, _) => Ok(Some(CommonResolutionEnum::TenEightyP)),
},
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
@@ -1,4 +1,5 @@
pub(crate) mod build_common;
pub(crate) mod seedance_common;
#[cfg(test)]
mod parity_tests;
pub mod grok_imagine_video;
@@ -13,7 +13,7 @@ pub fn build_artcraft_seedance_2p0(builder: GenerateVideoRequestBuilder) -> Resu
let request = build_artcraft_omni_video_request(
builder,
CommonVideoModelEnum::Seedance2p0,
SupportedResolutions::Full,
SupportedResolutions::FullWith4k,
UltraWideSupport::Supported,
)?;
let state = ArtcraftSeedance2p0RequestState { request };
@@ -105,6 +105,12 @@ mod tests {
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::TenEightyP));
}
#[test]
fn res_4k() {
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::FourK); }));
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourK));
}
#[test]
fn none_stays_none() {
let req = unwrap_request(make_builder(|_| {}));
@@ -1,7 +1,8 @@
use enums::common::generation::common_resolution::CommonResolution;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video::providers::artcraft::seedance_common::seedance_2p0_four_k_usd_cents;
use crate::generate::generate_video::providers::artcraft::seedance_2p0::request::ArtcraftSeedance2p0RequestState;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
// ── Pricing constants ──
//
@@ -47,6 +48,23 @@ impl ArtcraftSeedance2p0CostState {
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
if self.resolution == CommonResolution::FourK {
let usd_cents = seedance_2p0_four_k_usd_cents(
self.duration_seconds,
self.batch_count,
self.has_video_reference,
);
return VideoGenerationCostEstimate {
cost_in_credits: Some(usd_cents),
cost_in_usd_cents: Some(usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
};
}
let cents_per_second = match self.resolution {
CommonResolution::FourEightyP => CENTS_PER_SECOND_480P,
CommonResolution::TenEightyP => CENTS_PER_SECOND_1080P,
@@ -71,8 +89,8 @@ impl ArtcraftSeedance2p0CostState {
#[cfg(test)]
mod tests {
use crate::api::router_resolution::RouterResolution;
use crate::api::router_provider::RouterProvider;
use crate::api::router_resolution::RouterResolution;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
// -- 720p pricing --
@@ -157,6 +175,40 @@ mod tests {
}
}
// -- 4K pricing --
mod four_k_pricing {
use enums::common::generation::common_resolution::CommonResolution;
fn artcraft_4k_cents(duration_seconds: u16, batch_count: u16, has_video_reference: bool) -> u64 {
super::super::ArtcraftSeedance2p0CostState {
resolution: CommonResolution::FourK,
duration_seconds,
batch_count,
has_video_reference,
}
.estimate_cost()
.cost_in_usd_cents
.unwrap()
}
#[test]
fn explicit_4k_without_video_reference() {
assert_eq!(artcraft_4k_cents(4, 1, false), 347);
assert_eq!(artcraft_4k_cents(5, 1, false), 433);
assert_eq!(artcraft_4k_cents(10, 1, false), 866);
assert_eq!(artcraft_4k_cents(15, 1, false), 1299);
}
#[test]
fn explicit_4k_with_video_reference() {
assert_eq!(artcraft_4k_cents(4, 1, true), 416);
assert_eq!(artcraft_4k_cents(5, 1, true), 519);
assert_eq!(artcraft_4k_cents(10, 1, true), 1038);
assert_eq!(artcraft_4k_cents(15, 1, true), 1557);
}
}
// -- Relative pricing --
mod relative_pricing_tests {
@@ -171,6 +223,13 @@ mod tests {
assert!(c720 < c1080);
}
#[test]
fn cost_4k_more_expensive_than_1080p() {
let c1080 = cost_cents(Some(RouterResolution::TenEightyP), 5, 1);
let c4k = cost_cents(Some(RouterResolution::FourK), 5, 1);
assert!(c4k > c1080);
}
#[test]
fn cost_scales_with_duration() {
let c4 = cost_cents(Some(RouterResolution::SevenTwentyP), 4, 1);
@@ -201,6 +260,7 @@ mod tests {
Some(RouterResolution::FourEightyP),
Some(RouterResolution::SevenTwentyP),
Some(RouterResolution::TenEightyP),
Some(RouterResolution::FourK),
None,
];
for res in resolutions {
@@ -13,7 +13,7 @@ pub fn build_artcraft_seedance_2p0_bp(builder: GenerateVideoRequestBuilder) -> R
let request = build_artcraft_omni_video_request(
builder,
CommonVideoModelEnum::Seedance2p0BytePlus,
SupportedResolutions::Full,
SupportedResolutions::FullWith4k,
UltraWideSupport::Supported,
)?;
let state = ArtcraftSeedance2p0BytePlusRequestState { request };
@@ -101,6 +101,12 @@ mod tests {
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::TenEightyP));
}
#[test]
fn res_4k() {
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::FourK); }));
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourK));
}
#[test]
fn none_stays_none() {
let req = unwrap_request(make_builder(|_| {}));
@@ -1,7 +1,8 @@
use enums::common::generation::common_resolution::CommonResolution;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video::providers::artcraft::seedance_common::seedance_2p0_four_k_usd_cents;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bp::request::ArtcraftSeedance2p0BytePlusRequestState;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
/// USD cents per second by resolution:
/// 480p: $0.10/s = 10.0 ¢/s
@@ -15,6 +16,7 @@ pub struct ArtcraftSeedance2p0BytePlusCostState {
pub resolution: CommonResolution,
pub duration_seconds: u16,
pub batch_count: u16,
pub has_video_reference: bool,
}
impl ArtcraftSeedance2p0BytePlusCostState {
@@ -23,10 +25,30 @@ impl ArtcraftSeedance2p0BytePlusCostState {
.unwrap_or(CommonResolution::SevenTwentyP);
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
let batch_count = request.request.video_batch_count.unwrap_or(1);
Self { resolution, duration_seconds, batch_count }
let has_video_reference = request.request.reference_video_media_tokens
.as_ref()
.is_some_and(|tokens| !tokens.is_empty());
Self { resolution, duration_seconds, batch_count, has_video_reference }
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
if self.resolution == CommonResolution::FourK {
let usd_cents = seedance_2p0_four_k_usd_cents(
self.duration_seconds,
self.batch_count,
self.has_video_reference,
);
return VideoGenerationCostEstimate {
cost_in_credits: Some(usd_cents),
cost_in_usd_cents: Some(usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
};
}
let cents_per_second = match self.resolution {
CommonResolution::FourEightyP => CENTS_PER_SECOND_480P,
CommonResolution::TenEightyP => CENTS_PER_SECOND_1080P,
@@ -49,9 +71,9 @@ impl ArtcraftSeedance2p0BytePlusCostState {
#[cfg(test)]
mod tests {
use crate::api::router_provider::RouterProvider;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::router_provider::RouterProvider;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
mod pricing_720p {
@@ -125,6 +147,38 @@ mod tests {
}
}
mod four_k_pricing {
use enums::common::generation::common_resolution::CommonResolution;
fn artcraft_4k_cents(duration_seconds: u16, batch_count: u16, has_video_reference: bool) -> u64 {
super::super::ArtcraftSeedance2p0BytePlusCostState {
resolution: CommonResolution::FourK,
duration_seconds,
batch_count,
has_video_reference,
}
.estimate_cost()
.cost_in_usd_cents
.unwrap()
}
#[test]
fn explicit_4k_without_video_reference() {
assert_eq!(artcraft_4k_cents(4, 1, false), 347);
assert_eq!(artcraft_4k_cents(5, 1, false), 433);
assert_eq!(artcraft_4k_cents(10, 1, false), 866);
assert_eq!(artcraft_4k_cents(15, 1, false), 1299);
}
#[test]
fn explicit_4k_with_video_reference() {
assert_eq!(artcraft_4k_cents(4, 1, true), 416);
assert_eq!(artcraft_4k_cents(5, 1, true), 519);
assert_eq!(artcraft_4k_cents(10, 1, true), 1038);
assert_eq!(artcraft_4k_cents(15, 1, true), 1557);
}
}
mod relative_pricing_tests {
use super::*;
@@ -13,7 +13,7 @@ pub fn build_artcraft_seedance_2p0_bpu(builder: GenerateVideoRequestBuilder) ->
let request = build_artcraft_omni_video_request(
builder,
CommonVideoModelEnum::Seedance2p0BytePlusUltra,
SupportedResolutions::Full,
SupportedResolutions::FullWith4k,
UltraWideSupport::Supported,
)?;
let state = ArtcraftSeedance2p0BytePlusUltraRequestState { request };
@@ -101,6 +101,12 @@ mod tests {
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::TenEightyP));
}
#[test]
fn res_4k() {
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::FourK); }));
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourK));
}
#[test]
fn none_stays_none() {
let req = unwrap_request(make_builder(|_| {}));
@@ -1,7 +1,8 @@
use enums::common::generation::common_resolution::CommonResolution;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video::providers::artcraft::seedance_common::seedance_2p0_four_k_usd_cents;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_bpu::request::ArtcraftSeedance2p0BytePlusUltraRequestState;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
/// USD cents per second by resolution:
/// 480p: $0.10/s = 10.0 ¢/s
@@ -15,6 +16,7 @@ pub struct ArtcraftSeedance2p0BytePlusUltraCostState {
pub resolution: CommonResolution,
pub duration_seconds: u16,
pub batch_count: u16,
pub has_video_reference: bool,
}
impl ArtcraftSeedance2p0BytePlusUltraCostState {
@@ -23,10 +25,30 @@ impl ArtcraftSeedance2p0BytePlusUltraCostState {
.unwrap_or(CommonResolution::SevenTwentyP);
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
let batch_count = request.request.video_batch_count.unwrap_or(1);
Self { resolution, duration_seconds, batch_count }
let has_video_reference = request.request.reference_video_media_tokens
.as_ref()
.is_some_and(|tokens| !tokens.is_empty());
Self { resolution, duration_seconds, batch_count, has_video_reference }
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
if self.resolution == CommonResolution::FourK {
let usd_cents = seedance_2p0_four_k_usd_cents(
self.duration_seconds,
self.batch_count,
self.has_video_reference,
);
return VideoGenerationCostEstimate {
cost_in_credits: Some(usd_cents),
cost_in_usd_cents: Some(usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
};
}
let cents_per_second = match self.resolution {
CommonResolution::FourEightyP => CENTS_PER_SECOND_480P,
CommonResolution::TenEightyP => CENTS_PER_SECOND_1080P,
@@ -49,9 +71,9 @@ impl ArtcraftSeedance2p0BytePlusUltraCostState {
#[cfg(test)]
mod tests {
use crate::api::router_provider::RouterProvider;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::router_provider::RouterProvider;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
mod pricing_720p {
@@ -125,6 +147,38 @@ mod tests {
}
}
mod four_k_pricing {
use enums::common::generation::common_resolution::CommonResolution;
fn artcraft_4k_cents(duration_seconds: u16, batch_count: u16, has_video_reference: bool) -> u64 {
super::super::ArtcraftSeedance2p0BytePlusUltraCostState {
resolution: CommonResolution::FourK,
duration_seconds,
batch_count,
has_video_reference,
}
.estimate_cost()
.cost_in_usd_cents
.unwrap()
}
#[test]
fn explicit_4k_without_video_reference() {
assert_eq!(artcraft_4k_cents(4, 1, false), 347);
assert_eq!(artcraft_4k_cents(5, 1, false), 433);
assert_eq!(artcraft_4k_cents(10, 1, false), 866);
assert_eq!(artcraft_4k_cents(15, 1, false), 1299);
}
#[test]
fn explicit_4k_with_video_reference() {
assert_eq!(artcraft_4k_cents(4, 1, true), 416);
assert_eq!(artcraft_4k_cents(5, 1, true), 519);
assert_eq!(artcraft_4k_cents(10, 1, true), 1038);
assert_eq!(artcraft_4k_cents(15, 1, true), 1557);
}
}
mod relative_pricing_tests {
use super::*;
@@ -13,7 +13,7 @@ pub fn build_artcraft_seedance_2p0_u(builder: GenerateVideoRequestBuilder) -> Re
let request = build_artcraft_omni_video_request(
builder,
CommonVideoModelEnum::Seedance2p0Ultra,
SupportedResolutions::Full,
SupportedResolutions::FullWith4k,
UltraWideSupport::Supported,
)?;
let state = ArtcraftSeedance2p0UltraRequestState { request };
@@ -105,6 +105,12 @@ mod tests {
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::TenEightyP));
}
#[test]
fn res_4k() {
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::FourK); }));
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourK));
}
#[test]
fn none_stays_none() {
let req = unwrap_request(make_builder(|_| {}));
@@ -1,7 +1,8 @@
use enums::common::generation::common_resolution::CommonResolution;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video::providers::artcraft::seedance_common::seedance_2p0_four_k_usd_cents;
use crate::generate::generate_video::providers::artcraft::seedance_2p0_u::request::ArtcraftSeedance2p0UltraRequestState;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
// ── Pricing constants ──
@@ -13,6 +14,7 @@ pub struct ArtcraftSeedance2p0UltraCostState {
pub resolution: CommonResolution,
pub duration_seconds: u16,
pub batch_count: u16,
pub has_video_reference: bool,
}
impl ArtcraftSeedance2p0UltraCostState {
@@ -21,11 +23,31 @@ impl ArtcraftSeedance2p0UltraCostState {
.unwrap_or(CommonResolution::SevenTwentyP);
let duration_seconds = request.request.duration_seconds.unwrap_or(5);
let batch_count = request.request.video_batch_count.unwrap_or(1);
let has_video_reference = request.request.reference_video_media_tokens
.as_ref()
.is_some_and(|tokens| !tokens.is_empty());
Self { resolution, duration_seconds, batch_count }
Self { resolution, duration_seconds, batch_count, has_video_reference }
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
if self.resolution == CommonResolution::FourK {
let usd_cents = seedance_2p0_four_k_usd_cents(
self.duration_seconds,
self.batch_count,
self.has_video_reference,
);
return VideoGenerationCostEstimate {
cost_in_credits: Some(usd_cents),
cost_in_usd_cents: Some(usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
failures_are_refunded: None,
};
}
let cents_per_second = match self.resolution {
CommonResolution::FourEightyP => CENTS_PER_SECOND_480P,
CommonResolution::TenEightyP => CENTS_PER_SECOND_1080P,
@@ -49,9 +71,9 @@ impl ArtcraftSeedance2p0UltraCostState {
#[cfg(test)]
mod tests {
use crate::api::router_provider::RouterProvider;
use crate::api::router_resolution::RouterResolution;
use crate::api::router_video_model::RouterVideoModel;
use crate::api::router_provider::RouterProvider;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
mod pricing_720p {
@@ -122,6 +144,38 @@ mod tests {
}
}
mod four_k_pricing {
use enums::common::generation::common_resolution::CommonResolution;
fn artcraft_4k_cents(duration_seconds: u16, batch_count: u16, has_video_reference: bool) -> u64 {
super::super::ArtcraftSeedance2p0UltraCostState {
resolution: CommonResolution::FourK,
duration_seconds,
batch_count,
has_video_reference,
}
.estimate_cost()
.cost_in_usd_cents
.unwrap()
}
#[test]
fn explicit_4k_without_video_reference() {
assert_eq!(artcraft_4k_cents(4, 1, false), 347);
assert_eq!(artcraft_4k_cents(5, 1, false), 433);
assert_eq!(artcraft_4k_cents(10, 1, false), 866);
assert_eq!(artcraft_4k_cents(15, 1, false), 1299);
}
#[test]
fn explicit_4k_with_video_reference() {
assert_eq!(artcraft_4k_cents(4, 1, true), 416);
assert_eq!(artcraft_4k_cents(5, 1, true), 519);
assert_eq!(artcraft_4k_cents(10, 1, true), 1038);
assert_eq!(artcraft_4k_cents(15, 1, true), 1557);
}
}
mod relative_pricing {
use super::*;
@@ -0,0 +1,75 @@
//! Shared pricing for the Seedance 2.0 family of ArtCraft providers.
//!
//! ArtCraft credits equal USD cents (100 credits = $1.00).
// 4K is priced uniformly across the non-Fast Seedance 2.0 models. Rates are held
// in hundredths of a USD cent per second so the math is exact integer arithmetic
// (no floating point), then rounded up to whole cents.
/// 4K output price, in hundredths of a USD cent per second (86.60 ¢/s).
const FOUR_K_CENTI_CENTS_PER_SECOND: u64 = 8660;
/// Extra hundredths of a USD cent per second when a reference video is attached
/// at 4K (17.20 ¢/s surcharge).
const FOUR_K_VIDEO_REFERENCE_SURCHARGE_CENTI_CENTS_PER_SECOND: u64 = 1720;
/// ArtCraft's user-facing price (USD cents) for Seedance 2.0 at 4K.
///
/// A reference video adds a per-second surcharge (4K with and without a video
/// reference are priced differently). The cost scales with duration and batch
/// count, rounded up to whole cents.
pub fn seedance_2p0_four_k_usd_cents(
duration_seconds: u16,
batch_count: u16,
has_video_reference: bool,
) -> u64 {
let mut centi_cents_per_second = FOUR_K_CENTI_CENTS_PER_SECOND;
if has_video_reference {
centi_cents_per_second += FOUR_K_VIDEO_REFERENCE_SURCHARGE_CENTI_CENTS_PER_SECOND;
}
let total_centi_cents =
centi_cents_per_second * duration_seconds as u64 * batch_count as u64;
// Round up to whole cents.
total_centi_cents.div_ceil(100)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn four_k_without_video_reference() {
assert_eq!(seedance_2p0_four_k_usd_cents(4, 1, false), 347);
assert_eq!(seedance_2p0_four_k_usd_cents(5, 1, false), 433);
assert_eq!(seedance_2p0_four_k_usd_cents(10, 1, false), 866);
assert_eq!(seedance_2p0_four_k_usd_cents(15, 1, false), 1299);
}
#[test]
fn four_k_with_video_reference() {
assert_eq!(seedance_2p0_four_k_usd_cents(4, 1, true), 416);
assert_eq!(seedance_2p0_four_k_usd_cents(5, 1, true), 519);
assert_eq!(seedance_2p0_four_k_usd_cents(10, 1, true), 1038);
assert_eq!(seedance_2p0_four_k_usd_cents(15, 1, true), 1557);
}
#[test]
fn video_reference_always_costs_more() {
for &duration in &[4u16, 5, 10, 15] {
assert!(
seedance_2p0_four_k_usd_cents(duration, 1, true)
> seedance_2p0_four_k_usd_cents(duration, 1, false),
"video reference should cost more at {duration}s",
);
}
}
#[test]
fn batch_count_multiplies() {
assert_eq!(seedance_2p0_four_k_usd_cents(5, 2, false), 866);
assert_eq!(seedance_2p0_four_k_usd_cents(5, 4, false), 1732);
assert_eq!(seedance_2p0_four_k_usd_cents(5, 2, true), 1038);
}
}
@@ -111,7 +111,7 @@ fn nearest_aspect_ratio(aspect_ratio: RouterAspectRatio) -> KinoviAspectRatio {
}
}
// Seedance 2.0 Pro supports output resolutions: 480p, 720p, 1080p.
// Seedance 2.0 Pro supports output resolutions: 480p, 720p, 1080p, 4K.
fn plan_output_resolution(
resolution: Option<RouterResolution>,
strategy: RequestMismatchMitigationStrategy,
@@ -123,6 +123,7 @@ fn plan_output_resolution(
Some(RouterResolution::FourEightyP) => Ok(Some(KinoviOutputResolution::FourEightyP)),
Some(RouterResolution::SevenTwentyP) => Ok(Some(KinoviOutputResolution::SevenTwentyP)),
Some(RouterResolution::TenEightyP) => Ok(Some(KinoviOutputResolution::TenEightyP)),
Some(RouterResolution::FourK) => Ok(Some(KinoviOutputResolution::FourK)),
// Mismatches
Some(unsupported) => match strategy {
@@ -376,6 +377,16 @@ mod tests {
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::TenEightyP)));
}
#[test]
fn resolution_4k() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::FourK),
..seedance2pro_builder()
};
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::FourK)));
}
#[test]
fn resolution_none() {
let builder = GenerateVideoRequestBuilder { resolution: None, ..seedance2pro_builder() };
@@ -386,7 +397,7 @@ mod tests {
#[test]
fn unsupported_resolution_error_out() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::FourK),
resolution: Some(RouterResolution::ThreeK),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..seedance2pro_builder()
};
@@ -396,7 +407,7 @@ mod tests {
#[test]
fn unsupported_resolution_rounds_up() {
let builder = GenerateVideoRequestBuilder {
resolution: Some(RouterResolution::FourK),
resolution: Some(RouterResolution::ThreeK),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..seedance2pro_builder()
};
@@ -175,6 +175,121 @@ mod tests {
}
}
// ── 4K pricing ──
//
// Routes to the seedance2pro_client: 4K base is 200 credits/s (vs 90 at 1080p).
// Credits → cents at 243 credits/$1, rounded up.
mod pricing_4k {
use super::*;
#[test]
fn cost_4k_batch_1() {
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 4, KinoviBatchCount::One), 330);
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 5, KinoviBatchCount::One), 412);
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 10, KinoviBatchCount::One), 824);
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 15, KinoviBatchCount::One), 1235);
}
#[test]
fn cost_4k_batch_2() {
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 5, KinoviBatchCount::Two), 824);
}
#[test]
fn cost_4k_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::FourK, 5, KinoviBatchCount::Four), 1647);
}
}
// ── 4K: router cost must match the seedance2pro_client binding ──
//
// The router delegates to GenerateSeedance2p0Request::calculate_costs(), so these
// assert the exact 4K numbers AND cross-check the router against the binding for
// every duration (4/5/10/15s) with and without a video reference.
//
// 4K base = 200 credits/s; with a video reference = 240 credits/s (+40 surcharge).
// USD cents = ceil(total_credits / 243 * 100).
mod four_k_matches_binding {
use super::*;
use seedance2pro_client::generate::video::generate_seedance_2p0::GenerateSeedance2p0Request;
/// Router-side 4K cost (batch 1) for a duration / video-reference combo.
fn router(duration_seconds: u8, has_video_reference: bool) -> VideoGenerationCostEstimate {
KinoviSeedance2p0CostState {
resolution: Some(KinoviOutputResolution::FourK),
duration_seconds,
batch_count: Some(KinoviBatchCount::One),
has_video_reference,
}
.estimate_cost()
}
/// The seedance2pro_client binding's own 4K cost for the same inputs.
/// Returns (kinovi_credits, usd_cents_rounded_up).
fn binding(duration_seconds: u8, has_video_reference: bool) -> (u64, u64) {
let reference_video_urls = if has_video_reference {
Some(vec!["pricing-placeholder".to_string()])
} else {
None
};
let costs = GenerateSeedance2p0Request {
output_resolution: Some(KinoviOutputResolution::FourK),
duration_seconds,
batch_count: Some(KinoviBatchCount::One),
reference_video_urls,
prompt: String::new(),
aspect_ratio: None,
start_frame_url: None,
end_frame_url: None,
reference_image_urls: None,
reference_audio_urls: None,
character_ids: None,
use_face_blur_hack: None,
bitrate: None,
}
.calculate_costs();
(costs.total_cost.kinovi_credits, costs.total_cost.usd_cents_rounded_up)
}
#[test]
fn explicit_values_without_video_reference() {
// 200 credits/s.
assert_eq!((router(4, false).cost_in_credits, router(4, false).cost_in_usd_cents), (Some(800), Some(330)));
assert_eq!((router(5, false).cost_in_credits, router(5, false).cost_in_usd_cents), (Some(1000), Some(412)));
assert_eq!((router(10, false).cost_in_credits, router(10, false).cost_in_usd_cents), (Some(2000), Some(824)));
assert_eq!((router(15, false).cost_in_credits, router(15, false).cost_in_usd_cents), (Some(3000), Some(1235)));
}
#[test]
fn explicit_values_with_video_reference() {
// 240 credits/s (200 base + 40 surcharge).
assert_eq!((router(4, true).cost_in_credits, router(4, true).cost_in_usd_cents), (Some(960), Some(396)));
assert_eq!((router(5, true).cost_in_credits, router(5, true).cost_in_usd_cents), (Some(1200), Some(494)));
assert_eq!((router(10, true).cost_in_credits, router(10, true).cost_in_usd_cents), (Some(2400), Some(988)));
assert_eq!((router(15, true).cost_in_credits, router(15, true).cost_in_usd_cents), (Some(3600), Some(1482)));
}
#[test]
fn router_matches_binding_for_every_duration_and_video_ref() {
for &duration in &[4u8, 5, 10, 15] {
for &has_ref in &[false, true] {
let r = router(duration, has_ref);
let (b_credits, b_cents) = binding(duration, has_ref);
assert_eq!(
r.cost_in_credits, Some(b_credits),
"credits differ at {duration}s has_video_reference={has_ref}",
);
assert_eq!(
r.cost_in_usd_cents, Some(b_cents),
"usd cents differ at {duration}s has_video_reference={has_ref}",
);
}
}
}
}
// ── Relative pricing ──
mod relative_pricing_tests {
@@ -189,6 +304,13 @@ mod tests {
assert!(c720 < c1080, "720p ({}) should be cheaper than 1080p ({})", c720, c1080);
}
#[test]
fn cost_1080p_cheaper_than_4k() {
let c1080 = usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One);
let c4k = usd_cents(KinoviOutputResolution::FourK, 5, KinoviBatchCount::One);
assert!(c1080 < c4k, "1080p ({}) should be cheaper than 4K ({})", c1080, c4k);
}
#[test]
fn cost_scales_with_duration() {
let c4 = usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One);
@@ -306,6 +428,24 @@ mod tests {
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(31));
}
#[test]
fn from_draft_4k() {
let draft = make_draft(5, 1, Some(RouterResolution::FourK), false);
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
assert!(matches!(cost.resolution, Some(KinoviOutputResolution::FourK)));
assert_eq!(cost.estimate_cost().cost_in_credits, Some(1000));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(412));
}
#[test]
fn from_draft_4k_with_video_reference() {
let draft = make_draft(5, 1, Some(RouterResolution::FourK), true);
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
// 4K base 200 + video-ref surcharge 40 = 240 credits/s -> 1200 credits for 5s.
assert_eq!(cost.estimate_cost().cost_in_credits, Some(1200));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(494));
}
#[test]
fn from_draft_1080p_batch_4() {
let draft = make_draft(5, 4, Some(RouterResolution::TenEightyP), false);
@@ -388,6 +528,11 @@ mod tests {
fn credits_1080p() {
assert_eq!(credits(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One), 450);
}
#[test]
fn credits_4k() {
assert_eq!(credits(KinoviOutputResolution::FourK, 5, KinoviBatchCount::One), 1000);
}
}
// ── Helpers ──
@@ -310,6 +310,10 @@ mod tests {
request.calculate_costs().total_cost.kinovi_credits
}
fn total_usd_cents(request: &GenerateSeedance2p0Request) -> u64 {
request.calculate_costs().total_cost.usd_cents_rounded_up
}
// ── 4K pricing (Seedance 2.0 only) ──
//
// Base: 200 credits/sec. With a video reference: 240 credits/sec
@@ -363,6 +367,54 @@ mod tests {
assert_eq!(total_credits(&r4k_with_video_ref(15)), 3600);
}
// ── Total cost in USD cents — no video reference ──
//
// 200 credits/sec; usd_cents = ceil(total_credits / 243 * 100).
#[test]
fn four_k_no_video_ref_4_seconds_is_330_cents() {
assert_eq!(total_usd_cents(&r4k(4)), 330);
}
#[test]
fn four_k_no_video_ref_5_seconds_is_412_cents() {
assert_eq!(total_usd_cents(&r4k(5)), 412);
}
#[test]
fn four_k_no_video_ref_10_seconds_is_824_cents() {
assert_eq!(total_usd_cents(&r4k(10)), 824);
}
#[test]
fn four_k_no_video_ref_15_seconds_is_1235_cents() {
assert_eq!(total_usd_cents(&r4k(15)), 1235);
}
// ── Total cost in USD cents — with video reference ──
//
// 240 credits/sec (200 base + 40 surcharge); usd_cents = ceil(total_credits / 243 * 100).
#[test]
fn four_k_with_video_ref_4_seconds_is_396_cents() {
assert_eq!(total_usd_cents(&r4k_with_video_ref(4)), 396);
}
#[test]
fn four_k_with_video_ref_5_seconds_is_494_cents() {
assert_eq!(total_usd_cents(&r4k_with_video_ref(5)), 494);
}
#[test]
fn four_k_with_video_ref_10_seconds_is_988_cents() {
assert_eq!(total_usd_cents(&r4k_with_video_ref(10)), 988);
}
#[test]
fn four_k_with_video_ref_15_seconds_is_1482_cents() {
assert_eq!(total_usd_cents(&r4k_with_video_ref(15)), 1482);
}
// ── Base / surcharge breakdown (5 seconds) ──
#[test]
@@ -105,90 +105,6 @@ impl std::fmt::Debug for WorkflowRunTaskArgs<'_> {
}
}
impl WorkflowRunTaskRequest {
/// Estimates the credit cost for this generation request.
///
/// Pricing is per-second × batch count, with the per-second rate
/// depending on model type and output resolution:
///
/// | Model | 480p | 720p | 1080p | 4K |
/// |--------------|------|------|-------|-----|
/// | Pro | 15 | 40 | 90 | 200 |
/// | Fast | 10 | 28 | n/a | n/a |
/// | HappyHorse | 15 | 40 | 90 | n/a |
/// TODO(bt,2026-04-23): Not sure pricing for Happy Horse is correct here.
/// NB: 4K is Seedance 2.0 (Pro) only; Fast does not support it. (This base-only
/// estimate doesn't model the video-reference surcharge — see
/// `generate_seedance_2p0::calculate_costs` for the authoritative cost.)
///
/// Input mode (text, keyframe, reference) has no effect on cost.
/// Aspect ratio (`resolution` field) has no effect on cost.
pub fn estimate_credits(&self) -> u32 {
let credits_per_second: u32 = match (self.model_type, self.output_resolution) {
// Seedance 2.0 Pro
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::FourEightyP)) => 15,
(KinoviModelTypeRaw::Seedance2Pro, None)
| (KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::SevenTwentyP)) => 40,
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::TenEightyP)) => 90,
// 4K: 200 credits/sec (base). 4K is Seedance 2.0 (Pro) only.
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::FourK)) => 200,
// Seedance 2.0 Fast
(KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::FourEightyP)) => 10,
(KinoviModelTypeRaw::Seedance2Fast, None)
| (KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::SevenTwentyP)) => 28,
// NB: 1080p not officially supported for Fast, but price as 720p if requested
(KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::TenEightyP)) => 28,
// NB: 4K is NOT supported for Fast; price defensively at the 720p rate if ever requested.
(KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::FourK)) => 28,
// TODO(bt,2026-04-23): Not sure pricing for Happy Horse is correct here.
// Happy Horse 1.0 — same credit rates as Seedance 2.0 Pro
(KinoviModelTypeRaw::HappyHorse1p0, Some(KinoviOutputResolutionRaw::FourEightyP)) => 15,
(KinoviModelTypeRaw::HappyHorse1p0, None)
| (KinoviModelTypeRaw::HappyHorse1p0, Some(KinoviOutputResolutionRaw::SevenTwentyP)) => 40,
(KinoviModelTypeRaw::HappyHorse1p0, Some(KinoviOutputResolutionRaw::TenEightyP)) => 90,
// NB: Happy Horse does NOT support 4K (only Seedance 2.0 Pro does); price
// defensively at its 1080p rate if it's ever somehow requested.
(KinoviModelTypeRaw::HappyHorse1p0, Some(KinoviOutputResolutionRaw::FourK)) => 90,
};
let per_video = u32::from(self.duration_seconds) * credits_per_second;
let batch_multiplier: u32 = match self.batch_count {
KinoviBatchCountRaw::One => 1,
KinoviBatchCountRaw::Two => 2,
KinoviBatchCountRaw::Four => 4,
};
per_video * batch_multiplier
}
/// 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: 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
(KinoviModelTypeRaw::Seedance2Pro, None)
| (KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::SevenTwentyP)) => 250.0,
// Legacy: Seedance 2.0 Fast @ 720p — 22,000 credits for $99.99
(KinoviModelTypeRaw::Seedance2Fast, None)
| (KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::SevenTwentyP)) => 220.0,
// New pricing: 525,000 credits for $2,159.0909 (~243.16 credits/$1)
_ => 243.0,
}
}
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
let credits = self.estimate_credits() as f64;
let credits_per_dollar = self.credits_per_dollar();
let cost = credits / credits_per_dollar * 100.0;
cost.round() as u64
}
}
// --- Public enums ---
/// Video resolution / aspect ratio.
@@ -484,313 +400,6 @@ mod tests {
use crate::requests::prepare_file_upload::prepare_file_upload::{prepare_file_upload, PrepareFileUploadArgs};
use crate::requests::upload_file::upload_file::{upload_file, UploadFileArgs};
mod pricing_tests {
use super::*;
fn make_args(
model_type: KinoviModelTypeRaw,
duration_seconds: u8,
batch_count: KinoviBatchCountRaw,
output_resolution: Option<KinoviOutputResolutionRaw>,
) -> WorkflowRunTaskRequest {
WorkflowRunTaskRequest {
model_type,
prompt: String::new(),
aspect_ratio: KinoviAspectRatioRaw::Square1x1,
duration_seconds,
batch_count,
start_frame_url: None,
end_frame_url: None,
reference_image_urls: None,
reference_video_urls: None,
reference_audio_urls: None,
character_ids: None,
output_resolution,
use_face_blur_hack: None,
bitrate: None,
}
}
fn pro(dur: u8, batch: KinoviBatchCountRaw) -> WorkflowRunTaskRequest {
make_args(KinoviModelTypeRaw::Seedance2Pro, dur, batch, None)
}
fn pro_res(dur: u8, batch: KinoviBatchCountRaw, res: KinoviOutputResolutionRaw) -> WorkflowRunTaskRequest {
make_args(KinoviModelTypeRaw::Seedance2Pro, dur, batch, Some(res))
}
fn fast(dur: u8, batch: KinoviBatchCountRaw) -> WorkflowRunTaskRequest {
make_args(KinoviModelTypeRaw::Seedance2Fast, dur, batch, None)
}
fn fast_res(dur: u8, batch: KinoviBatchCountRaw, res: KinoviOutputResolutionRaw) -> WorkflowRunTaskRequest {
make_args(KinoviModelTypeRaw::Seedance2Fast, dur, batch, Some(res))
}
// ── Spot checks: exact values from the pricing table ──
#[test]
fn spot_check_pro_720p() {
assert_eq!(pro(5, KinoviBatchCountRaw::One).estimate_credits(), 200);
assert_eq!(pro(10, KinoviBatchCountRaw::One).estimate_credits(), 400);
assert_eq!(pro(15, KinoviBatchCountRaw::One).estimate_credits(), 600);
}
#[test]
fn spot_check_pro_480p() {
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits(), 75);
assert_eq!(pro_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits(), 150);
assert_eq!(pro_res(15, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits(), 225);
}
#[test]
fn spot_check_pro_1080p() {
assert_eq!(pro_res(4, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 360);
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 450);
assert_eq!(pro_res(6, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 540);
assert_eq!(pro_res(7, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 630);
assert_eq!(pro_res(8, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 720);
assert_eq!(pro_res(9, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 810);
assert_eq!(pro_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 900);
assert_eq!(pro_res(11, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 990);
assert_eq!(pro_res(12, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 1080);
assert_eq!(pro_res(13, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 1170);
assert_eq!(pro_res(14, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 1260);
assert_eq!(pro_res(15, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 1350);
}
#[test]
fn spot_check_pro_4k() {
// 4K is Seedance 2.0 (Pro) only. Base rate: 200 credits/sec.
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourK).estimate_credits(), 1000);
assert_eq!(pro_res(15, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourK).estimate_credits(), 3000);
}
#[test]
fn pro_4k_more_expensive_than_1080p() {
let c1080 = pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits();
let c4k = pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourK).estimate_credits();
assert!(c4k > c1080, "4K ({}) should cost more than 1080p ({})", c4k, c1080);
}
#[test]
fn spot_check_fast_720p() {
assert_eq!(fast(5, KinoviBatchCountRaw::One).estimate_credits(), 140);
assert_eq!(fast(10, KinoviBatchCountRaw::One).estimate_credits(), 280);
}
#[test]
fn spot_check_fast_480p() {
assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits(), 50);
assert_eq!(fast_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits(), 100);
}
#[test]
fn pro_more_expensive_than_fast_720p() {
let p = pro(5, KinoviBatchCountRaw::One).estimate_credits();
let f = fast(5, KinoviBatchCountRaw::One).estimate_credits();
assert!(p > f, "Pro 720p ({}) should be more than Fast 720p ({})", p, f);
}
#[test]
fn pro_more_expensive_than_fast_480p() {
let p = pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits();
let f = fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits();
assert!(p > f, "Pro 480p ({}) should be more than Fast 480p ({})", p, f);
}
#[test]
fn pro_1080p_more_than_720p_more_than_480p() {
let c480 = pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits();
let c720 = pro(5, KinoviBatchCountRaw::One).estimate_credits();
let c1080 = pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits();
assert!(c480 < c720, "480p ({}) should be less than 720p ({})", c480, c720);
assert!(c720 < c1080, "720p ({}) should be less than 1080p ({})", c720, c1080);
}
#[test]
fn fast_720p_more_than_480p() {
let c480 = fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits();
let c720 = fast(5, KinoviBatchCountRaw::One).estimate_credits();
assert!(c480 < c720, "Fast 480p ({}) should be less than Fast 720p ({})", c480, c720);
}
#[test]
fn pro_none_same_as_720p() {
let none = pro(5, KinoviBatchCountRaw::One).estimate_credits();
let explicit = pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::SevenTwentyP).estimate_credits();
assert_eq!(none, explicit);
}
#[test]
fn fast_none_same_as_720p() {
let none = fast(5, KinoviBatchCountRaw::One).estimate_credits();
let explicit = fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::SevenTwentyP).estimate_credits();
assert_eq!(none, explicit);
}
#[test]
fn table_driven_credits() {
let cases: Vec<(KinoviModelTypeRaw, Option<KinoviOutputResolutionRaw>, u8, KinoviBatchCountRaw, u32)> = vec![
(KinoviModelTypeRaw::Seedance2Pro, None, 4, KinoviBatchCountRaw::One, 160),
(KinoviModelTypeRaw::Seedance2Pro, None, 5, KinoviBatchCountRaw::Two, 400),
(KinoviModelTypeRaw::Seedance2Pro, None, 10, KinoviBatchCountRaw::Four, 1600),
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::FourEightyP), 5, KinoviBatchCountRaw::One, 75),
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::FourEightyP), 10, KinoviBatchCountRaw::Two, 300),
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::FourEightyP), 15, KinoviBatchCountRaw::Four, 900),
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::TenEightyP), 5, KinoviBatchCountRaw::One, 450),
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::TenEightyP), 10, KinoviBatchCountRaw::Two, 1800),
(KinoviModelTypeRaw::Seedance2Pro, Some(KinoviOutputResolutionRaw::TenEightyP), 15, KinoviBatchCountRaw::Four, 5400),
(KinoviModelTypeRaw::Seedance2Fast, None, 4, KinoviBatchCountRaw::One, 112),
(KinoviModelTypeRaw::Seedance2Fast, None, 5, KinoviBatchCountRaw::Two, 280),
(KinoviModelTypeRaw::Seedance2Fast, None, 15, KinoviBatchCountRaw::Four, 1680),
(KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::FourEightyP), 5, KinoviBatchCountRaw::One, 50),
(KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::FourEightyP), 10, KinoviBatchCountRaw::Two, 200),
(KinoviModelTypeRaw::Seedance2Fast, Some(KinoviOutputResolutionRaw::FourEightyP), 15, KinoviBatchCountRaw::Four, 600),
];
for (i, (model, res, dur, batch, expected)) in cases.iter().enumerate() {
let args = make_args(*model, *dur, *batch, *res);
let actual = args.estimate_credits();
assert_eq!(
actual, *expected,
"Case {}: {:?} {:?} {}s batch {:?} — expected {} credits, got {}",
i, model, res, dur, batch, expected, actual,
);
}
}
#[test]
fn aspect_ratio_does_not_affect_credits() {
let resolutions = [
KinoviAspectRatioRaw::Landscape16x9,
KinoviAspectRatioRaw::Portrait9x16,
KinoviAspectRatioRaw::Square1x1,
KinoviAspectRatioRaw::Landscape4x3,
KinoviAspectRatioRaw::Portrait3x4,
];
let baseline = pro(5, KinoviBatchCountRaw::One).estimate_credits();
for res in &resolutions {
let req = WorkflowRunTaskRequest {
model_type: KinoviModelTypeRaw::Seedance2Pro,
prompt: String::new(),
aspect_ratio: *res,
duration_seconds: 5,
batch_count: KinoviBatchCountRaw::One,
start_frame_url: None,
end_frame_url: None,
reference_image_urls: None,
reference_video_urls: None,
reference_audio_urls: None,
character_ids: None,
output_resolution: None,
use_face_blur_hack: None,
bitrate: None,
};
assert_eq!(
req.estimate_credits(), baseline,
"Aspect ratio {:?} should not change credits from baseline {}",
res, baseline,
);
}
}
#[test]
fn usd_cents_legacy_pro_720p() {
assert_eq!(pro(5, KinoviBatchCountRaw::One).estimate_cost_in_usd_cents(), 80);
assert_eq!(pro(10, KinoviBatchCountRaw::One).estimate_cost_in_usd_cents(), 160);
assert_eq!(pro(15, KinoviBatchCountRaw::One).estimate_cost_in_usd_cents(), 240);
assert_eq!(pro(5, KinoviBatchCountRaw::Two).estimate_cost_in_usd_cents(), 160);
assert_eq!(pro(5, KinoviBatchCountRaw::Four).estimate_cost_in_usd_cents(), 320);
}
#[test]
fn usd_cents_legacy_fast_720p() {
assert_eq!(fast(5, KinoviBatchCountRaw::One).estimate_cost_in_usd_cents(), 64);
assert_eq!(fast(10, KinoviBatchCountRaw::One).estimate_cost_in_usd_cents(), 127);
}
#[test]
fn usd_cents_new_pro_480p() {
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(), 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(), 21);
assert_eq!(fast_res(10, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_cost_in_usd_cents(), 41);
}
#[test]
fn credits_per_dollar_legacy_rates() {
assert_eq!(pro(5, KinoviBatchCountRaw::One).credits_per_dollar(), 250.0);
assert_eq!(fast(5, KinoviBatchCountRaw::One).credits_per_dollar(), 220.0);
assert_eq!(pro_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::SevenTwentyP).credits_per_dollar(), 250.0);
assert_eq!(fast_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::SevenTwentyP).credits_per_dollar(), 220.0);
}
#[test]
fn credits_per_dollar_new_rate() {
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 ──
fn happy_horse(dur: u8, batch: KinoviBatchCountRaw) -> WorkflowRunTaskRequest {
make_args(KinoviModelTypeRaw::HappyHorse1p0, dur, batch, None)
}
fn happy_horse_res(dur: u8, batch: KinoviBatchCountRaw, res: KinoviOutputResolutionRaw) -> WorkflowRunTaskRequest {
make_args(KinoviModelTypeRaw::HappyHorse1p0, dur, batch, Some(res))
}
#[test]
fn spot_check_happy_horse_720p() {
// Same as Pro: 40 credits/sec
assert_eq!(happy_horse(5, KinoviBatchCountRaw::One).estimate_credits(), 200);
assert_eq!(happy_horse(10, KinoviBatchCountRaw::One).estimate_credits(), 400);
assert_eq!(happy_horse(15, KinoviBatchCountRaw::One).estimate_credits(), 600);
}
#[test]
fn spot_check_happy_horse_480p() {
assert_eq!(happy_horse_res(5, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::FourEightyP).estimate_credits(), 75);
}
#[test]
fn spot_check_happy_horse_1080p() {
assert_eq!(happy_horse_res(4, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 360);
assert_eq!(happy_horse_res(15, KinoviBatchCountRaw::One, KinoviOutputResolutionRaw::TenEightyP).estimate_credits(), 1350);
}
#[test]
fn happy_horse_uses_new_pricing_rate() {
// 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() {
// 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);
}
}
// ── Bitrate serialization ──
//
// The optional `bitrate_mode` field is sent only when `High` is requested;
@@ -43,6 +43,7 @@ pub fn seedance_2p0_video_models() -> Vec<OmniGenVideoModelDetails> {
CommonResolution::FourEightyP,
CommonResolution::SevenTwentyP,
CommonResolution::TenEightyP,
CommonResolution::FourK,
]),
resolution_default: Some(CommonResolution::SevenTwentyP),
bitrate_options: Some(vec![
@@ -136,6 +137,7 @@ pub fn seedance_2p0_video_models() -> Vec<OmniGenVideoModelDetails> {
CommonResolution::FourEightyP,
CommonResolution::SevenTwentyP,
CommonResolution::TenEightyP,
CommonResolution::FourK,
]),
resolution_default: Some(CommonResolution::SevenTwentyP),
bitrate_options: Some(vec![
@@ -229,6 +231,7 @@ pub fn seedance_2p0_video_models() -> Vec<OmniGenVideoModelDetails> {
CommonResolution::FourEightyP,
CommonResolution::SevenTwentyP,
CommonResolution::TenEightyP,
CommonResolution::FourK,
]),
resolution_default: Some(CommonResolution::SevenTwentyP),
bitrate_options: Some(vec![