fixup rates

This commit is contained in:
Hanashi
2026-08-14 00:14:53 -04:00
parent 8b0eeb3f15
commit e3efbdc92d
5 changed files with 496 additions and 698 deletions
@@ -2,6 +2,7 @@ use kinovi_web_client::generate::video::generate_seedance_2p0_fast::{
GenerateSeedance2p0FastRequest, KinoviSeedance2p0FastBatchCount, GenerateSeedance2p0FastRequest, KinoviSeedance2p0FastBatchCount,
KinoviSeedance2p0FastOutputResolution, KinoviSeedance2p0FastOutputResolution,
}; };
use kinovi_web_client::pricing::kinovi_cost_calculator_trait::KinoviCostCalculatorTrait;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate; use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video::providers::kinovi::seedance_2p0_fast::draft::KinoviSeedance2p0FastDraftState; use crate::generate::generate_video::providers::kinovi::seedance_2p0_fast::draft::KinoviSeedance2p0FastDraftState;
@@ -66,8 +67,13 @@ impl KinoviSeedance2p0FastCostState {
bitrate: None, bitrate: None,
}; };
let costs = pricing_request.calculate_costs(); // Enterprise tier: what generations actually cost us (our discounted
let cost_in_credits = costs.total_cost.kinovi_credits; // per-model credit rate at our bulk credit purchase rate).
let costs = pricing_request.calculate_enterprise_costs();
// Enterprise Fast credits can be fractional (e.g. 480p 5s = 52.5). The
// router's credit field is an integer, so round to the nearest credit;
// the USD cents (the authoritative charge) are already rounded up.
let cost_in_credits = costs.total_cost.kinovi_credits.round() as u64;
let cost_in_usd_cents = costs.total_cost.usd_cents_rounded_up; let cost_in_usd_cents = costs.total_cost.usd_cents_rounded_up;
VideoGenerationCostEstimate { VideoGenerationCostEstimate {
@@ -101,61 +107,61 @@ mod tests {
// ── Direct estimate_cost() tests ── // ── Direct estimate_cost() tests ──
// These must match the reference implementation values exactly. // These must match the reference implementation values exactly.
// -- 720p (28 credits/sec, 243 credits/$1) -- // -- 720p (enterprise 21 credits/sec, 243.16 credits/$1) --
mod pricing_720p { mod pricing_720p {
use super::*; use super::*;
#[test] #[test]
fn cost_720p_batch_1() { fn cost_720p_batch_1() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One), 47); assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One), 35);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 58); assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 44);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One), 116); assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One), 87);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One), 173); assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One), 130);
} }
#[test] #[test]
fn cost_720p_batch_2() { fn cost_720p_batch_2() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two), 116); assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two), 87);
} }
#[test] #[test]
fn cost_720p_batch_3() { fn cost_720p_batch_3() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Three), 173); assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Three), 130);
} }
#[test] #[test]
fn cost_720p_batch_4() { fn cost_720p_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Four), 231); assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Four), 173);
} }
} }
// -- 480p (14 credits/sec, 243 credits/$1) -- // -- 480p (enterprise 10.5 credits/sec, 243.16 credits/$1) --
mod pricing_480p { mod pricing_480p {
use super::*; use super::*;
#[test] #[test]
fn cost_480p_batch_1() { fn cost_480p_batch_1() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One), 24); assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One), 18);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 29); assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 22);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One), 58); assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One), 44);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One), 87); assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One), 65);
} }
#[test] #[test]
fn cost_480p_batch_2() { fn cost_480p_batch_2() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Two), 58); assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Two), 44);
} }
#[test] #[test]
fn cost_480p_batch_3() { fn cost_480p_batch_3() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Three), 87); assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Three), 65);
} }
#[test] #[test]
fn cost_480p_batch_4() { fn cost_480p_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 116); assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 87);
} }
} }
@@ -202,10 +208,11 @@ mod tests {
}; };
let without = base.estimate_cost(); let without = base.estimate_cost();
let with = KinoviSeedance2p0FastCostState { has_video_reference: true, ..base }.estimate_cost(); let with = KinoviSeedance2p0FastCostState { has_video_reference: true, ..base }.estimate_cost();
// Fast 720p surcharge is +6 credits/s: 140 -> 170 credits (17000/243 = 69.96 -> 70 cents). // Enterprise Fast 720p surcharge is +4.5 credits/s: 105 -> 127.5 credits
assert_eq!(without.cost_in_credits, Some(140)); // (12750/243.16 = 52.43 -> 53 cents; credits round to 128).
assert_eq!(with.cost_in_credits, Some(170)); assert_eq!(without.cost_in_credits, Some(105));
assert_eq!(with.cost_in_usd_cents, Some(70)); assert_eq!(with.cost_in_credits, Some(128));
assert_eq!(with.cost_in_usd_cents, Some(53));
} }
// ── from_request() tests ── // ── from_request() tests ──
@@ -221,7 +228,7 @@ mod tests {
assert_eq!(cost.duration_seconds, 5); assert_eq!(cost.duration_seconds, 5);
assert!(matches!(cost.batch_count, Some(KinoviBatchCount::One))); assert!(matches!(cost.batch_count, Some(KinoviBatchCount::One)));
assert!(!cost.has_video_reference); assert!(!cost.has_video_reference);
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(58)); assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(44));
} }
#[test] #[test]
@@ -229,14 +236,14 @@ mod tests {
let req = make_request_state(None, 5, KinoviBatchCount::One, false); let req = make_request_state(None, 5, KinoviBatchCount::One, false);
let cost = KinoviSeedance2p0FastCostState::from_request(&req); let cost = KinoviSeedance2p0FastCostState::from_request(&req);
assert!(cost.resolution.is_none()); assert!(cost.resolution.is_none());
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(58)); assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(44));
} }
#[test] #[test]
fn from_request_480p() { fn from_request_480p() {
let req = make_request_state(Some(KinoviOutputResolution::FourEightyP), 5, KinoviBatchCount::One, false); let req = make_request_state(Some(KinoviOutputResolution::FourEightyP), 5, KinoviBatchCount::One, false);
let cost = KinoviSeedance2p0FastCostState::from_request(&req); let cost = KinoviSeedance2p0FastCostState::from_request(&req);
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(29)); assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(22));
} }
#[test] #[test]
@@ -244,9 +251,9 @@ mod tests {
let req = make_request_state(Some(KinoviOutputResolution::SevenTwentyP), 5, KinoviBatchCount::One, true); let req = make_request_state(Some(KinoviOutputResolution::SevenTwentyP), 5, KinoviBatchCount::One, true);
let cost = KinoviSeedance2p0FastCostState::from_request(&req); let cost = KinoviSeedance2p0FastCostState::from_request(&req);
assert!(cost.has_video_reference); assert!(cost.has_video_reference);
// Video refs add a +6 credits/s surcharge at 720p: 170 credits -> 89 cents. // Enterprise video refs add +4.5 credits/s at 720p: 127.5 credits -> 53 cents.
assert_eq!(cost.estimate_cost().cost_in_credits, Some(170)); assert_eq!(cost.estimate_cost().cost_in_credits, Some(128));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(70)); assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(53));
} }
#[test] #[test]
@@ -271,7 +278,7 @@ mod tests {
assert_eq!(cost.duration_seconds, 5); assert_eq!(cost.duration_seconds, 5);
assert!(matches!(cost.batch_count, Some(KinoviBatchCount::One))); assert!(matches!(cost.batch_count, Some(KinoviBatchCount::One)));
assert!(!cost.has_video_reference); assert!(!cost.has_video_reference);
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(58)); assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(44));
} }
#[test] #[test]
@@ -279,7 +286,7 @@ mod tests {
let draft = make_draft(5, 1, Some(RouterResolution::FourEightyP), false); let draft = make_draft(5, 1, Some(RouterResolution::FourEightyP), false);
let cost = KinoviSeedance2p0FastCostState::from_draft(&draft); let cost = KinoviSeedance2p0FastCostState::from_draft(&draft);
assert!(matches!(cost.resolution, Some(KinoviOutputResolution::FourEightyP))); assert!(matches!(cost.resolution, Some(KinoviOutputResolution::FourEightyP)));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(29)); assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(22));
} }
#[test] #[test]
@@ -287,9 +294,9 @@ mod tests {
let draft = make_draft(5, 1, None, true); let draft = make_draft(5, 1, None, true);
let cost = KinoviSeedance2p0FastCostState::from_draft(&draft); let cost = KinoviSeedance2p0FastCostState::from_draft(&draft);
assert!(cost.has_video_reference); assert!(cost.has_video_reference);
// Video refs add a +6 credits/s surcharge at 720p: 170 credits -> 89 cents. // Enterprise video refs add +4.5 credits/s at 720p: 127.5 credits -> 53 cents.
assert_eq!(cost.estimate_cost().cost_in_credits, Some(170)); assert_eq!(cost.estimate_cost().cost_in_credits, Some(128));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(70)); assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(53));
} }
#[test] #[test]
@@ -305,7 +312,7 @@ mod tests {
let cost = KinoviSeedance2p0FastCostState::from_draft(&draft); let cost = KinoviSeedance2p0FastCostState::from_draft(&draft);
assert_eq!(cost.duration_seconds, 15); assert_eq!(cost.duration_seconds, 15);
assert!(matches!(cost.batch_count, Some(KinoviBatchCount::Two))); assert!(matches!(cost.batch_count, Some(KinoviBatchCount::Two)));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(346)); assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(260));
} }
} }
@@ -343,12 +350,14 @@ mod tests {
#[test] #[test]
fn credits_720p() { fn credits_720p() {
assert_eq!(credits(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 140); // Enterprise 21 credits/s × 5s = 105.
assert_eq!(credits(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 105);
} }
#[test] #[test]
fn credits_480p() { fn credits_480p() {
assert_eq!(credits(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 70); // Enterprise 10.5 credits/s × 5s = 52.5, rounded to 53.
assert_eq!(credits(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 53);
} }
} }
File diff suppressed because it is too large Load Diff
@@ -1,32 +1,12 @@
use crate::pricing::cost::kinovi_fractional_generation_cost::KinoviFractionalGenerationCost; use crate::pricing::cost::kinovi_fractional_generation_cost::KinoviFractionalGenerationCost;
use crate::pricing::kinovi_pricing_tier::KinoviPricingTier; use crate::pricing::kinovi_pricing_tier::KinoviPricingTier;
// ───────────────────────────────────────────────────────────────────────────── /// The cost of a Kinovi Seedance generation (Seedance 2.0, Fast, and Mini),
// WHY THIS TYPE EXISTS (don't merge it into KinoviSeedanceGenerationCost): /// with the video-reference surcharge broken out from the base price.
//
// This is the fractional twin of `KinoviSeedanceGenerationCost`. It is
// structurally identical (base + optional video-reference surcharge = total)
// and prices the SAME way conceptually — but its credits are fractional
// (`KinoviFractionalGenerationCost`, `f64`) rather than whole
// (`KinoviGenerationCost`, `u64`).
//
// Seedance 2.0 Mini at 480p bills 7.5 credits/sec, so odd durations land on
// FRACTIONAL credits (5s = 37.5, 15s = 112.5), and Seedance 2.0's enterprise
// rates are fractional (37.9 credits/sec at 720p). The remaining whole-credit
// Seedance video models use `KinoviSeedanceGenerationCost` (u64 credits).
//
// Making the shared `u64` credit type fractional would ripple `f64` through
// those whole-credit models AND `artcraft_router` (which stores credits as
// `u64`), forcing hundreds of integer-literal edits. Keeping fractional
// credits in their own small type is the cheaper, better-isolated choice.
// ─────────────────────────────────────────────────────────────────────────────
/// The cost of a fractional-credit Kinovi Seedance generation (Seedance 2.0,
/// Seedance 2.0 Mini), with the video-reference surcharge broken out from the
/// base price.
/// ///
/// Mirrors [`crate::pricing::cost::kinovi_seedance_generation_cost::KinoviSeedanceGenerationCost`], /// Credits are fractional (`f64`): Mini's consumer 480p rate is 7.5/sec, and
/// but with FRACTIONAL credits (see the module note above). /// the enterprise discount rates land on fractional credits at most
/// durations (e.g. Seedance 2.0 at 37.9/sec).
/// ///
/// `total_cost` covers base + surcharge. NB: the total's USD conversions are /// `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 /// computed from the SUMMED credits (rounded once), so they may differ by a
@@ -1,117 +0,0 @@
use crate::pricing::cost::kinovi_generation_cost::KinoviGenerationCost;
/// The cost of a Kinovi Seedance generation, with the video-reference
/// surcharge broken out from the base price.
///
/// Used by the WHOLE-credit Seedance video models (currently Fast). Models
/// that bill fractional credits (Seedance 2.0, Seedance 2.0 Mini) use the
/// `f64` twin,
/// [`crate::pricing::cost::kinovi_seedance_fractional_generation_cost::KinoviSeedanceFractionalGenerationCost`]
/// — see that file for why they aren't merged.
///
/// `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<KinoviGenerationCost>,
}
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<u64>,
) -> 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);
}
}
@@ -1,4 +1,3 @@
pub mod kinovi_fractional_generation_cost; pub mod kinovi_fractional_generation_cost;
pub mod kinovi_generation_cost; pub mod kinovi_generation_cost;
pub mod kinovi_seedance_fractional_generation_cost; pub mod kinovi_seedance_fractional_generation_cost;
pub mod kinovi_seedance_generation_cost;