This commit is contained in:
Brandon Thomas committed 2026-06-25 07:38:42 -04:00
1 parent d400571ca0
commit 1a09b4dd95
4 files changed
+140 -112

No files matched your search

@@ -0,0 +1,85 @@
use crate::cost::constants::CREDITS_PER_DOLLAR;
/// The cost of a Kinovi generation that bills FRACTIONAL credits, in both
/// native credits and USD cents.
///
/// This is the fractional-credit counterpart of
/// [`crate::cost::kinovi_generation_cost::KinoviGenerationCost`] (which uses
/// `u64`). Some Kinovi models bill non-integer credits — e.g. Seedance 2.0
/// Mini at 480p bills 7.5 credits/sec, landing on half-credits like 37.5 at
/// odd durations. Any model with a fractional credit rate uses this type;
/// whole-credit models use the cheaper `u64` `KinoviGenerationCost`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct KinoviFractionalGenerationCost {
/// The cost of the generation in Kinovi credits. May be fractional (e.g.
/// 37.5 for Seedance 2.0 Mini 480p at 5 seconds).
pub kinovi_credits: f64,
/// Estimated cost in cents, rounded up to the nearest whole cent.
/// This does not account for discounts, prorations, etc.
pub usd_cents_rounded_up: u64,
/// Estimated cost in cents, rounded down to the nearest whole cent.
pub usd_cents_rounded_down: u64,
/// Estimated cost in cents without rounding (the exact fractional value).
pub usd_cents_fractional: f64,
}
impl KinoviFractionalGenerationCost {
/// Build a cost from a (possibly fractional) Kinovi credit amount, deriving
/// the USD cents via the credit-package rate.
///
/// The conversion is done on integer hundredths-of-a-credit, so the integer
/// cent results are exact (no floating-point drift). Credit amounts are
/// rounded to the nearest hundredth of a credit, which covers every Kinovi
/// rate seen so far (e.g. Mini's half-credits).
pub fn from_kinovi_credits(kinovi_credits: f64) -> Self {
// usd_cents == credits × 100 / CREDITS_PER_DOLLAR == hundredths / CREDITS_PER_DOLLAR.
let hundredths_of_credit = (kinovi_credits * 100.0).round() as u64;
Self {
kinovi_credits,
usd_cents_rounded_up: hundredths_of_credit.div_ceil(CREDITS_PER_DOLLAR),
usd_cents_rounded_down: hundredths_of_credit / CREDITS_PER_DOLLAR,
usd_cents_fractional: hundredths_of_credit as f64 / CREDITS_PER_DOLLAR as f64,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const FLOAT_TOLERANCE: f64 = 1e-9;
#[test]
fn fractional_credits_convert_to_usd() {
// 37.5 credits; 3750/243 = 15.4320 cents.
let cost = KinoviFractionalGenerationCost::from_kinovi_credits(37.5);
assert_eq!(cost.kinovi_credits, 37.5);
assert_eq!(cost.usd_cents_rounded_up, 16);
assert_eq!(cost.usd_cents_rounded_down, 15);
assert!((cost.usd_cents_fractional - (3750.0 / 243.0)).abs() < FLOAT_TOLERANCE);
}
#[test]
fn whole_credits_convert_to_usd() {
// 100 credits; 10000/243 = 41.1522 cents.
let cost = KinoviFractionalGenerationCost::from_kinovi_credits(100.0);
assert_eq!(cost.kinovi_credits, 100.0);
assert_eq!(cost.usd_cents_rounded_up, 42);
assert_eq!(cost.usd_cents_rounded_down, 41);
assert!((cost.usd_cents_fractional - (10000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
}
#[test]
fn rounded_bounds_bracket_the_fractional_value() {
for half_credits in [0u64, 1, 15, 75, 95, 200, 225, 600, 720] {
let credits = half_credits as f64 / 2.0;
let cost = KinoviFractionalGenerationCost::from_kinovi_credits(credits);
assert!(cost.usd_cents_rounded_down as f64 <= cost.usd_cents_fractional);
assert!(cost.usd_cents_fractional <= cost.usd_cents_rounded_up as f64);
assert!(cost.usd_cents_rounded_up - cost.usd_cents_rounded_down <= 1);
}
}
}
@@ -3,6 +3,11 @@ use crate::cost::kinovi_generation_cost::KinoviGenerationCost;
/// The cost of a Kinovi Seedance generation, with the video-reference
/// surcharge broken out from the base price.
///
/// Used by the WHOLE-credit Seedance video models (regular + Fast). Seedance
/// 2.0 Mini bills fractional credits and has its own `f64` twin,
/// [`crate::cost::kinovi_seedance_mini_generation_cost::KinoviSeedanceMiniGenerationCost`]
/// — 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.
@@ -1,12 +1,31 @@
use crate::cost::constants::CREDITS_PER_DOLLAR;
use crate::cost::kinovi_fractional_generation_cost::KinoviFractionalGenerationCost;
// ─────────────────────────────────────────────────────────────────────────────
// WHY THIS TYPE EXISTS (don't merge it into KinoviSeedanceGenerationCost):
//
// This is the Mini 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). Every OTHER whole-credit
// Seedance video model uses `KinoviSeedanceGenerationCost` (u64 credits).
//
// Making the shared `u64` credit type fractional would ripple `f64` through all
// of those whole-credit models AND `artcraft_router` (which stores credits as
// `u64`), forcing hundreds of integer-literal edits for one model's benefit.
// Keeping Mini's fractional credits in their own small type is the cheaper,
// better-isolated choice. The inner `KinoviFractionalGenerationCost` is generic
// (not Mini-specific) — other fractional-credit models can reuse it.
// ─────────────────────────────────────────────────────────────────────────────
/// The cost of a Kinovi Seedance 2.0 Mini generation, with the video-reference
/// surcharge broken out from the base price.
///
/// Unlike the other Seedance models, Mini credits can be FRACTIONAL (480p is
/// 7.5 credits/sec, so odd durations land on half-credits like 37.5). Credits
/// are therefore tracked as `f64`. USD conversions are derived from the exact
/// fractional credit amount.
/// Mirrors [`crate::cost::kinovi_seedance_generation_cost::KinoviSeedanceGenerationCost`],
/// but with FRACTIONAL credits (see the module note above).
///
/// `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
@@ -14,15 +33,15 @@ use crate::cost::constants::CREDITS_PER_DOLLAR;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct KinoviSeedanceMiniGenerationCost {
/// The full cost (base + any surcharge), in credits and USD cents.
pub total_cost: KinoviMiniGenerationCost,
pub total_cost: KinoviFractionalGenerationCost,
/// The base generation cost (resolution rate × output duration × batch
/// count), excluding any surcharges.
pub base_cost: KinoviMiniGenerationCost,
pub base_cost: KinoviFractionalGenerationCost,
/// 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<KinoviMiniGenerationCost>,
pub video_reference_surcharge_cost: Option<KinoviFractionalGenerationCost>,
}
impl KinoviSeedanceMiniGenerationCost {
@@ -35,51 +54,10 @@ impl KinoviSeedanceMiniGenerationCost {
) -> Self {
let total_credits = base_credits + maybe_video_reference_surcharge_credits.unwrap_or(0.0);
Self {
total_cost: KinoviMiniGenerationCost::from_kinovi_credits(total_credits),
base_cost: KinoviMiniGenerationCost::from_kinovi_credits(base_credits),
total_cost: KinoviFractionalGenerationCost::from_kinovi_credits(total_credits),
base_cost: KinoviFractionalGenerationCost::from_kinovi_credits(base_credits),
video_reference_surcharge_cost: maybe_video_reference_surcharge_credits
.map(KinoviMiniGenerationCost::from_kinovi_credits),
}
}
}
/// The cost of a Kinovi Seedance 2.0 Mini generation, in both native credits
/// (fractional) and USD cents.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct KinoviMiniGenerationCost {
/// The cost of the generation in Kinovi credits. May be fractional (e.g.
/// 37.5 for 480p at 5 seconds).
pub kinovi_credits: f64,
/// Estimated cost in cents, rounded up to the nearest whole cent.
/// This does not account for discounts, prorations, etc.
pub usd_cents_rounded_up: u64,
/// Estimated cost in cents, rounded down to the nearest whole cent.
pub usd_cents_rounded_down: u64,
/// Estimated cost in cents without rounding (the exact fractional value).
pub usd_cents_fractional: f64,
}
impl KinoviMiniGenerationCost {
/// Build a cost from a (possibly fractional) Kinovi credit amount, deriving
/// the USD cents via the credit-package rate.
///
/// Mini credits are always multiples of 0.5, so the USD conversion is done
/// on integer half-credits to keep it exact (no floating-point rounding in
/// the integer cent results).
pub fn from_kinovi_credits(kinovi_credits: f64) -> Self {
// half_credits = credits × 2 (exact, since credits is a multiple of 0.5).
// credits × 100 == half_credits × 50, so this is the same numerator the
// integer KinoviGenerationCost uses, computed without fractional drift.
let half_credits = (kinovi_credits * 2.0).round() as u64;
let total_hundredths = half_credits * 50;
Self {
kinovi_credits,
usd_cents_rounded_up: total_hundredths.div_ceil(CREDITS_PER_DOLLAR),
usd_cents_rounded_down: total_hundredths / CREDITS_PER_DOLLAR,
usd_cents_fractional: total_hundredths as f64 / CREDITS_PER_DOLLAR as f64,
.map(KinoviFractionalGenerationCost::from_kinovi_credits),
}
}
}
@@ -88,67 +66,26 @@ impl KinoviMiniGenerationCost {
mod tests {
use super::*;
const FLOAT_TOLERANCE: f64 = 1e-9;
mod generation_cost {
use super::*;
#[test]
fn fractional_credits_convert_to_usd() {
// 37.5 credits; 3750/243 = 15.4320 cents.
let cost = KinoviMiniGenerationCost::from_kinovi_credits(37.5);
assert_eq!(cost.kinovi_credits, 37.5);
assert_eq!(cost.usd_cents_rounded_up, 16);
assert_eq!(cost.usd_cents_rounded_down, 15);
assert!((cost.usd_cents_fractional - (3750.0 / 243.0)).abs() < FLOAT_TOLERANCE);
}
#[test]
fn whole_credits_convert_to_usd() {
// 100 credits; 10000/243 = 41.1522 cents.
let cost = KinoviMiniGenerationCost::from_kinovi_credits(100.0);
assert_eq!(cost.kinovi_credits, 100.0);
assert_eq!(cost.usd_cents_rounded_up, 42);
assert_eq!(cost.usd_cents_rounded_down, 41);
assert!((cost.usd_cents_fractional - (10000.0 / 243.0)).abs() < FLOAT_TOLERANCE);
}
#[test]
fn rounded_bounds_bracket_the_fractional_value() {
for half_credits in [0u64, 1, 15, 75, 95, 200, 225, 600, 720] {
let credits = half_credits as f64 / 2.0;
let cost = KinoviMiniGenerationCost::from_kinovi_credits(credits);
assert!(cost.usd_cents_rounded_down as f64 <= cost.usd_cents_fractional);
assert!(cost.usd_cents_fractional <= cost.usd_cents_rounded_up as f64);
assert!(cost.usd_cents_rounded_up - cost.usd_cents_rounded_down <= 1);
}
}
#[test]
fn without_surcharge_total_equals_base() {
let cost = KinoviSeedanceMiniGenerationCost::from_base_and_surcharge(37.5, None);
assert!(cost.video_reference_surcharge_cost.is_none());
assert_eq!(cost.base_cost.kinovi_credits, 37.5);
assert_eq!(cost.total_cost, cost.base_cost);
}
mod seedance_mini_cost {
use super::*;
#[test]
fn without_surcharge_total_equals_base() {
let cost = KinoviSeedanceMiniGenerationCost::from_base_and_surcharge(37.5, None);
assert!(cost.video_reference_surcharge_cost.is_none());
assert_eq!(cost.base_cost.kinovi_credits, 37.5);
assert_eq!(cost.total_cost, cost.base_cost);
}
#[test]
fn with_surcharge_sums_into_total() {
// 480p 5s with video ref: 37.5 base + 10 surcharge = 47.5 total.
let cost = KinoviSeedanceMiniGenerationCost::from_base_and_surcharge(37.5, Some(10.0));
assert_eq!(cost.base_cost.kinovi_credits, 37.5);
assert_eq!(
cost.video_reference_surcharge_cost.map(|c| c.kinovi_credits),
Some(10.0),
);
assert_eq!(cost.total_cost.kinovi_credits, 47.5);
// 47.5 credits; 4750/243 = 19.5473 → 20¢.
assert_eq!(cost.total_cost.usd_cents_rounded_up, 20);
assert_eq!(cost.total_cost.usd_cents_rounded_down, 19);
}
#[test]
fn with_surcharge_sums_into_total() {
// 480p 5s with video ref: 37.5 base + 10 surcharge = 47.5 total.
let cost = KinoviSeedanceMiniGenerationCost::from_base_and_surcharge(37.5, Some(10.0));
assert_eq!(cost.base_cost.kinovi_credits, 37.5);
assert_eq!(
cost.video_reference_surcharge_cost.map(|c| c.kinovi_credits),
Some(10.0),
);
assert_eq!(cost.total_cost.kinovi_credits, 47.5);
// 47.5 credits; 4750/243 = 19.5473 → 20¢.
assert_eq!(cost.total_cost.usd_cents_rounded_up, 20);
assert_eq!(cost.total_cost.usd_cents_rounded_down, 19);
}
}
@@ -1,4 +1,5 @@
pub mod constants;
pub mod kinovi_fractional_generation_cost;
pub mod kinovi_generation_cost;
pub mod kinovi_seedance_generation_cost;
pub mod kinovi_seedance_mini_generation_cost;