mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-20 17:02:53 -04:00
fixup rates
This commit is contained in:
+45
-36
@@ -2,6 +2,7 @@ use kinovi_web_client::generate::video::generate_seedance_2p0_fast::{
|
||||
GenerateSeedance2p0FastRequest, KinoviSeedance2p0FastBatchCount,
|
||||
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::providers::kinovi::seedance_2p0_fast::draft::KinoviSeedance2p0FastDraftState;
|
||||
@@ -66,8 +67,13 @@ impl KinoviSeedance2p0FastCostState {
|
||||
bitrate: None,
|
||||
};
|
||||
|
||||
let costs = pricing_request.calculate_costs();
|
||||
let cost_in_credits = costs.total_cost.kinovi_credits;
|
||||
// Enterprise tier: what generations actually cost us (our discounted
|
||||
// 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;
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
@@ -101,61 +107,61 @@ mod tests {
|
||||
// ── Direct estimate_cost() tests ──
|
||||
// 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 {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_720p_batch_1() {
|
||||
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);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One), 35);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 44);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One), 87);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One), 130);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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]
|
||||
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]
|
||||
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 {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_480p_batch_1() {
|
||||
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);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One), 18);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 22);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One), 44);
|
||||
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One), 65);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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]
|
||||
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]
|
||||
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 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).
|
||||
assert_eq!(without.cost_in_credits, Some(140));
|
||||
assert_eq!(with.cost_in_credits, Some(170));
|
||||
assert_eq!(with.cost_in_usd_cents, Some(70));
|
||||
// Enterprise Fast 720p surcharge is +4.5 credits/s: 105 -> 127.5 credits
|
||||
// (12750/243.16 = 52.43 -> 53 cents; credits round to 128).
|
||||
assert_eq!(without.cost_in_credits, Some(105));
|
||||
assert_eq!(with.cost_in_credits, Some(128));
|
||||
assert_eq!(with.cost_in_usd_cents, Some(53));
|
||||
}
|
||||
|
||||
// ── from_request() tests ──
|
||||
@@ -221,7 +228,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(58));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(44));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -229,14 +236,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(58));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(44));
|
||||
}
|
||||
|
||||
#[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(29));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(22));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -244,9 +251,9 @@ mod tests {
|
||||
let req = make_request_state(Some(KinoviOutputResolution::SevenTwentyP), 5, KinoviBatchCount::One, true);
|
||||
let cost = KinoviSeedance2p0FastCostState::from_request(&req);
|
||||
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(70));
|
||||
// Enterprise video refs add +4.5 credits/s at 720p: 127.5 credits -> 53 cents.
|
||||
assert_eq!(cost.estimate_cost().cost_in_credits, Some(128));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(53));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -271,7 +278,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(58));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(44));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -279,7 +286,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(29));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(22));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -287,9 +294,9 @@ mod tests {
|
||||
let draft = make_draft(5, 1, None, true);
|
||||
let cost = KinoviSeedance2p0FastCostState::from_draft(&draft);
|
||||
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(70));
|
||||
// Enterprise video refs add +4.5 credits/s at 720p: 127.5 credits -> 53 cents.
|
||||
assert_eq!(cost.estimate_cost().cost_in_credits, Some(128));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(53));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -305,7 +312,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(346));
|
||||
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(260));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,12 +350,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
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]
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+446
-519
File diff suppressed because it is too large
Load Diff
+5
-25
@@ -1,32 +1,12 @@
|
||||
use crate::pricing::cost::kinovi_fractional_generation_cost::KinoviFractionalGenerationCost;
|
||||
use crate::pricing::kinovi_pricing_tier::KinoviPricingTier;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WHY THIS TYPE EXISTS (don't merge it into KinoviSeedanceGenerationCost):
|
||||
//
|
||||
// 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.
|
||||
/// The cost of a Kinovi Seedance generation (Seedance 2.0, Fast, and Mini),
|
||||
/// with the video-reference surcharge broken out from the base price.
|
||||
///
|
||||
/// Mirrors [`crate::pricing::cost::kinovi_seedance_generation_cost::KinoviSeedanceGenerationCost`],
|
||||
/// but with FRACTIONAL credits (see the module note above).
|
||||
/// Credits are fractional (`f64`): Mini's consumer 480p rate is 7.5/sec, and
|
||||
/// 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
|
||||
/// computed from the SUMMED credits (rounded once), so they may differ by a
|
||||
|
||||
-117
@@ -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_generation_cost;
|
||||
pub mod kinovi_seedance_fractional_generation_cost;
|
||||
pub mod kinovi_seedance_generation_cost;
|
||||
|
||||
Reference in New Issue
Block a user