update costs + tests

This commit is contained in:
Brandon Thomas
2026-04-24 02:55:32 -04:00
parent bc6a46046d
commit 2f59fccf25
@@ -1,6 +1,6 @@
use seedance2pro_client::requests::workflow_run_task::workflow_run_task::{
KinoviAspectRatioRaw, KinoviBatchCountRaw, KinoviModelTypeRaw,
KinoviOutputResolutionRaw, WorkflowRunTaskRequest,
use seedance2pro_client::generate::video::generate_happy_horse_1p0::{
GenerateHappyHorse1p0Request, KinoviHappyHorse1p0BatchCount,
KinoviHappyHorse1p0OutputResolution,
};
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
@@ -8,49 +8,42 @@ use crate::generate::generate_video_v2::providers::kinovi::happy_horse_1p0::draf
use crate::generate::generate_video_v2::providers::kinovi::happy_horse_1p0::request::KinoviHappyHorse1p0RequestState;
pub struct KinoviHappyHorse1p0CostState {
pub resolution: Option<KinoviOutputResolutionRaw>,
pub resolution: Option<KinoviHappyHorse1p0OutputResolution>,
pub duration_seconds: u8,
pub batch_count: KinoviBatchCountRaw,
pub batch_count: Option<KinoviHappyHorse1p0BatchCount>,
}
impl KinoviHappyHorse1p0CostState {
pub fn from_request(request: &KinoviHappyHorse1p0RequestState) -> Self {
Self {
resolution: request.request.output_resolution.map(map_output_resolution),
resolution: request.request.output_resolution,
duration_seconds: request.request.duration_seconds,
batch_count: map_batch_count(request.request.batch_count),
batch_count: request.request.batch_count,
}
}
pub fn from_draft(draft: &KinoviHappyHorse1p0DraftState) -> Self {
Self {
resolution: draft.resolution.map(map_output_resolution),
resolution: draft.resolution,
duration_seconds: draft.duration_seconds,
batch_count: map_batch_count(draft.batch_count),
batch_count: draft.batch_count,
}
}
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
let raw_request = WorkflowRunTaskRequest {
model_type: KinoviModelTypeRaw::HappyHorse1p0,
let pricing_request = GenerateHappyHorse1p0Request {
output_resolution: self.resolution,
duration_seconds: self.duration_seconds,
batch_count: self.batch_count,
// No impact on price
prompt: String::new(),
aspect_ratio: KinoviAspectRatioRaw::Landscape16x9,
aspect_ratio: None,
start_frame_url: None,
end_frame_url: None,
reference_image_urls: None,
reference_video_urls: None,
reference_audio_urls: None,
character_ids: None,
use_face_blur_hack: None,
};
let cost_in_credits = raw_request.estimate_credits();
let cost_in_usd_cents = raw_request.estimate_cost_in_usd_cents();
let cost_in_credits = pricing_request.estimate_credits();
let cost_in_usd_cents = pricing_request.estimate_cost_in_usd_cents();
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_credits as u64),
@@ -63,27 +56,6 @@ impl KinoviHappyHorse1p0CostState {
}
}
// ── Mapping helpers ──
use seedance2pro_client::generate::video::generate_happy_horse_1p0::{
KinoviHappyHorse1p0BatchCount, KinoviHappyHorse1p0OutputResolution,
};
fn map_output_resolution(res: KinoviHappyHorse1p0OutputResolution) -> KinoviOutputResolutionRaw {
match res {
KinoviHappyHorse1p0OutputResolution::SevenTwentyP => KinoviOutputResolutionRaw::SevenTwentyP,
KinoviHappyHorse1p0OutputResolution::TenEightyP => KinoviOutputResolutionRaw::TenEightyP,
}
}
fn map_batch_count(bc: Option<KinoviHappyHorse1p0BatchCount>) -> KinoviBatchCountRaw {
match bc {
None | Some(KinoviHappyHorse1p0BatchCount::One) => KinoviBatchCountRaw::One,
Some(KinoviHappyHorse1p0BatchCount::Two) => KinoviBatchCountRaw::Two,
Some(KinoviHappyHorse1p0BatchCount::Four) => KinoviBatchCountRaw::Four,
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -93,58 +65,102 @@ mod tests {
use crate::generate::generate_video_v2::video_generation_draft::VideoGenerationDraftRequest;
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
// ── 720p pricing ──
// ── 720p pricing (33 credits/sec) ──
mod pricing_720p {
use super::*;
#[test]
fn cost_720p_batch_1() {
assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::SevenTwentyP), 4, KinoviBatchCountRaw::One), 83);
assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::SevenTwentyP), 5, KinoviBatchCountRaw::One), 104);
assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::SevenTwentyP), 10, KinoviBatchCountRaw::One), 207);
assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::SevenTwentyP), 15, KinoviBatchCountRaw::One), 311);
fn credits_720p_every_duration() {
assert_eq!(credits(None, 3), 99);
assert_eq!(credits(None, 4), 132);
assert_eq!(credits(None, 5), 165);
assert_eq!(credits(None, 6), 198);
assert_eq!(credits(None, 7), 231);
assert_eq!(credits(None, 8), 264);
assert_eq!(credits(None, 9), 297);
assert_eq!(credits(None, 10), 330);
assert_eq!(credits(None, 11), 363);
assert_eq!(credits(None, 12), 396);
assert_eq!(credits(None, 13), 429);
assert_eq!(credits(None, 14), 462);
assert_eq!(credits(None, 15), 495);
}
#[test]
fn cost_720p_batch_2() {
assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::SevenTwentyP), 5, KinoviBatchCountRaw::Two), 207);
}
#[test]
fn cost_720p_batch_4() {
assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::SevenTwentyP), 5, KinoviBatchCountRaw::Four), 415);
}
#[test]
fn default_resolution_same_as_720p() {
let default = usd_cents(None, 5, KinoviBatchCountRaw::One);
let explicit = usd_cents(Some(KinoviOutputResolutionRaw::SevenTwentyP), 5, KinoviBatchCountRaw::One);
fn explicit_720p_same_as_default() {
let default = credits(None, 5);
let explicit = credits(Some(KinoviHappyHorse1p0OutputResolution::SevenTwentyP), 5);
assert_eq!(default, explicit);
}
#[test]
fn usd_cents_720p() {
// 165 credits / 193 * 100 = 85.49 → 85¢
assert_eq!(usd_cents(None, 5, None), 85);
// 495 credits / 193 * 100 = 256.48 → 256¢
assert_eq!(usd_cents(None, 15, None), 256);
}
}
// ── 1080p pricing ──
// ── 1080p pricing (66 credits/sec) ──
mod pricing_1080p {
use super::*;
#[test]
fn cost_1080p_batch_1() {
assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::TenEightyP), 4, KinoviBatchCountRaw::One), 187);
assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::TenEightyP), 5, KinoviBatchCountRaw::One), 233);
assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::TenEightyP), 10, KinoviBatchCountRaw::One), 466);
assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::TenEightyP), 15, KinoviBatchCountRaw::One), 699);
fn credits_1080p_every_duration() {
let r = Some(KinoviHappyHorse1p0OutputResolution::TenEightyP);
assert_eq!(credits(r, 3), 198);
assert_eq!(credits(r, 4), 264);
assert_eq!(credits(r, 5), 330);
assert_eq!(credits(r, 6), 396);
assert_eq!(credits(r, 7), 462);
assert_eq!(credits(r, 8), 528);
assert_eq!(credits(r, 9), 594);
assert_eq!(credits(r, 10), 660);
assert_eq!(credits(r, 11), 726);
assert_eq!(credits(r, 12), 792);
assert_eq!(credits(r, 13), 858);
assert_eq!(credits(r, 14), 924);
assert_eq!(credits(r, 15), 990);
}
#[test]
fn cost_1080p_batch_2() {
assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::TenEightyP), 5, KinoviBatchCountRaw::Two), 466);
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);
}
}
// ── Batch multiplier ──
mod batch_tests {
use super::*;
#[test]
fn batch_2_doubles_credits() {
let base = credits(None, 5);
let batch2 = credits_with_batch(None, 5, Some(KinoviHappyHorse1p0BatchCount::Two));
assert_eq!(batch2, base * 2);
}
#[test]
fn cost_1080p_batch_4() {
assert_eq!(usd_cents(Some(KinoviOutputResolutionRaw::TenEightyP), 5, KinoviBatchCountRaw::Four), 933);
fn batch_4_quadruples_credits() {
let base = credits(None, 5);
let batch4 = credits_with_batch(None, 5, Some(KinoviHappyHorse1p0BatchCount::Four));
assert_eq!(batch4, base * 4);
}
#[test]
fn batch_applies_to_1080p() {
let r = Some(KinoviHappyHorse1p0OutputResolution::TenEightyP);
let base = credits(r, 5);
let batch4 = credits_with_batch(r, 5, Some(KinoviHappyHorse1p0BatchCount::Four));
assert_eq!(batch4, base * 4);
}
}
@@ -154,40 +170,33 @@ mod tests {
use super::*;
#[test]
fn cost_720p_cheaper_than_1080p() {
let c720 = usd_cents(Some(KinoviOutputResolutionRaw::SevenTwentyP), 5, KinoviBatchCountRaw::One);
let c1080 = usd_cents(Some(KinoviOutputResolutionRaw::TenEightyP), 5, KinoviBatchCountRaw::One);
assert!(c720 < c1080, "720p ({}) should be cheaper than 1080p ({})", c720, c1080);
fn r1080p_is_double_720p() {
for dur in 3..=15u8 {
let c720 = credits(None, dur);
let c1080 = credits(Some(KinoviHappyHorse1p0OutputResolution::TenEightyP), dur);
assert_eq!(c1080, c720 * 2, "1080p should be 2× 720p at {}s", dur);
}
}
#[test]
fn cost_scales_with_duration() {
let c4 = usd_cents(None, 4, KinoviBatchCountRaw::One);
let c10 = usd_cents(None, 10, KinoviBatchCountRaw::One);
let c15 = usd_cents(None, 15, KinoviBatchCountRaw::One);
assert!(c4 < c10);
let c3 = credits(None, 3);
let c10 = credits(None, 10);
let c15 = credits(None, 15);
assert!(c3 < c10);
assert!(c10 < c15);
}
#[test]
fn cost_scales_with_batch() {
let b1 = usd_cents(None, 5, KinoviBatchCountRaw::One);
let b2 = usd_cents(None, 5, KinoviBatchCountRaw::Two);
let b4 = usd_cents(None, 5, KinoviBatchCountRaw::Four);
assert!(b1 < b2);
assert!(b2 < b4);
}
#[test]
fn not_free() {
let est = KinoviHappyHorse1p0CostState {
resolution: None, duration_seconds: 5, batch_count: KinoviBatchCountRaw::One,
resolution: None, duration_seconds: 5, batch_count: None,
}.estimate_cost();
assert!(!est.is_free);
}
}
// ── from_draft() tests ──
// ── from_draft() ──
mod from_draft_tests {
use super::*;
@@ -196,22 +205,19 @@ mod tests {
fn from_draft_720p_default() {
let draft = make_draft(5, 1, None);
let cost = KinoviHappyHorse1p0CostState::from_draft(&draft);
assert!(cost.resolution.is_none());
assert_eq!(cost.duration_seconds, 5);
assert!(matches!(cost.batch_count, KinoviBatchCountRaw::One));
assert_eq!(cost.estimate_cost().cost_in_credits, Some(165));
}
#[test]
fn from_draft_1080p_batch_4() {
let draft = make_draft(5, 4, Some(CommonResolution::TenEightyP));
let cost = KinoviHappyHorse1p0CostState::from_draft(&draft);
assert!(matches!(cost.resolution, Some(KinoviOutputResolutionRaw::TenEightyP)));
assert!(matches!(cost.batch_count, KinoviBatchCountRaw::Four));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(933));
assert_eq!(cost.estimate_cost().cost_in_credits, Some(330 * 4));
}
}
// ── from_request() tests ──
// ── from_request() ──
mod from_request_tests {
use super::*;
@@ -220,9 +226,7 @@ mod tests {
fn from_request_default() {
let req = make_request_state(None, 5, None);
let cost = KinoviHappyHorse1p0CostState::from_request(&req);
assert!(cost.resolution.is_none());
assert_eq!(cost.duration_seconds, 5);
assert!(matches!(cost.batch_count, KinoviBatchCountRaw::One));
assert_eq!(cost.estimate_cost().cost_in_credits, Some(165));
}
#[test]
@@ -231,18 +235,18 @@ mod tests {
Some(KinoviHappyHorse1p0OutputResolution::TenEightyP), 5, None,
);
let cost = KinoviHappyHorse1p0CostState::from_request(&req);
assert!(matches!(cost.resolution, Some(KinoviOutputResolutionRaw::TenEightyP)));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(233));
assert_eq!(cost.estimate_cost().cost_in_credits, Some(330));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(171));
}
}
// ── Cross-check: from_draft matches from_request ──
// ── Cross-check: draft matches request ──
mod cross_check {
use super::*;
#[test]
fn draft_and_request_produce_same_cost_720p() {
fn draft_and_request_produce_same_cost() {
let draft = make_draft(5, 1, Some(CommonResolution::SevenTwentyP));
let req = make_request_state(
Some(KinoviHappyHorse1p0OutputResolution::SevenTwentyP), 5, None,
@@ -254,7 +258,7 @@ mod tests {
}
#[test]
fn draft_and_request_produce_same_cost_1080p() {
fn draft_and_request_produce_same_cost_1080p_batch_2() {
let draft = make_draft(10, 2, Some(CommonResolution::TenEightyP));
let req = make_request_state(
Some(KinoviHappyHorse1p0OutputResolution::TenEightyP), 10,
@@ -267,38 +271,30 @@ mod tests {
}
}
// ── Credits spot checks ──
mod credits {
use super::*;
#[test]
fn credits_720p() {
let est = KinoviHappyHorse1p0CostState {
resolution: Some(KinoviOutputResolutionRaw::SevenTwentyP),
duration_seconds: 5,
batch_count: KinoviBatchCountRaw::One,
}.estimate_cost();
assert_eq!(est.cost_in_credits, Some(200));
}
#[test]
fn credits_1080p() {
let est = KinoviHappyHorse1p0CostState {
resolution: Some(KinoviOutputResolutionRaw::TenEightyP),
duration_seconds: 5,
batch_count: KinoviBatchCountRaw::One,
}.estimate_cost();
assert_eq!(est.cost_in_credits, Some(450));
}
}
// ── Helpers ──
fn usd_cents(
resolution: Option<KinoviOutputResolutionRaw>,
fn credits(
resolution: Option<KinoviHappyHorse1p0OutputResolution>,
duration_seconds: u8,
batch_count: KinoviBatchCountRaw,
) -> u64 {
credits_with_batch(resolution, duration_seconds, None)
}
fn credits_with_batch(
resolution: Option<KinoviHappyHorse1p0OutputResolution>,
duration_seconds: u8,
batch_count: Option<KinoviHappyHorse1p0BatchCount>,
) -> u64 {
KinoviHappyHorse1p0CostState { resolution, duration_seconds, batch_count }
.estimate_cost()
.cost_in_credits
.unwrap()
}
fn usd_cents(
resolution: Option<KinoviHappyHorse1p0OutputResolution>,
duration_seconds: u8,
batch_count: Option<KinoviHappyHorse1p0BatchCount>,
) -> u64 {
KinoviHappyHorse1p0CostState { resolution, duration_seconds, batch_count }
.estimate_cost()
@@ -335,7 +331,7 @@ mod tests {
batch_count: Option<KinoviHappyHorse1p0BatchCount>,
) -> KinoviHappyHorse1p0RequestState {
KinoviHappyHorse1p0RequestState {
request: seedance2pro_client::generate::video::generate_happy_horse_1p0::GenerateHappyHorse1p0Request {
request: GenerateHappyHorse1p0Request {
prompt: "test".to_string(),
aspect_ratio: None,
output_resolution: resolution,