Compare commits

...
Author SHA1 Message Date
Brandon Thomas 8672af848c add video ref costs 2026-06-11 22:55:25 -04:00
Brandon Thomas 361632da6d pricing 2026-06-11 22:39:38 -04:00
Brandon Thomas 45ce366a8f cleanup 2026-06-11 22:32:58 -04:00
Brandon Thomas 48929d9ccd pricing 2026-06-11 22:24:05 -04:00
Brandon Thomas e4c20ce7e8 log costs per generation 2026-06-11 18:57:09 -04:00
114 changed files with 1123 additions and 440 deletions
@@ -120,7 +120,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 3
"max_size": 11
}
},
{
@@ -60,7 +60,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -70,7 +70,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -210,7 +210,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -220,7 +220,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -30,7 +30,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 3
"max_size": 11
}
},
{
@@ -180,7 +180,7 @@
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 20
}
},
{
@@ -40,7 +40,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -50,7 +50,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -50,7 +50,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -60,7 +60,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -210,7 +210,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -220,7 +220,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -40,7 +40,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -50,7 +50,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -180,7 +180,7 @@
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 20
}
},
{
@@ -30,7 +30,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 3
"max_size": 11
}
},
{
@@ -190,7 +190,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -30,7 +30,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 3
"max_size": 11
}
}
],
@@ -1,12 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = ?,\n maybe_model_token = ?,\n\n maybe_input_source_token = ?,\n maybe_input_source_token_type = ?,\n\n maybe_download_url = ?,\n maybe_cover_image_media_file_token = ?,\n\n maybe_prompt_token = ?,\n maybe_wallet_ledger_entry_token = ?,\n\n maybe_raw_inference_text = ?,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n platform_type = ?,\n\n priority_level = ?,\n is_keepalive_required = ?,\n max_duration_seconds = ?,\n is_debug_request = ?,\n\n maybe_routing_tag = ?,\n\n maybe_debug_log_event_token = ?,\n\n frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = ?,\n maybe_model_token = ?,\n\n maybe_input_source_token = ?,\n maybe_input_source_token_type = ?,\n\n maybe_download_url = ?,\n maybe_cover_image_media_file_token = ?,\n\n maybe_prompt_token = ?,\n maybe_wallet_ledger_entry_token = ?,\n\n maybe_raw_inference_text = ?,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n platform_type = ?,\n\n maybe_external_third_party_cost_credits = ?,\n maybe_external_third_party_cost_usd_cents = ?,\n maybe_system_cost_credits = ?,\n maybe_system_cost_usd_cents = ?,\n\n priority_level = ?,\n is_keepalive_required = ?,\n max_duration_seconds = ?,\n is_debug_request = ?,\n\n maybe_routing_tag = ?,\n\n maybe_debug_log_event_token = ?,\n\n frontend_failure_category = ?,\n failure_reason = ?,\n\n status = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 31
"Right": 35
},
"nullable": []
},
"hash": "108731dc51d915ebb8b6ed391c5bae4c41ae6f2c813be92c07d8a10ca401db53"
"hash": "5c67c3dd7f8bb6771c6b058a8cf6ffcc66b2be2bea41bd988070713173982dab"
}
@@ -160,7 +160,7 @@
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 20
}
},
{
@@ -60,7 +60,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -70,7 +70,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -150,7 +150,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 3
"max_size": 11
}
},
{
@@ -160,7 +160,7 @@
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 20
}
},
{
@@ -190,7 +190,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -200,7 +200,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 5
"max_size": 11
}
},
{
@@ -210,7 +210,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 5
"max_size": 11
}
},
{
@@ -160,7 +160,7 @@
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 20
}
},
{
@@ -50,7 +50,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -60,7 +60,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -140,7 +140,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -150,7 +150,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 3
"max_size": 11
}
},
{
@@ -30,7 +30,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 3
"max_size": 11
}
},
{
@@ -160,7 +160,7 @@
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 20
}
},
{
@@ -270,7 +270,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -160,7 +160,7 @@
"type": "LongLong",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 20
}
},
{
@@ -190,7 +190,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -200,7 +200,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 5
"max_size": 11
}
},
{
@@ -210,7 +210,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 5
"max_size": 11
}
},
{
@@ -150,7 +150,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 3
"max_size": 11
}
},
{
@@ -180,7 +180,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -190,7 +190,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -140,7 +140,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -180,7 +180,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -190,7 +190,7 @@
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -190,7 +190,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -140,7 +140,7 @@
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 10
"max_size": 11
}
},
{
@@ -141,21 +141,45 @@ mod tests {
fn batch_1_matches_kinovi() {
let kinovi = KinoviMidjourney7CostState { batch_count: KinoviMidjourneyBatchCount::One }
.estimate_cost();
assert_eq!(cost_cents(1), kinovi.cost_in_usd_cents.unwrap());
// NB: The kinovi provider estimate rounds fractional cents UP
// (KinoviGenerationCost) while this artcraft user price rounds to
// nearest — kinovi may exceed it by at most one cent.
let artcraft_cents = cost_cents(1);
let kinovi_cents = kinovi.cost_in_usd_cents.unwrap();
assert!(
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
"batch 1: artcraft={artcraft_cents}, kinovi={kinovi_cents}",
);
}
#[test]
fn batch_2_matches_kinovi() {
let kinovi = KinoviMidjourney7CostState { batch_count: KinoviMidjourneyBatchCount::Two }
.estimate_cost();
assert_eq!(cost_cents(2), kinovi.cost_in_usd_cents.unwrap());
// NB: The kinovi provider estimate rounds fractional cents UP
// (KinoviGenerationCost) while this artcraft user price rounds to
// nearest — kinovi may exceed it by at most one cent.
let artcraft_cents = cost_cents(2);
let kinovi_cents = kinovi.cost_in_usd_cents.unwrap();
assert!(
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
"batch 2: artcraft={artcraft_cents}, kinovi={kinovi_cents}",
);
}
#[test]
fn batch_4_matches_kinovi() {
let kinovi = KinoviMidjourney7CostState { batch_count: KinoviMidjourneyBatchCount::Four }
.estimate_cost();
assert_eq!(cost_cents(4), kinovi.cost_in_usd_cents.unwrap());
// NB: The kinovi provider estimate rounds fractional cents UP
// (KinoviGenerationCost) while this artcraft user price rounds to
// nearest — kinovi may exceed it by at most one cent.
let artcraft_cents = cost_cents(4);
let kinovi_cents = kinovi.cost_in_usd_cents.unwrap();
assert!(
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
"batch 4: artcraft={artcraft_cents}, kinovi={kinovi_cents}",
);
}
}
@@ -93,21 +93,45 @@ mod tests {
fn batch_1_matches_kinovi() {
let kinovi = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::One }
.estimate_cost();
assert_eq!(cost_cents(1), kinovi.cost_in_usd_cents.unwrap());
// NB: The kinovi provider estimate rounds fractional cents UP
// (KinoviGenerationCost) while this artcraft user price rounds to
// nearest — kinovi may exceed it by at most one cent.
let artcraft_cents = cost_cents(1);
let kinovi_cents = kinovi.cost_in_usd_cents.unwrap();
assert!(
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
"batch 1: artcraft={artcraft_cents}, kinovi={kinovi_cents}",
);
}
#[test]
fn batch_2_matches_kinovi() {
let kinovi = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::Two }
.estimate_cost();
assert_eq!(cost_cents(2), kinovi.cost_in_usd_cents.unwrap());
// NB: The kinovi provider estimate rounds fractional cents UP
// (KinoviGenerationCost) while this artcraft user price rounds to
// nearest — kinovi may exceed it by at most one cent.
let artcraft_cents = cost_cents(2);
let kinovi_cents = kinovi.cost_in_usd_cents.unwrap();
assert!(
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
"batch 2: artcraft={artcraft_cents}, kinovi={kinovi_cents}",
);
}
#[test]
fn batch_4_matches_kinovi() {
let kinovi = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::Four }
.estimate_cost();
assert_eq!(cost_cents(4), kinovi.cost_in_usd_cents.unwrap());
// NB: The kinovi provider estimate rounds fractional cents UP
// (KinoviGenerationCost) while this artcraft user price rounds to
// nearest — kinovi may exceed it by at most one cent.
let artcraft_cents = cost_cents(4);
let kinovi_cents = kinovi.cost_in_usd_cents.unwrap();
assert!(
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
"batch 4: artcraft={artcraft_cents}, kinovi={kinovi_cents}",
);
}
}
}
@@ -93,21 +93,45 @@ mod tests {
fn batch_1_matches_kinovi() {
let kinovi = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::One }
.estimate_cost();
assert_eq!(cost_cents(1), kinovi.cost_in_usd_cents.unwrap());
// NB: The kinovi provider estimate rounds fractional cents UP
// (KinoviGenerationCost) while this artcraft user price rounds to
// nearest — kinovi may exceed it by at most one cent.
let artcraft_cents = cost_cents(1);
let kinovi_cents = kinovi.cost_in_usd_cents.unwrap();
assert!(
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
"batch 1: artcraft={artcraft_cents}, kinovi={kinovi_cents}",
);
}
#[test]
fn batch_2_matches_kinovi() {
let kinovi = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::Two }
.estimate_cost();
assert_eq!(cost_cents(2), kinovi.cost_in_usd_cents.unwrap());
// NB: The kinovi provider estimate rounds fractional cents UP
// (KinoviGenerationCost) while this artcraft user price rounds to
// nearest — kinovi may exceed it by at most one cent.
let artcraft_cents = cost_cents(2);
let kinovi_cents = kinovi.cost_in_usd_cents.unwrap();
assert!(
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
"batch 2: artcraft={artcraft_cents}, kinovi={kinovi_cents}",
);
}
#[test]
fn batch_4_matches_kinovi() {
let kinovi = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::Four }
.estimate_cost();
assert_eq!(cost_cents(4), kinovi.cost_in_usd_cents.unwrap());
// NB: The kinovi provider estimate rounds fractional cents UP
// (KinoviGenerationCost) while this artcraft user price rounds to
// nearest — kinovi may exceed it by at most one cent.
let artcraft_cents = cost_cents(4);
let kinovi_cents = kinovi.cost_in_usd_cents.unwrap();
assert!(
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
"batch 4: artcraft={artcraft_cents}, kinovi={kinovi_cents}",
);
}
}
}
@@ -37,8 +37,9 @@ impl KinoviMidjourney7CostState {
reference_image_urls: None,
};
let cost_in_credits = pricing_request.estimate_credits() as u64;
let cost_in_usd_cents = pricing_request.estimate_cost_in_usd_cents();
let costs = pricing_request.calculate_costs();
let cost_in_credits = costs.kinovi_credits;
let cost_in_usd_cents = costs.usd_cents_rounded_up;
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_credits),
@@ -93,13 +94,13 @@ mod tests {
#[test]
fn batch_one_is_six_cents() {
// 12 / 193 × 100 = 6.21 → 6¢
assert_eq!(usd_cents_for(KinoviMidjourneyBatchCount::One), 6);
assert_eq!(usd_cents_for(KinoviMidjourneyBatchCount::One), 7); // rounds UP
}
#[test]
fn batch_two_is_twelve_cents() {
// 24 / 193 × 100 = 12.44 → 12¢
assert_eq!(usd_cents_for(KinoviMidjourneyBatchCount::Two), 12);
assert_eq!(usd_cents_for(KinoviMidjourneyBatchCount::Two), 13); // rounds UP
}
#[test]
@@ -35,8 +35,9 @@ impl KinoviMidjourney7NijiCostState {
reference_image_urls: None,
};
let cost_in_credits = pricing_request.estimate_credits() as u64;
let cost_in_usd_cents = pricing_request.estimate_cost_in_usd_cents();
let costs = pricing_request.calculate_costs();
let cost_in_credits = costs.kinovi_credits;
let cost_in_usd_cents = costs.usd_cents_rounded_up;
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_credits),
@@ -59,7 +60,7 @@ mod tests {
let cost = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::One }
.estimate_cost();
assert_eq!(cost.cost_in_credits, Some(12));
assert_eq!(cost.cost_in_usd_cents, Some(6));
assert_eq!(cost.cost_in_usd_cents, Some(7)); // 1200/193 = 6.22 -> rounds UP
}
#[test]
@@ -67,7 +68,7 @@ mod tests {
let cost = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::Two }
.estimate_cost();
assert_eq!(cost.cost_in_credits, Some(24));
assert_eq!(cost.cost_in_usd_cents, Some(12));
assert_eq!(cost.cost_in_usd_cents, Some(13)); // 2400/193 = 12.44 -> rounds UP
}
#[test]
@@ -35,8 +35,9 @@ impl KinoviMidjourney8CostState {
reference_image_urls: None,
};
let cost_in_credits = pricing_request.estimate_credits() as u64;
let cost_in_usd_cents = pricing_request.estimate_cost_in_usd_cents();
let costs = pricing_request.calculate_costs();
let cost_in_credits = costs.kinovi_credits;
let cost_in_usd_cents = costs.usd_cents_rounded_up;
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_credits),
@@ -59,7 +60,7 @@ mod tests {
let cost = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::One }
.estimate_cost();
assert_eq!(cost.cost_in_credits, Some(12));
assert_eq!(cost.cost_in_usd_cents, Some(6));
assert_eq!(cost.cost_in_usd_cents, Some(7)); // 1200/193 = 6.22 -> rounds UP
}
#[test]
@@ -67,7 +68,7 @@ mod tests {
let cost = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::Two }
.estimate_cost();
assert_eq!(cost.cost_in_credits, Some(24));
assert_eq!(cost.cost_in_usd_cents, Some(12));
assert_eq!(cost.cost_in_usd_cents, Some(13)); // 2400/193 = 12.44 -> rounds UP
}
#[test]
@@ -234,10 +234,16 @@ mod tests {
.estimate_cost()
.expect("kinovi estimate_cost");
assert_eq!(
artcraft_cost.cost_in_usd_cents, kinovi_cost.cost_in_usd_cents,
"USD cents mismatch: res={:?} dur={}s batch={}",
res, dur, batch,
// NB: The kinovi provider estimate rounds fractional cents UP
// (KinoviGenerationCost), while the artcraft user-facing price
// here rounds to nearest — so kinovi may exceed artcraft by at
// most one cent.
let artcraft_cents = artcraft_cost.cost_in_usd_cents.expect("artcraft cents");
let kinovi_cents = kinovi_cost.cost_in_usd_cents.expect("kinovi cents");
assert!(
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
"USD cents mismatch: res={:?} dur={}s batch={} (artcraft={}, kinovi={})",
res, dur, batch, artcraft_cents, kinovi_cents,
);
}
}
@@ -248,10 +248,16 @@ mod tests {
.estimate_cost()
.expect("kinovi estimate_cost");
assert_eq!(
artcraft_cost.cost_in_usd_cents, kinovi_cost.cost_in_usd_cents,
"USD cents mismatch: res={:?} dur={}s batch={}",
res, dur, batch,
// NB: The kinovi provider estimate rounds fractional cents UP
// (KinoviGenerationCost), while the artcraft user-facing price
// here rounds to nearest — so kinovi may exceed artcraft by at
// most one cent.
let artcraft_cents = artcraft_cost.cost_in_usd_cents.expect("artcraft cents");
let kinovi_cents = kinovi_cost.cost_in_usd_cents.expect("kinovi cents");
assert!(
kinovi_cents == artcraft_cents || kinovi_cents == artcraft_cents + 1,
"USD cents mismatch: res={:?} dur={}s batch={} (artcraft={}, kinovi={})",
res, dur, batch, artcraft_cents, kinovi_cents,
);
}
}
@@ -42,11 +42,12 @@ impl KinoviHappyHorse1p0CostState {
start_frame_url: None,
};
let cost_in_credits = pricing_request.estimate_credits();
let cost_in_usd_cents = pricing_request.estimate_cost_in_usd_cents();
let costs = pricing_request.calculate_costs();
let cost_in_credits = costs.kinovi_credits;
let cost_in_usd_cents = costs.usd_cents_rounded_up;
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_credits as u64),
cost_in_credits: Some(cost_in_credits),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
@@ -98,9 +99,9 @@ mod tests {
#[test]
fn usd_cents_720p() {
// 165 credits / 193 * 100 = 85.49 → 85¢
assert_eq!(usd_cents(None, 5, None), 85);
assert_eq!(usd_cents(None, 5, None), 86); // 16500/193 = 85.49 -> rounds UP
// 495 credits / 193 * 100 = 256.48 → 256¢
assert_eq!(usd_cents(None, 15, None), 256);
assert_eq!(usd_cents(None, 15, None), 257); // 49500/193 = 256.48 -> rounds UP
}
}
@@ -46,23 +46,31 @@ impl KinoviSeedance2p0CostState {
duration_seconds: self.duration_seconds,
batch_count: self.batch_count,
// PRESENCE of reference videos changes the price (per-second
// surcharge); the URL contents don't.
reference_video_urls: if self.has_video_reference {
Some(vec!["pricing-placeholder".to_string()])
} else {
None
},
// No impact on price
prompt: String::new(),
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 = pricing_request.estimate_credits();
let cost_in_usd_cents = pricing_request.estimate_cost_in_usd_cents();
let costs = pricing_request.calculate_costs();
let cost_in_credits = costs.kinovi_credits;
let cost_in_usd_cents = costs.usd_cents_rounded_up;
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_credits as u64),
cost_in_credits: Some(cost_in_credits),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
@@ -97,16 +105,16 @@ mod tests {
fn cost_720p_batch_1() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One), 83);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 104);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 6, KinoviBatchCount::One), 124);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 7, KinoviBatchCount::One), 145);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One), 207);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 6, KinoviBatchCount::One), 125);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 7, KinoviBatchCount::One), 146);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One), 208);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One), 311);
}
#[test]
fn cost_720p_batch_2() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::Two), 166);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two), 207);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two), 208);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::Two), 622);
}
@@ -125,7 +133,7 @@ mod tests {
#[test]
fn cost_480p_batch_1() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One), 31);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One), 32);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 39);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One), 78);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One), 117);
@@ -138,7 +146,7 @@ mod tests {
#[test]
fn cost_480p_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 155);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 156);
}
}
@@ -150,14 +158,14 @@ mod tests {
#[test]
fn cost_1080p_batch_1() {
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 4, KinoviBatchCount::One), 187);
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One), 233);
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 10, KinoviBatchCount::One), 466);
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 15, KinoviBatchCount::One), 699);
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::One), 234);
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 10, KinoviBatchCount::One), 467);
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 15, KinoviBatchCount::One), 700);
}
#[test]
fn cost_1080p_batch_2() {
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Two), 466);
assert_eq!(usd_cents(KinoviOutputResolution::TenEightyP, 5, KinoviBatchCount::Two), 467);
}
#[test]
@@ -202,7 +210,7 @@ mod tests {
// -- Video reference does NOT affect cost (yet) --
#[test]
fn video_reference_does_not_affect_cost() {
fn video_reference_adds_surcharge() {
let base = KinoviSeedance2p0CostState {
resolution: Some(KinoviOutputResolution::SevenTwentyP),
duration_seconds: 5,
@@ -211,8 +219,10 @@ mod tests {
};
let without = base.estimate_cost();
let with = KinoviSeedance2p0CostState { has_video_reference: true, ..base }.estimate_cost();
assert_eq!(without.cost_in_usd_cents, with.cost_in_usd_cents);
assert_eq!(without.cost_in_credits, with.cost_in_credits);
// 720p surcharge is +8 credits/s: 200 -> 240 credits (24000/193 = 124.35 -> 125 cents).
assert_eq!(without.cost_in_credits, Some(200));
assert_eq!(with.cost_in_credits, Some(240));
assert_eq!(with.cost_in_usd_cents, Some(125));
}
// ── from_request() tests ──
@@ -250,7 +260,7 @@ mod tests {
fn from_request_1080p_batch_2() {
let req = make_request_state(Some(KinoviOutputResolution::TenEightyP), 5, KinoviBatchCount::Two, false);
let cost = KinoviSeedance2p0CostState::from_request(&req);
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(466));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(467));
}
#[test]
@@ -258,8 +268,9 @@ mod tests {
let req = make_request_state(Some(KinoviOutputResolution::SevenTwentyP), 5, KinoviBatchCount::One, true);
let cost = KinoviSeedance2p0CostState::from_request(&req);
assert!(cost.has_video_reference);
// Video refs don't affect cost yet
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(104));
// Video refs add a +8 credits/s surcharge at 720p: 240 credits -> 125 cents.
assert_eq!(cost.estimate_cost().cost_in_credits, Some(240));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(125));
}
#[test]
@@ -308,8 +319,9 @@ mod tests {
let draft = make_draft(5, 1, None, true);
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
assert!(cost.has_video_reference);
// Video refs don't affect cost yet
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(104));
// Video refs add a +8 credits/s surcharge at 720p: 240 credits -> 125 cents.
assert_eq!(cost.estimate_cost().cost_in_credits, Some(240));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(125));
}
#[test]
@@ -46,23 +46,31 @@ impl KinoviSeedance2p0FastCostState {
duration_seconds: self.duration_seconds,
batch_count: self.batch_count,
// PRESENCE of reference videos changes the price (per-second
// surcharge); the URL contents don't.
reference_video_urls: if self.has_video_reference {
Some(vec!["pricing-placeholder".to_string()])
} else {
None
},
// No impact on price
prompt: String::new(),
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 = pricing_request.estimate_credits();
let cost_in_usd_cents = pricing_request.estimate_cost_in_usd_cents();
let costs = pricing_request.calculate_costs();
let cost_in_credits = costs.kinovi_credits;
let cost_in_usd_cents = costs.usd_cents_rounded_up;
VideoGenerationCostEstimate {
cost_in_credits: Some(cost_in_credits as u64),
cost_in_credits: Some(cost_in_credits),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
@@ -99,20 +107,20 @@ mod tests {
#[test]
fn cost_720p_batch_1() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One), 58);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 4, KinoviBatchCount::One), 59);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::One), 73);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One), 145);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 10, KinoviBatchCount::One), 146);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 15, KinoviBatchCount::One), 218);
}
#[test]
fn cost_720p_batch_2() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two), 145);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Two), 146);
}
#[test]
fn cost_720p_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Four), 290);
assert_eq!(usd_cents(KinoviOutputResolution::SevenTwentyP, 5, KinoviBatchCount::Four), 291);
}
}
@@ -123,8 +131,8 @@ mod tests {
#[test]
fn cost_480p_batch_1() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One), 29);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 36);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 4, KinoviBatchCount::One), 30);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::One), 37);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 10, KinoviBatchCount::One), 73);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 15, KinoviBatchCount::One), 109);
}
@@ -136,7 +144,7 @@ mod tests {
#[test]
fn cost_480p_batch_4() {
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 145);
assert_eq!(usd_cents(KinoviOutputResolution::FourEightyP, 5, KinoviBatchCount::Four), 146);
}
}
@@ -174,7 +182,7 @@ mod tests {
// -- Video reference does NOT affect cost (yet) --
#[test]
fn video_reference_does_not_affect_cost() {
fn video_reference_adds_surcharge() {
let base = KinoviSeedance2p0FastCostState {
resolution: Some(KinoviOutputResolution::SevenTwentyP),
duration_seconds: 5,
@@ -183,8 +191,10 @@ mod tests {
};
let without = base.estimate_cost();
let with = KinoviSeedance2p0FastCostState { has_video_reference: true, ..base }.estimate_cost();
assert_eq!(without.cost_in_usd_cents, with.cost_in_usd_cents);
assert_eq!(without.cost_in_credits, with.cost_in_credits);
// Fast 720p surcharge is +6 credits/s: 140 -> 170 credits (17000/193 = 88.08 -> 89 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(89));
}
// ── from_request() tests ──
@@ -215,7 +225,7 @@ mod tests {
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(36));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(37));
}
#[test]
@@ -223,8 +233,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 don't affect cost yet
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(73));
// 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(89));
}
#[test]
@@ -256,7 +267,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(36));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(37));
}
#[test]
@@ -264,8 +275,9 @@ mod tests {
let draft = make_draft(5, 1, None, true);
let cost = KinoviSeedance2p0FastCostState::from_draft(&draft);
assert!(cost.has_video_reference);
// Video refs don't affect cost yet
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(73));
// 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(89));
}
#[test]
@@ -281,7 +293,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(435));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(436));
}
}
@@ -0,0 +1,61 @@
/// Kinovi credits per US dollar.
/// Credit package: 22,000 credits for $114 (~192.98 credits/$1, rounded to 193).
const CREDITS_PER_DOLLAR: u64 = 193;
/// The cost of a Kinovi generation, in both native credits and USD cents.
pub struct KinoviGenerationCost {
/// The cost of the generation in Kinovi credits.
pub kinovi_credits: u64,
/// Estimated cost in cents, rounded up to the nearest whole cent.
/// This does not account for discounts, prorations, etc.
/// This is always rounded up when fractional.
pub usd_cents_rounded_up: u64,
}
impl KinoviGenerationCost {
/// Build a cost from a Kinovi credit amount, deriving the USD cents via
/// the credit-package rate (always rounding up fractional cents).
pub fn from_kinovi_credits(kinovi_credits: u64) -> Self {
// Integer ceiling of (credits * 100 / CREDITS_PER_DOLLAR).
let usd_cents_rounded_up = (kinovi_credits * 100).div_ceil(CREDITS_PER_DOLLAR);
Self {
kinovi_credits,
usd_cents_rounded_up,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_credits_is_zero_cents() {
let cost = KinoviGenerationCost::from_kinovi_credits(0);
assert_eq!(cost.kinovi_credits, 0);
assert_eq!(cost.usd_cents_rounded_up, 0);
}
#[test]
fn exact_dollar_boundaries_do_not_round() {
// 193 credits = exactly $1.00.
let cost = KinoviGenerationCost::from_kinovi_credits(193);
assert_eq!(cost.kinovi_credits, 193);
assert_eq!(cost.usd_cents_rounded_up, 100);
let cost = KinoviGenerationCost::from_kinovi_credits(386);
assert_eq!(cost.usd_cents_rounded_up, 200);
}
#[test]
fn fractional_cents_round_up() {
// 194 credits = 100.518... cents -> 101.
let cost = KinoviGenerationCost::from_kinovi_credits(194);
assert_eq!(cost.usd_cents_rounded_up, 101);
// 1 credit = 0.518... cents -> 1.
let cost = KinoviGenerationCost::from_kinovi_credits(1);
assert_eq!(cost.usd_cents_rounded_up, 1);
}
}
@@ -1,3 +1,4 @@
use crate::generate::cost::KinoviGenerationCost;
use crate::creds::seedance2pro_session::Seedance2ProSession;
use crate::error::seedance2pro_error::Seedance2ProError;
use crate::requests::generate_image::generate_image::{
@@ -36,12 +37,21 @@ pub struct GenerateMidjourneyV7Request {
}
impl GenerateMidjourneyV7Request {
pub fn estimate_credits(&self) -> u32 {
self.to_inner_request().estimate_credits()
/// Calculate the cost of this request, in Kinovi credits and USD cents
/// (rounded up).
pub fn calculate_costs(&self) -> KinoviGenerationCost {
self.to_inner_request().calculate_costs()
}
#[deprecated(note = "Use calculate_costs() instead")]
pub fn estimate_credits(&self) -> u32 {
self.calculate_costs().kinovi_credits as u32
}
/// NB: Rounds UP fractional cents (the historical behavior rounded to nearest).
#[deprecated(note = "Use calculate_costs() instead")]
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
self.to_inner_request().estimate_cost_in_usd_cents()
self.calculate_costs().usd_cents_rounded_up
}
pub(crate) fn to_inner_request(&self) -> KinoviGenerateImageRequest {
@@ -264,27 +274,27 @@ mod tests {
#[test]
fn batch_one_is_twelve_credits() {
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).estimate_credits(), 12);
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().kinovi_credits, 12);
}
#[test]
fn batch_two_is_twentyfour_credits() {
assert_eq!(make_request(KinoviMidjourneyBatchCount::Two).estimate_credits(), 24);
assert_eq!(make_request(KinoviMidjourneyBatchCount::Two).calculate_costs().kinovi_credits, 24);
}
#[test]
fn batch_four_is_fortyeight_credits() {
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).estimate_credits(), 48);
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().kinovi_credits, 48);
}
#[test]
fn usd_cents_batch_one_is_six() {
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).estimate_cost_in_usd_cents(), 6);
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 7); // 1200/193 = 6.22 -> rounds UP
}
#[test]
fn usd_cents_batch_four_is_twentyfive() {
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).estimate_cost_in_usd_cents(), 25);
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 25); // 4800/193 = 24.87 -> rounds UP
}
/// Pricing must match the inner module byte-for-byte (sanity that no
@@ -298,8 +308,8 @@ mod tests {
] {
let outer = make_request(batch);
let inner = outer.to_inner_request();
assert_eq!(outer.estimate_credits(), inner.estimate_credits(), "batch={:?}", batch);
assert_eq!(outer.estimate_cost_in_usd_cents(), inner.estimate_cost_in_usd_cents(), "batch={:?}", batch);
assert_eq!(outer.calculate_costs().kinovi_credits, inner.calculate_costs().kinovi_credits, "batch={:?}", batch);
assert_eq!(outer.calculate_costs().usd_cents_rounded_up, inner.calculate_costs().usd_cents_rounded_up, "batch={:?}", batch);
}
}
}
@@ -1,3 +1,4 @@
use crate::generate::cost::KinoviGenerationCost;
use crate::creds::seedance2pro_session::Seedance2ProSession;
use crate::error::seedance2pro_error::Seedance2ProError;
use crate::requests::generate_image::generate_image::{
@@ -36,12 +37,21 @@ pub struct GenerateMidjourneyV7NijiRequest {
}
impl GenerateMidjourneyV7NijiRequest {
pub fn estimate_credits(&self) -> u32 {
self.to_inner_request().estimate_credits()
/// Calculate the cost of this request, in Kinovi credits and USD cents
/// (rounded up).
pub fn calculate_costs(&self) -> KinoviGenerationCost {
self.to_inner_request().calculate_costs()
}
#[deprecated(note = "Use calculate_costs() instead")]
pub fn estimate_credits(&self) -> u32 {
self.calculate_costs().kinovi_credits as u32
}
/// NB: Rounds UP fractional cents (the historical behavior rounded to nearest).
#[deprecated(note = "Use calculate_costs() instead")]
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
self.to_inner_request().estimate_cost_in_usd_cents()
self.calculate_costs().usd_cents_rounded_up
}
pub(crate) fn to_inner_request(&self) -> KinoviGenerateImageRequest {
@@ -262,27 +272,27 @@ mod tests {
#[test]
fn batch_one_is_twelve_credits() {
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).estimate_credits(), 12);
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().kinovi_credits, 12);
}
#[test]
fn batch_two_is_twentyfour_credits() {
assert_eq!(make_request(KinoviMidjourneyBatchCount::Two).estimate_credits(), 24);
assert_eq!(make_request(KinoviMidjourneyBatchCount::Two).calculate_costs().kinovi_credits, 24);
}
#[test]
fn batch_four_is_fortyeight_credits() {
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).estimate_credits(), 48);
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().kinovi_credits, 48);
}
#[test]
fn usd_cents_batch_one_is_six() {
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).estimate_cost_in_usd_cents(), 6);
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 7); // 1200/193 = 6.22 -> rounds UP
}
#[test]
fn usd_cents_batch_four_is_twentyfive() {
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).estimate_cost_in_usd_cents(), 25);
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 25); // 4800/193 = 24.87 -> rounds UP
}
/// Pricing must match the inner module exactly.
@@ -295,8 +305,8 @@ mod tests {
] {
let outer = make_request(batch);
let inner = outer.to_inner_request();
assert_eq!(outer.estimate_credits(), inner.estimate_credits(), "batch={:?}", batch);
assert_eq!(outer.estimate_cost_in_usd_cents(), inner.estimate_cost_in_usd_cents(), "batch={:?}", batch);
assert_eq!(outer.calculate_costs().kinovi_credits, inner.calculate_costs().kinovi_credits, "batch={:?}", batch);
assert_eq!(outer.calculate_costs().usd_cents_rounded_up, inner.calculate_costs().usd_cents_rounded_up, "batch={:?}", batch);
}
}
}
@@ -1,3 +1,4 @@
use crate::generate::cost::KinoviGenerationCost;
use crate::creds::seedance2pro_session::Seedance2ProSession;
use crate::error::seedance2pro_error::Seedance2ProError;
use crate::requests::generate_image::generate_image::{
@@ -36,12 +37,21 @@ pub struct GenerateMidjourneyV8Request {
}
impl GenerateMidjourneyV8Request {
pub fn estimate_credits(&self) -> u32 {
self.to_inner_request().estimate_credits()
/// Calculate the cost of this request, in Kinovi credits and USD cents
/// (rounded up).
pub fn calculate_costs(&self) -> KinoviGenerationCost {
self.to_inner_request().calculate_costs()
}
#[deprecated(note = "Use calculate_costs() instead")]
pub fn estimate_credits(&self) -> u32 {
self.calculate_costs().kinovi_credits as u32
}
/// NB: Rounds UP fractional cents (the historical behavior rounded to nearest).
#[deprecated(note = "Use calculate_costs() instead")]
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
self.to_inner_request().estimate_cost_in_usd_cents()
self.calculate_costs().usd_cents_rounded_up
}
pub(crate) fn to_inner_request(&self) -> KinoviGenerateImageRequest {
@@ -259,27 +269,27 @@ mod tests {
#[test]
fn batch_one_is_twelve_credits() {
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).estimate_credits(), 12);
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().kinovi_credits, 12);
}
#[test]
fn batch_two_is_twentyfour_credits() {
assert_eq!(make_request(KinoviMidjourneyBatchCount::Two).estimate_credits(), 24);
assert_eq!(make_request(KinoviMidjourneyBatchCount::Two).calculate_costs().kinovi_credits, 24);
}
#[test]
fn batch_four_is_fortyeight_credits() {
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).estimate_credits(), 48);
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().kinovi_credits, 48);
}
#[test]
fn usd_cents_batch_one_is_six() {
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).estimate_cost_in_usd_cents(), 6);
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 7); // 1200/193 = 6.22 -> rounds UP
}
#[test]
fn usd_cents_batch_four_is_twentyfive() {
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).estimate_cost_in_usd_cents(), 25);
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 25); // 4800/193 = 24.87 -> rounds UP
}
/// Pricing must match the inner module exactly.
@@ -292,8 +302,8 @@ mod tests {
] {
let outer = make_request(batch);
let inner = outer.to_inner_request();
assert_eq!(outer.estimate_credits(), inner.estimate_credits(), "batch={:?}", batch);
assert_eq!(outer.estimate_cost_in_usd_cents(), inner.estimate_cost_in_usd_cents(), "batch={:?}", batch);
assert_eq!(outer.calculate_costs().kinovi_credits, inner.calculate_costs().kinovi_credits, "batch={:?}", batch);
assert_eq!(outer.calculate_costs().usd_cents_rounded_up, inner.calculate_costs().usd_cents_rounded_up, "batch={:?}", batch);
}
}
}
@@ -1,2 +1,3 @@
pub mod cost;
pub mod image;
pub mod video;
@@ -1,5 +1,6 @@
use crate::creds::seedance2pro_session::Seedance2ProSession;
use crate::error::seedance2pro_error::Seedance2ProError;
use crate::generate::cost::KinoviGenerationCost;
use crate::requests::kinovi_host::KinoviHost;
use crate::requests::workflow_run_task::workflow_run_task::{
workflow_run_task, KinoviAspectRatioRaw, KinoviBatchCountRaw,
@@ -66,32 +67,35 @@ pub enum KinoviHappyHorse1p0BatchCount {
impl GenerateHappyHorse1p0Request {
/// Estimate the credit cost for this generation request.
pub fn estimate_credits(&self) -> u32 {
let credits_per_second: u32 = match self.output_resolution {
/// Calculate the cost of this generation request, in Kinovi credits and
/// USD cents (rounded up).
pub fn calculate_costs(&self) -> KinoviGenerationCost {
let credits_per_second: u64 = match self.output_resolution {
Some(KinoviHappyHorse1p0OutputResolution::TenEightyP) => 66,
Some(KinoviHappyHorse1p0OutputResolution::SevenTwentyP) | None => 33,
};
let per_video = u32::from(self.duration_seconds) * credits_per_second;
let batch_multiplier: u32 = match self.batch_count {
let per_video = u64::from(self.duration_seconds) * credits_per_second;
let batch_multiplier: u64 = match self.batch_count {
None | Some(KinoviHappyHorse1p0BatchCount::One) => 1,
Some(KinoviHappyHorse1p0BatchCount::Two) => 2,
Some(KinoviHappyHorse1p0BatchCount::Four) => 4,
};
per_video * batch_multiplier
KinoviGenerationCost::from_kinovi_credits(per_video * batch_multiplier)
}
/// Credits per dollar for billing conversion.
/// 22,000 credits / $114 ≈ 192.98, rounded to 193.
fn credits_per_dollar() -> f64 {
193.0
/// Estimate the credit cost for this generation request.
#[deprecated(note = "Use calculate_costs() instead")]
pub fn estimate_credits(&self) -> u32 {
self.calculate_costs().kinovi_credits as u32
}
/// Estimate the USD cost in cents for this generation request.
/// NB: Rounds UP fractional cents (the historical behavior rounded to nearest).
#[deprecated(note = "Use calculate_costs() instead")]
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
let credits = self.estimate_credits() as f64;
let cost = credits / Self::credits_per_dollar() * 100.0;
cost.round() as u64
self.calculate_costs().usd_cents_rounded_up
}
}
@@ -215,25 +219,25 @@ mod tests {
#[test]
fn every_duration() {
assert_eq!(r720(3).estimate_credits(), 99);
assert_eq!(r720(4).estimate_credits(), 132);
assert_eq!(r720(5).estimate_credits(), 165);
assert_eq!(r720(6).estimate_credits(), 198);
assert_eq!(r720(7).estimate_credits(), 231);
assert_eq!(r720(8).estimate_credits(), 264);
assert_eq!(r720(9).estimate_credits(), 297);
assert_eq!(r720(10).estimate_credits(), 330);
assert_eq!(r720(11).estimate_credits(), 363);
assert_eq!(r720(12).estimate_credits(), 396);
assert_eq!(r720(13).estimate_credits(), 429);
assert_eq!(r720(14).estimate_credits(), 462);
assert_eq!(r720(15).estimate_credits(), 495);
assert_eq!(r720(3).calculate_costs().kinovi_credits, 99);
assert_eq!(r720(4).calculate_costs().kinovi_credits, 132);
assert_eq!(r720(5).calculate_costs().kinovi_credits, 165);
assert_eq!(r720(6).calculate_costs().kinovi_credits, 198);
assert_eq!(r720(7).calculate_costs().kinovi_credits, 231);
assert_eq!(r720(8).calculate_costs().kinovi_credits, 264);
assert_eq!(r720(9).calculate_costs().kinovi_credits, 297);
assert_eq!(r720(10).calculate_costs().kinovi_credits, 330);
assert_eq!(r720(11).calculate_costs().kinovi_credits, 363);
assert_eq!(r720(12).calculate_costs().kinovi_credits, 396);
assert_eq!(r720(13).calculate_costs().kinovi_credits, 429);
assert_eq!(r720(14).calculate_costs().kinovi_credits, 462);
assert_eq!(r720(15).calculate_costs().kinovi_credits, 495);
}
#[test]
fn explicit_720p_same_as_default() {
let default = r720(5).estimate_credits();
let explicit = make_request(5, Some(KinoviHappyHorse1p0OutputResolution::SevenTwentyP), None).estimate_credits();
let default = r720(5).calculate_costs().kinovi_credits;
let explicit = make_request(5, Some(KinoviHappyHorse1p0OutputResolution::SevenTwentyP), None).calculate_costs().kinovi_credits;
assert_eq!(default, explicit);
}
}
@@ -245,19 +249,19 @@ mod tests {
#[test]
fn every_duration() {
assert_eq!(r1080(3).estimate_credits(), 198);
assert_eq!(r1080(4).estimate_credits(), 264);
assert_eq!(r1080(5).estimate_credits(), 330);
assert_eq!(r1080(6).estimate_credits(), 396);
assert_eq!(r1080(7).estimate_credits(), 462);
assert_eq!(r1080(8).estimate_credits(), 528);
assert_eq!(r1080(9).estimate_credits(), 594);
assert_eq!(r1080(10).estimate_credits(), 660);
assert_eq!(r1080(11).estimate_credits(), 726);
assert_eq!(r1080(12).estimate_credits(), 792);
assert_eq!(r1080(13).estimate_credits(), 858);
assert_eq!(r1080(14).estimate_credits(), 924);
assert_eq!(r1080(15).estimate_credits(), 990);
assert_eq!(r1080(3).calculate_costs().kinovi_credits, 198);
assert_eq!(r1080(4).calculate_costs().kinovi_credits, 264);
assert_eq!(r1080(5).calculate_costs().kinovi_credits, 330);
assert_eq!(r1080(6).calculate_costs().kinovi_credits, 396);
assert_eq!(r1080(7).calculate_costs().kinovi_credits, 462);
assert_eq!(r1080(8).calculate_costs().kinovi_credits, 528);
assert_eq!(r1080(9).calculate_costs().kinovi_credits, 594);
assert_eq!(r1080(10).calculate_costs().kinovi_credits, 660);
assert_eq!(r1080(11).calculate_costs().kinovi_credits, 726);
assert_eq!(r1080(12).calculate_costs().kinovi_credits, 792);
assert_eq!(r1080(13).calculate_costs().kinovi_credits, 858);
assert_eq!(r1080(14).calculate_costs().kinovi_credits, 924);
assert_eq!(r1080(15).calculate_costs().kinovi_credits, 990);
}
}
@@ -268,30 +272,30 @@ mod tests {
#[test]
fn batch_1_is_base() {
let base = r720(5).estimate_credits();
let explicit = make_request(5, None, Some(KinoviHappyHorse1p0BatchCount::One)).estimate_credits();
let base = r720(5).calculate_costs().kinovi_credits;
let explicit = make_request(5, None, Some(KinoviHappyHorse1p0BatchCount::One)).calculate_costs().kinovi_credits;
assert_eq!(base, explicit);
}
#[test]
fn batch_2_doubles() {
let base = r720(5).estimate_credits();
let batch2 = make_request(5, None, Some(KinoviHappyHorse1p0BatchCount::Two)).estimate_credits();
let base = r720(5).calculate_costs().kinovi_credits;
let batch2 = make_request(5, None, Some(KinoviHappyHorse1p0BatchCount::Two)).calculate_costs().kinovi_credits;
assert_eq!(batch2, base * 2);
}
#[test]
fn batch_4_quadruples() {
let base = r720(5).estimate_credits();
let batch4 = make_request(5, None, Some(KinoviHappyHorse1p0BatchCount::Four)).estimate_credits();
let base = r720(5).calculate_costs().kinovi_credits;
let batch4 = make_request(5, None, Some(KinoviHappyHorse1p0BatchCount::Four)).calculate_costs().kinovi_credits;
assert_eq!(batch4, base * 4);
}
#[test]
fn batch_multiplier_applies_to_1080p() {
let base = r1080(5).estimate_credits();
let batch2 = make_request(5, Some(KinoviHappyHorse1p0OutputResolution::TenEightyP), Some(KinoviHappyHorse1p0BatchCount::Two)).estimate_credits();
let batch4 = make_request(5, Some(KinoviHappyHorse1p0OutputResolution::TenEightyP), Some(KinoviHappyHorse1p0BatchCount::Four)).estimate_credits();
let base = r1080(5).calculate_costs().kinovi_credits;
let batch2 = make_request(5, Some(KinoviHappyHorse1p0OutputResolution::TenEightyP), Some(KinoviHappyHorse1p0BatchCount::Two)).calculate_costs().kinovi_credits;
let batch4 = make_request(5, Some(KinoviHappyHorse1p0OutputResolution::TenEightyP), Some(KinoviHappyHorse1p0BatchCount::Four)).calculate_costs().kinovi_credits;
assert_eq!(batch2, base * 2);
assert_eq!(batch4, base * 4);
}
@@ -305,17 +309,17 @@ mod tests {
#[test]
fn r1080p_is_exactly_double_720p() {
for dur in 3..=15u8 {
let c720 = make_request(dur, None, None).estimate_credits();
let c1080 = make_request(dur, Some(KinoviHappyHorse1p0OutputResolution::TenEightyP), None).estimate_credits();
let c720 = make_request(dur, None, None).calculate_costs().kinovi_credits;
let c1080 = make_request(dur, Some(KinoviHappyHorse1p0OutputResolution::TenEightyP), None).calculate_costs().kinovi_credits;
assert_eq!(c1080, c720 * 2, "1080p should be 2× 720p at {}s", dur);
}
}
#[test]
fn cost_scales_with_duration() {
let c3 = r720(3).estimate_credits();
let c10 = r720(10).estimate_credits();
let c15 = r720(15).estimate_credits();
let c3 = r720(3).calculate_costs().kinovi_credits;
let c10 = r720(10).calculate_costs().kinovi_credits;
let c15 = r720(15).calculate_costs().kinovi_credits;
assert!(c3 < c10);
assert!(c10 < c15);
}
@@ -323,45 +327,59 @@ mod tests {
// ── USD cents ──
mod usd_cents_tests {
mod calculate_costs_tests {
use super::*;
/// Both fields together: credits and ceil-rounded USD cents.
/// (USD cents are always rounded UP when fractional.)
#[test]
fn credits_per_dollar_is_193() {
assert_eq!(GenerateHappyHorse1p0Request::credits_per_dollar(), 193.0);
fn costs_720p_5s() {
// 33 credits/s × 5s = 165 credits; 16500/193 = 85.49 → rounds UP to 86¢
let costs = r720(5).calculate_costs();
assert_eq!(costs.kinovi_credits, 165);
assert_eq!(costs.usd_cents_rounded_up, 86);
}
#[test]
fn usd_cents_720p_5s() {
// 165 credits / 193 * 100 = 85.49 → 85¢
assert_eq!(r720(5).estimate_cost_in_usd_cents(), 85);
fn costs_1080p_5s() {
// 66 credits/s × 5s = 330 credits; 33000/193 = 170.98 → 171¢
let costs = r1080(5).calculate_costs();
assert_eq!(costs.kinovi_credits, 330);
assert_eq!(costs.usd_cents_rounded_up, 171);
}
#[test]
fn usd_cents_1080p_5s() {
// 330 credits / 193 * 100 = 170.98 → 171¢
assert_eq!(r1080(5).estimate_cost_in_usd_cents(), 171);
fn costs_720p_15s() {
// 33 credits/s × 15s = 495 credits; 49500/193 = 256.48 → rounds UP to 257¢
let costs = r720(15).calculate_costs();
assert_eq!(costs.kinovi_credits, 495);
assert_eq!(costs.usd_cents_rounded_up, 257);
}
#[test]
fn usd_cents_720p_15s() {
// 495 credits / 193 * 100 = 256.48 → 256¢
assert_eq!(r720(15).estimate_cost_in_usd_cents(), 256);
fn costs_1080p_15s() {
// 66 credits/s × 15s = 990 credits; 99000/193 = 512.95 → 513¢
let costs = r1080(15).calculate_costs();
assert_eq!(costs.kinovi_credits, 990);
assert_eq!(costs.usd_cents_rounded_up, 513);
}
#[test]
fn usd_cents_1080p_15s() {
// 990 credits / 193 * 100 = 512.95 → 513¢
assert_eq!(r1080(15).estimate_cost_in_usd_cents(), 513);
fn costs_batch_2_720p_5s() {
// 165 credits × 2 = 330 credits; 33000/193 = 170.98 → 171¢
let costs = make_request(5, None, Some(KinoviHappyHorse1p0BatchCount::Two)).calculate_costs();
assert_eq!(costs.kinovi_credits, 330);
assert_eq!(costs.usd_cents_rounded_up, 171);
}
/// The deprecated shims return the corresponding struct fields.
#[test]
fn batch_multiplies_usd_cents() {
let base = r720(5).estimate_cost_in_usd_cents();
let batch2 = make_request(5, None, Some(KinoviHappyHorse1p0BatchCount::Two)).estimate_cost_in_usd_cents();
// Batch 2 should be approximately 2× base (rounding may differ slightly)
assert!(batch2 >= base * 2 - 1 && batch2 <= base * 2 + 1,
"batch 2 ({}) should be ~2× base ({})", batch2, base);
#[allow(deprecated)]
fn deprecated_methods_delegate() {
let request = r720(5);
let costs = request.calculate_costs();
assert_eq!(u64::from(request.estimate_credits()), costs.kinovi_credits);
assert_eq!(request.estimate_cost_in_usd_cents(), costs.usd_cents_rounded_up);
}
}
@@ -369,7 +387,7 @@ mod tests {
#[test]
fn aspect_ratio_does_not_affect_credits() {
let baseline = r720(5).estimate_credits();
let baseline = r720(5).calculate_costs().kinovi_credits;
let ratios = [
KinoviHappyHorse1p0AspectRatio::Landscape16x9,
@@ -389,7 +407,7 @@ mod tests {
start_frame_url: None,
};
assert_eq!(
req.estimate_credits(), baseline,
req.calculate_costs().kinovi_credits, baseline,
"Aspect ratio {:?} should not change credits from baseline {}", ar, baseline,
);
}
@@ -1,5 +1,6 @@
use crate::creds::seedance2pro_session::Seedance2ProSession;
use crate::error::seedance2pro_error::Seedance2ProError;
use crate::generate::cost::KinoviGenerationCost;
use crate::requests::kinovi_host::KinoviHost;
use crate::requests::workflow_run_task::workflow_run_task::{
workflow_run_task, KinoviAspectRatioRaw, KinoviBatchCountRaw,
@@ -74,34 +75,67 @@ pub enum KinoviSeedance2p0BatchCount {
// Credit package: 22,000 credits for $114 (~192.98 credits/$1, rounded to 193).
impl GenerateSeedance2p0Request {
/// Estimate the credit cost for this generation request.
pub fn estimate_credits(&self) -> u32 {
let credits_per_second: u32 = match self.output_resolution {
/// Calculate the cost of this generation request, in Kinovi credits and
/// USD cents (rounded up).
///
/// Attaching reference VIDEOS adds a per-output-second surcharge (see the
/// pricing table below). Reference images and audio are free.
pub fn calculate_costs(&self) -> KinoviGenerationCost {
let credits_per_second: u64 = match self.output_resolution {
Some(KinoviSeedance2p0OutputResolution::FourEightyP) => 15,
Some(KinoviSeedance2p0OutputResolution::SevenTwentyP) | None => 40,
Some(KinoviSeedance2p0OutputResolution::TenEightyP) => 90,
};
let per_video = u32::from(self.duration_seconds) * credits_per_second;
let batch_multiplier: u32 = match self.batch_count {
// Video-reference surcharge, billed per second of OUTPUT duration
// (not the reference video's duration):
//
// | Resolution | Surcharge credits/sec |
// |------------|-----------------------|
// | 480p | 4 |
// | 720p | 8 |
// | 1080p | 18 |
//
// NB: Assumed flat per generation regardless of how many reference
// videos are attached (Kinovi's pricing page only shows one).
let video_reference_surcharge_per_second: u64 = if self.has_video_reference() {
match self.output_resolution {
Some(KinoviSeedance2p0OutputResolution::FourEightyP) => 4,
Some(KinoviSeedance2p0OutputResolution::SevenTwentyP) | None => 8,
Some(KinoviSeedance2p0OutputResolution::TenEightyP) => 18,
}
} else {
0
};
let per_video = u64::from(self.duration_seconds)
* (credits_per_second + video_reference_surcharge_per_second);
let batch_multiplier: u64 = match self.batch_count {
None | Some(KinoviSeedance2p0BatchCount::One) => 1,
Some(KinoviSeedance2p0BatchCount::Two) => 2,
Some(KinoviSeedance2p0BatchCount::Four) => 4,
};
per_video * batch_multiplier
KinoviGenerationCost::from_kinovi_credits(per_video * batch_multiplier)
}
/// Credits per dollar for billing conversion.
/// 22,000 credits / $114 ≈ 192.98, rounded to 193.
fn credits_per_dollar() -> f64 {
193.0
fn has_video_reference(&self) -> bool {
self.reference_video_urls
.as_ref()
.is_some_and(|urls| !urls.is_empty())
}
/// Estimate the credit cost for this generation request.
#[deprecated(note = "Use calculate_costs() instead")]
pub fn estimate_credits(&self) -> u32 {
self.calculate_costs().kinovi_credits as u32
}
/// Estimate the USD cost in cents for this generation request.
/// NB: Rounds UP fractional cents (the historical behavior rounded to nearest).
#[deprecated(note = "Use calculate_costs() instead")]
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
let credits = self.estimate_credits() as f64;
let cost = credits / Self::credits_per_dollar() * 100.0;
cost.round() as u64
self.calculate_costs().usd_cents_rounded_up
}
}
@@ -235,19 +269,19 @@ mod tests {
#[test]
fn every_duration() {
assert_eq!(r480(3).estimate_credits(), 45);
assert_eq!(r480(4).estimate_credits(), 60);
assert_eq!(r480(5).estimate_credits(), 75);
assert_eq!(r480(6).estimate_credits(), 90);
assert_eq!(r480(7).estimate_credits(), 105);
assert_eq!(r480(8).estimate_credits(), 120);
assert_eq!(r480(9).estimate_credits(), 135);
assert_eq!(r480(10).estimate_credits(), 150);
assert_eq!(r480(11).estimate_credits(), 165);
assert_eq!(r480(12).estimate_credits(), 180);
assert_eq!(r480(13).estimate_credits(), 195);
assert_eq!(r480(14).estimate_credits(), 210);
assert_eq!(r480(15).estimate_credits(), 225);
assert_eq!(r480(3).calculate_costs().kinovi_credits, 45);
assert_eq!(r480(4).calculate_costs().kinovi_credits, 60);
assert_eq!(r480(5).calculate_costs().kinovi_credits, 75);
assert_eq!(r480(6).calculate_costs().kinovi_credits, 90);
assert_eq!(r480(7).calculate_costs().kinovi_credits, 105);
assert_eq!(r480(8).calculate_costs().kinovi_credits, 120);
assert_eq!(r480(9).calculate_costs().kinovi_credits, 135);
assert_eq!(r480(10).calculate_costs().kinovi_credits, 150);
assert_eq!(r480(11).calculate_costs().kinovi_credits, 165);
assert_eq!(r480(12).calculate_costs().kinovi_credits, 180);
assert_eq!(r480(13).calculate_costs().kinovi_credits, 195);
assert_eq!(r480(14).calculate_costs().kinovi_credits, 210);
assert_eq!(r480(15).calculate_costs().kinovi_credits, 225);
}
}
@@ -258,25 +292,25 @@ mod tests {
#[test]
fn every_duration() {
assert_eq!(r720(3).estimate_credits(), 120);
assert_eq!(r720(4).estimate_credits(), 160);
assert_eq!(r720(5).estimate_credits(), 200);
assert_eq!(r720(6).estimate_credits(), 240);
assert_eq!(r720(7).estimate_credits(), 280);
assert_eq!(r720(8).estimate_credits(), 320);
assert_eq!(r720(9).estimate_credits(), 360);
assert_eq!(r720(10).estimate_credits(), 400);
assert_eq!(r720(11).estimate_credits(), 440);
assert_eq!(r720(12).estimate_credits(), 480);
assert_eq!(r720(13).estimate_credits(), 520);
assert_eq!(r720(14).estimate_credits(), 560);
assert_eq!(r720(15).estimate_credits(), 600);
assert_eq!(r720(3).calculate_costs().kinovi_credits, 120);
assert_eq!(r720(4).calculate_costs().kinovi_credits, 160);
assert_eq!(r720(5).calculate_costs().kinovi_credits, 200);
assert_eq!(r720(6).calculate_costs().kinovi_credits, 240);
assert_eq!(r720(7).calculate_costs().kinovi_credits, 280);
assert_eq!(r720(8).calculate_costs().kinovi_credits, 320);
assert_eq!(r720(9).calculate_costs().kinovi_credits, 360);
assert_eq!(r720(10).calculate_costs().kinovi_credits, 400);
assert_eq!(r720(11).calculate_costs().kinovi_credits, 440);
assert_eq!(r720(12).calculate_costs().kinovi_credits, 480);
assert_eq!(r720(13).calculate_costs().kinovi_credits, 520);
assert_eq!(r720(14).calculate_costs().kinovi_credits, 560);
assert_eq!(r720(15).calculate_costs().kinovi_credits, 600);
}
#[test]
fn explicit_720p_same_as_default() {
let default = r720(5).estimate_credits();
let explicit = build_request(5, Some(KinoviSeedance2p0OutputResolution::SevenTwentyP), None).estimate_credits();
let default = r720(5).calculate_costs().kinovi_credits;
let explicit = build_request(5, Some(KinoviSeedance2p0OutputResolution::SevenTwentyP), None).calculate_costs().kinovi_credits;
assert_eq!(default, explicit);
}
}
@@ -288,19 +322,19 @@ mod tests {
#[test]
fn every_duration() {
assert_eq!(r1080(3).estimate_credits(), 270);
assert_eq!(r1080(4).estimate_credits(), 360);
assert_eq!(r1080(5).estimate_credits(), 450);
assert_eq!(r1080(6).estimate_credits(), 540);
assert_eq!(r1080(7).estimate_credits(), 630);
assert_eq!(r1080(8).estimate_credits(), 720);
assert_eq!(r1080(9).estimate_credits(), 810);
assert_eq!(r1080(10).estimate_credits(), 900);
assert_eq!(r1080(11).estimate_credits(), 990);
assert_eq!(r1080(12).estimate_credits(), 1080);
assert_eq!(r1080(13).estimate_credits(), 1170);
assert_eq!(r1080(14).estimate_credits(), 1260);
assert_eq!(r1080(15).estimate_credits(), 1350);
assert_eq!(r1080(3).calculate_costs().kinovi_credits, 270);
assert_eq!(r1080(4).calculate_costs().kinovi_credits, 360);
assert_eq!(r1080(5).calculate_costs().kinovi_credits, 450);
assert_eq!(r1080(6).calculate_costs().kinovi_credits, 540);
assert_eq!(r1080(7).calculate_costs().kinovi_credits, 630);
assert_eq!(r1080(8).calculate_costs().kinovi_credits, 720);
assert_eq!(r1080(9).calculate_costs().kinovi_credits, 810);
assert_eq!(r1080(10).calculate_costs().kinovi_credits, 900);
assert_eq!(r1080(11).calculate_costs().kinovi_credits, 990);
assert_eq!(r1080(12).calculate_costs().kinovi_credits, 1080);
assert_eq!(r1080(13).calculate_costs().kinovi_credits, 1170);
assert_eq!(r1080(14).calculate_costs().kinovi_credits, 1260);
assert_eq!(r1080(15).calculate_costs().kinovi_credits, 1350);
}
}
@@ -311,39 +345,39 @@ mod tests {
#[test]
fn batch_1_is_base() {
let base = r720(5).estimate_credits();
let explicit = build_request(5, None, Some(KinoviSeedance2p0BatchCount::One)).estimate_credits();
let base = r720(5).calculate_costs().kinovi_credits;
let explicit = build_request(5, None, Some(KinoviSeedance2p0BatchCount::One)).calculate_costs().kinovi_credits;
assert_eq!(base, explicit);
}
#[test]
fn batch_2_doubles() {
let base = r720(5).estimate_credits();
let batch2 = build_request(5, None, Some(KinoviSeedance2p0BatchCount::Two)).estimate_credits();
let base = r720(5).calculate_costs().kinovi_credits;
let batch2 = build_request(5, None, Some(KinoviSeedance2p0BatchCount::Two)).calculate_costs().kinovi_credits;
assert_eq!(batch2, base * 2);
}
#[test]
fn batch_4_quadruples() {
let base = r720(5).estimate_credits();
let batch4 = build_request(5, None, Some(KinoviSeedance2p0BatchCount::Four)).estimate_credits();
let base = r720(5).calculate_costs().kinovi_credits;
let batch4 = build_request(5, None, Some(KinoviSeedance2p0BatchCount::Four)).calculate_costs().kinovi_credits;
assert_eq!(batch4, base * 4);
}
#[test]
fn batch_multiplier_applies_to_1080p() {
let base = r1080(5).estimate_credits();
let batch2 = build_request(5, Some(KinoviSeedance2p0OutputResolution::TenEightyP), Some(KinoviSeedance2p0BatchCount::Two)).estimate_credits();
let batch4 = build_request(5, Some(KinoviSeedance2p0OutputResolution::TenEightyP), Some(KinoviSeedance2p0BatchCount::Four)).estimate_credits();
let base = r1080(5).calculate_costs().kinovi_credits;
let batch2 = build_request(5, Some(KinoviSeedance2p0OutputResolution::TenEightyP), Some(KinoviSeedance2p0BatchCount::Two)).calculate_costs().kinovi_credits;
let batch4 = build_request(5, Some(KinoviSeedance2p0OutputResolution::TenEightyP), Some(KinoviSeedance2p0BatchCount::Four)).calculate_costs().kinovi_credits;
assert_eq!(batch2, base * 2);
assert_eq!(batch4, base * 4);
}
#[test]
fn batch_multiplier_applies_to_480p() {
let base = r480(5).estimate_credits();
let batch2 = build_request(5, Some(KinoviSeedance2p0OutputResolution::FourEightyP), Some(KinoviSeedance2p0BatchCount::Two)).estimate_credits();
let batch4 = build_request(5, Some(KinoviSeedance2p0OutputResolution::FourEightyP), Some(KinoviSeedance2p0BatchCount::Four)).estimate_credits();
let base = r480(5).calculate_costs().kinovi_credits;
let batch2 = build_request(5, Some(KinoviSeedance2p0OutputResolution::FourEightyP), Some(KinoviSeedance2p0BatchCount::Two)).calculate_costs().kinovi_credits;
let batch4 = build_request(5, Some(KinoviSeedance2p0OutputResolution::FourEightyP), Some(KinoviSeedance2p0BatchCount::Four)).calculate_costs().kinovi_credits;
assert_eq!(batch2, base * 2);
assert_eq!(batch4, base * 4);
}
@@ -356,9 +390,9 @@ mod tests {
#[test]
fn cost_scales_with_duration() {
let c3 = r720(3).estimate_credits();
let c10 = r720(10).estimate_credits();
let c15 = r720(15).estimate_credits();
let c3 = r720(3).calculate_costs().kinovi_credits;
let c10 = r720(10).calculate_costs().kinovi_credits;
let c15 = r720(15).calculate_costs().kinovi_credits;
assert!(c3 < c10);
assert!(c10 < c15);
}
@@ -366,9 +400,9 @@ mod tests {
#[test]
fn resolution_ordering() {
for dur in 3..=15u8 {
let c480 = build_request(dur, Some(KinoviSeedance2p0OutputResolution::FourEightyP), None).estimate_credits();
let c720 = build_request(dur, None, None).estimate_credits();
let c1080 = build_request(dur, Some(KinoviSeedance2p0OutputResolution::TenEightyP), None).estimate_credits();
let c480 = build_request(dur, Some(KinoviSeedance2p0OutputResolution::FourEightyP), None).calculate_costs().kinovi_credits;
let c720 = build_request(dur, None, None).calculate_costs().kinovi_credits;
let c1080 = build_request(dur, Some(KinoviSeedance2p0OutputResolution::TenEightyP), None).calculate_costs().kinovi_credits;
assert!(c480 < c720, "480p should be cheaper than 720p at {}s", dur);
assert!(c720 < c1080, "720p should be cheaper than 1080p at {}s", dur);
}
@@ -377,50 +411,145 @@ mod tests {
// ── USD cents ──
mod usd_cents_tests {
mod calculate_costs_tests {
use super::*;
/// Both fields together: credits and ceil-rounded USD cents.
/// (USD cents are always rounded UP when fractional.)
#[test]
fn credits_per_dollar_is_193() {
assert_eq!(GenerateSeedance2p0Request::credits_per_dollar(), 193.0);
fn costs_480p_5s() {
// 15 credits/s × 5s = 75 credits; 7500/193 = 38.86 → 39¢
let costs = r480(5).calculate_costs();
assert_eq!(costs.kinovi_credits, 75);
assert_eq!(costs.usd_cents_rounded_up, 39);
}
#[test]
fn usd_cents_720p_5s() {
// 200 credits / 193 * 100 = 103.63 → 104¢
assert_eq!(r720(5).estimate_cost_in_usd_cents(), 104);
fn costs_720p_5s() {
// 40 credits/s × 5s = 200 credits; 20000/193 = 103.63 → 104¢
let costs = r720(5).calculate_costs();
assert_eq!(costs.kinovi_credits, 200);
assert_eq!(costs.usd_cents_rounded_up, 104);
}
#[test]
fn usd_cents_480p_5s() {
// 75 credits / 193 * 100 = 38.86 → 39¢
assert_eq!(r480(5).estimate_cost_in_usd_cents(), 39);
fn costs_1080p_5s() {
// 90 credits/s × 5s = 450 credits; 45000/193 = 233.16 → rounds UP to 234¢
let costs = r1080(5).calculate_costs();
assert_eq!(costs.kinovi_credits, 450);
assert_eq!(costs.usd_cents_rounded_up, 234);
}
#[test]
fn usd_cents_1080p_5s() {
// 450 credits / 193 * 100 = 233.16 → 233¢
assert_eq!(r1080(5).estimate_cost_in_usd_cents(), 233);
fn costs_720p_15s() {
// 40 credits/s × 15s = 600 credits; 60000/193 = 310.88 → 311¢
let costs = r720(15).calculate_costs();
assert_eq!(costs.kinovi_credits, 600);
assert_eq!(costs.usd_cents_rounded_up, 311);
}
#[test]
fn usd_cents_720p_15s() {
// 600 credits / 193 * 100 = 310.88 → 311¢
assert_eq!(r720(15).estimate_cost_in_usd_cents(), 311);
fn costs_1080p_15s() {
// 90 credits/s × 15s = 1350 credits; 135000/193 = 699.48 → rounds UP to 700¢
let costs = r1080(15).calculate_costs();
assert_eq!(costs.kinovi_credits, 1350);
assert_eq!(costs.usd_cents_rounded_up, 700);
}
#[test]
fn usd_cents_1080p_15s() {
// 1350 credits / 193 * 100 = 699.48 → 699¢
assert_eq!(r1080(15).estimate_cost_in_usd_cents(), 699);
fn costs_batch_4_720p_5s() {
// 200 credits × 4 = 800 credits; 80000/193 = 414.51 → 415¢
let costs = build_request(5, None, Some(KinoviSeedance2p0BatchCount::Four)).calculate_costs();
assert_eq!(costs.kinovi_credits, 800);
assert_eq!(costs.usd_cents_rounded_up, 415);
}
/// The deprecated shims return the corresponding struct fields.
#[test]
#[allow(deprecated)]
fn deprecated_methods_delegate() {
let request = r720(5);
let costs = request.calculate_costs();
assert_eq!(u64::from(request.estimate_credits()), costs.kinovi_credits);
assert_eq!(request.estimate_cost_in_usd_cents(), costs.usd_cents_rounded_up);
}
}
// ── Video-reference surcharge ──
//
// Per Kinovi's pricing page, attaching a reference video adds a
// per-output-second surcharge (480p: +4/s, 720p: +8/s, 1080p: +18/s).
mod video_reference_surcharge_tests {
use super::*;
fn with_video_ref(mut request: GenerateSeedance2p0Request) -> GenerateSeedance2p0Request {
request.reference_video_urls = Some(vec!["https://example.com/ref.mp4".to_string()]);
request
}
/// The full base + surcharge table from Kinovi's pricing page
/// ("With Video Uploads", 10 sec video ref).
#[test]
fn kinovi_pricing_table_with_video_reference() {
let cases: &[(fn(u8) -> GenerateSeedance2p0Request, u8, u64)] = &[
// 480p: base + 4/s
(r480, 4, 60 + 16),
(r480, 5, 75 + 20),
(r480, 10, 150 + 40),
(r480, 15, 225 + 60),
// 720p: base + 8/s
(r720, 4, 160 + 32),
(r720, 5, 200 + 40),
(r720, 10, 400 + 80),
(r720, 15, 600 + 120),
// 1080p: base + 18/s
(r1080, 4, 360 + 72),
(r1080, 5, 450 + 90),
(r1080, 10, 900 + 180),
(r1080, 15, 1350 + 270),
];
for (make, duration, expected_credits) in cases {
let costs = with_video_ref(make(*duration)).calculate_costs();
assert_eq!(costs.kinovi_credits, *expected_credits,
"duration {duration}s should cost {expected_credits} credits with a video reference");
}
}
#[test]
fn batch_multiplies_usd_cents() {
let base = r720(5).estimate_cost_in_usd_cents();
let batch2 = build_request(5, None, Some(KinoviSeedance2p0BatchCount::Two)).estimate_cost_in_usd_cents();
assert!(batch2 >= base * 2 - 1 && batch2 <= base * 2 + 1,
"batch 2 ({}) should be ~2× base ({})", batch2, base);
fn surcharge_includes_usd_cents() {
// 720p 5s + video ref = 240 credits; 24000/193 = 124.35 → 125¢
let costs = with_video_ref(r720(5)).calculate_costs();
assert_eq!(costs.kinovi_credits, 240);
assert_eq!(costs.usd_cents_rounded_up, 125);
}
#[test]
fn empty_video_reference_list_has_no_surcharge() {
let mut request = r720(5);
request.reference_video_urls = Some(vec![]);
assert_eq!(request.calculate_costs().kinovi_credits, 200);
}
/// Surcharge is flat per generation regardless of how many reference
/// videos are attached (assumption — Kinovi's page only shows one).
#[test]
fn multiple_video_references_charge_once() {
let mut request = r720(5);
request.reference_video_urls = Some(vec![
"https://example.com/a.mp4".to_string(),
"https://example.com/b.mp4".to_string(),
]);
assert_eq!(request.calculate_costs().kinovi_credits, 240);
}
/// The surcharge applies per generated video, so batches multiply it.
#[test]
fn batch_multiplies_surcharge() {
let request = with_video_ref(build_request(5, None, Some(KinoviSeedance2p0BatchCount::Two)));
// (200 base + 40 surcharge) × 2 = 480 credits
assert_eq!(request.calculate_costs().kinovi_credits, 480);
}
}
@@ -428,7 +557,7 @@ mod tests {
#[test]
fn aspect_ratio_does_not_affect_credits() {
let baseline = r720(5).estimate_credits();
let baseline = r720(5).calculate_costs().kinovi_credits;
let ratios = [
KinoviSeedance2p0AspectRatio::Landscape16x9,
@@ -455,7 +584,7 @@ mod tests {
use_face_blur_hack: None,
};
assert_eq!(
req.estimate_credits(), baseline,
req.calculate_costs().kinovi_credits, baseline,
"Aspect ratio {:?} should not change credits from baseline {}", ar, baseline,
);
}
@@ -1,5 +1,6 @@
use crate::creds::seedance2pro_session::Seedance2ProSession;
use crate::error::seedance2pro_error::Seedance2ProError;
use crate::generate::cost::KinoviGenerationCost;
use crate::requests::kinovi_host::KinoviHost;
use crate::requests::workflow_run_task::workflow_run_task::{
workflow_run_task, KinoviAspectRatioRaw, KinoviBatchCountRaw,
@@ -72,33 +73,64 @@ pub enum KinoviSeedance2p0FastBatchCount {
// Credit package: 22,000 credits for $114 (~192.98 credits/$1, rounded to 193).
impl GenerateSeedance2p0FastRequest {
/// Estimate the credit cost for this generation request.
pub fn estimate_credits(&self) -> u32 {
let credits_per_second: u32 = match self.output_resolution {
/// Calculate the cost of this generation request, in Kinovi credits and
/// USD cents (rounded up).
///
/// Attaching reference VIDEOS adds a per-output-second surcharge (see the
/// pricing table below). Reference images and audio are free.
pub fn calculate_costs(&self) -> KinoviGenerationCost {
let credits_per_second: u64 = match self.output_resolution {
Some(KinoviSeedance2p0FastOutputResolution::FourEightyP) => 14,
Some(KinoviSeedance2p0FastOutputResolution::SevenTwentyP) | None => 28,
};
let per_video = u32::from(self.duration_seconds) * credits_per_second;
let batch_multiplier: u32 = match self.batch_count {
// Video-reference surcharge, billed per second of OUTPUT duration
// (not the reference video's duration):
//
// | Resolution | Surcharge credits/sec |
// |------------|-----------------------|
// | 480p | 4 |
// | 720p | 6 |
//
// NB: Assumed flat per generation regardless of how many reference
// videos are attached (Kinovi's pricing page only shows one).
let video_reference_surcharge_per_second: u64 = if self.has_video_reference() {
match self.output_resolution {
Some(KinoviSeedance2p0FastOutputResolution::FourEightyP) => 4,
Some(KinoviSeedance2p0FastOutputResolution::SevenTwentyP) | None => 6,
}
} else {
0
};
let per_video = u64::from(self.duration_seconds)
* (credits_per_second + video_reference_surcharge_per_second);
let batch_multiplier: u64 = match self.batch_count {
None | Some(KinoviSeedance2p0FastBatchCount::One) => 1,
Some(KinoviSeedance2p0FastBatchCount::Two) => 2,
Some(KinoviSeedance2p0FastBatchCount::Four) => 4,
};
per_video * batch_multiplier
KinoviGenerationCost::from_kinovi_credits(per_video * batch_multiplier)
}
/// Credits per dollar for billing conversion.
/// 22,000 credits / $114 ≈ 192.98, rounded to 193.
fn credits_per_dollar() -> f64 {
193.0
fn has_video_reference(&self) -> bool {
self.reference_video_urls
.as_ref()
.is_some_and(|urls| !urls.is_empty())
}
/// Estimate the credit cost for this generation request.
#[deprecated(note = "Use calculate_costs() instead")]
pub fn estimate_credits(&self) -> u32 {
self.calculate_costs().kinovi_credits as u32
}
/// Estimate the USD cost in cents for this generation request.
/// NB: Rounds UP fractional cents (the historical behavior rounded to nearest).
#[deprecated(note = "Use calculate_costs() instead")]
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
let credits = self.estimate_credits() as f64;
let cost = credits / Self::credits_per_dollar() * 100.0;
cost.round() as u64
self.calculate_costs().usd_cents_rounded_up
}
}
@@ -227,19 +259,19 @@ mod tests {
#[test]
fn every_duration() {
assert_eq!(r480(3).estimate_credits(), 42);
assert_eq!(r480(4).estimate_credits(), 56);
assert_eq!(r480(5).estimate_credits(), 70);
assert_eq!(r480(6).estimate_credits(), 84);
assert_eq!(r480(7).estimate_credits(), 98);
assert_eq!(r480(8).estimate_credits(), 112);
assert_eq!(r480(9).estimate_credits(), 126);
assert_eq!(r480(10).estimate_credits(), 140);
assert_eq!(r480(11).estimate_credits(), 154);
assert_eq!(r480(12).estimate_credits(), 168);
assert_eq!(r480(13).estimate_credits(), 182);
assert_eq!(r480(14).estimate_credits(), 196);
assert_eq!(r480(15).estimate_credits(), 210);
assert_eq!(r480(3).calculate_costs().kinovi_credits, 42);
assert_eq!(r480(4).calculate_costs().kinovi_credits, 56);
assert_eq!(r480(5).calculate_costs().kinovi_credits, 70);
assert_eq!(r480(6).calculate_costs().kinovi_credits, 84);
assert_eq!(r480(7).calculate_costs().kinovi_credits, 98);
assert_eq!(r480(8).calculate_costs().kinovi_credits, 112);
assert_eq!(r480(9).calculate_costs().kinovi_credits, 126);
assert_eq!(r480(10).calculate_costs().kinovi_credits, 140);
assert_eq!(r480(11).calculate_costs().kinovi_credits, 154);
assert_eq!(r480(12).calculate_costs().kinovi_credits, 168);
assert_eq!(r480(13).calculate_costs().kinovi_credits, 182);
assert_eq!(r480(14).calculate_costs().kinovi_credits, 196);
assert_eq!(r480(15).calculate_costs().kinovi_credits, 210);
}
}
@@ -250,25 +282,25 @@ mod tests {
#[test]
fn every_duration() {
assert_eq!(r720(3).estimate_credits(), 84);
assert_eq!(r720(4).estimate_credits(), 112);
assert_eq!(r720(5).estimate_credits(), 140);
assert_eq!(r720(6).estimate_credits(), 168);
assert_eq!(r720(7).estimate_credits(), 196);
assert_eq!(r720(8).estimate_credits(), 224);
assert_eq!(r720(9).estimate_credits(), 252);
assert_eq!(r720(10).estimate_credits(), 280);
assert_eq!(r720(11).estimate_credits(), 308);
assert_eq!(r720(12).estimate_credits(), 336);
assert_eq!(r720(13).estimate_credits(), 364);
assert_eq!(r720(14).estimate_credits(), 392);
assert_eq!(r720(15).estimate_credits(), 420);
assert_eq!(r720(3).calculate_costs().kinovi_credits, 84);
assert_eq!(r720(4).calculate_costs().kinovi_credits, 112);
assert_eq!(r720(5).calculate_costs().kinovi_credits, 140);
assert_eq!(r720(6).calculate_costs().kinovi_credits, 168);
assert_eq!(r720(7).calculate_costs().kinovi_credits, 196);
assert_eq!(r720(8).calculate_costs().kinovi_credits, 224);
assert_eq!(r720(9).calculate_costs().kinovi_credits, 252);
assert_eq!(r720(10).calculate_costs().kinovi_credits, 280);
assert_eq!(r720(11).calculate_costs().kinovi_credits, 308);
assert_eq!(r720(12).calculate_costs().kinovi_credits, 336);
assert_eq!(r720(13).calculate_costs().kinovi_credits, 364);
assert_eq!(r720(14).calculate_costs().kinovi_credits, 392);
assert_eq!(r720(15).calculate_costs().kinovi_credits, 420);
}
#[test]
fn explicit_720p_same_as_default() {
let default = r720(5).estimate_credits();
let explicit = build_request(5, Some(KinoviSeedance2p0FastOutputResolution::SevenTwentyP), None).estimate_credits();
let default = r720(5).calculate_costs().kinovi_credits;
let explicit = build_request(5, Some(KinoviSeedance2p0FastOutputResolution::SevenTwentyP), None).calculate_costs().kinovi_credits;
assert_eq!(default, explicit);
}
}
@@ -280,30 +312,30 @@ mod tests {
#[test]
fn batch_1_is_base() {
let base = r720(5).estimate_credits();
let explicit = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::One)).estimate_credits();
let base = r720(5).calculate_costs().kinovi_credits;
let explicit = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::One)).calculate_costs().kinovi_credits;
assert_eq!(base, explicit);
}
#[test]
fn batch_2_doubles() {
let base = r720(5).estimate_credits();
let batch2 = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Two)).estimate_credits();
let base = r720(5).calculate_costs().kinovi_credits;
let batch2 = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Two)).calculate_costs().kinovi_credits;
assert_eq!(batch2, base * 2);
}
#[test]
fn batch_4_quadruples() {
let base = r720(5).estimate_credits();
let batch4 = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Four)).estimate_credits();
let base = r720(5).calculate_costs().kinovi_credits;
let batch4 = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Four)).calculate_costs().kinovi_credits;
assert_eq!(batch4, base * 4);
}
#[test]
fn batch_multiplier_applies_to_480p() {
let base = r480(5).estimate_credits();
let batch2 = build_request(5, Some(KinoviSeedance2p0FastOutputResolution::FourEightyP), Some(KinoviSeedance2p0FastBatchCount::Two)).estimate_credits();
let batch4 = build_request(5, Some(KinoviSeedance2p0FastOutputResolution::FourEightyP), Some(KinoviSeedance2p0FastBatchCount::Four)).estimate_credits();
let base = r480(5).calculate_costs().kinovi_credits;
let batch2 = build_request(5, Some(KinoviSeedance2p0FastOutputResolution::FourEightyP), Some(KinoviSeedance2p0FastBatchCount::Two)).calculate_costs().kinovi_credits;
let batch4 = build_request(5, Some(KinoviSeedance2p0FastOutputResolution::FourEightyP), Some(KinoviSeedance2p0FastBatchCount::Four)).calculate_costs().kinovi_credits;
assert_eq!(batch2, base * 2);
assert_eq!(batch4, base * 4);
}
@@ -316,9 +348,9 @@ mod tests {
#[test]
fn cost_scales_with_duration() {
let c3 = r720(3).estimate_credits();
let c10 = r720(10).estimate_credits();
let c15 = r720(15).estimate_credits();
let c3 = r720(3).calculate_costs().kinovi_credits;
let c10 = r720(10).calculate_costs().kinovi_credits;
let c15 = r720(15).calculate_costs().kinovi_credits;
assert!(c3 < c10);
assert!(c10 < c15);
}
@@ -326,8 +358,8 @@ mod tests {
#[test]
fn resolution_ordering() {
for dur in 3..=15u8 {
let c480 = build_request(dur, Some(KinoviSeedance2p0FastOutputResolution::FourEightyP), None).estimate_credits();
let c720 = build_request(dur, None, None).estimate_credits();
let c480 = build_request(dur, Some(KinoviSeedance2p0FastOutputResolution::FourEightyP), None).calculate_costs().kinovi_credits;
let c720 = build_request(dur, None, None).calculate_costs().kinovi_credits;
assert!(c480 < c720, "480p should be cheaper than 720p at {}s", dur);
}
}
@@ -335,44 +367,133 @@ mod tests {
// ── USD cents ──
mod usd_cents_tests {
mod calculate_costs_tests {
use super::*;
/// Both fields together: credits and ceil-rounded USD cents.
/// (USD cents are always rounded UP when fractional.)
#[test]
fn credits_per_dollar_is_193() {
assert_eq!(GenerateSeedance2p0FastRequest::credits_per_dollar(), 193.0);
fn costs_480p_5s() {
// 14 credits/s × 5s = 70 credits; 7000/193 = 36.27 → rounds UP to 37¢
let costs = r480(5).calculate_costs();
assert_eq!(costs.kinovi_credits, 70);
assert_eq!(costs.usd_cents_rounded_up, 37);
}
#[test]
fn usd_cents_720p_5s() {
// 140 credits / 193 * 100 = 72.54 → 73¢
assert_eq!(r720(5).estimate_cost_in_usd_cents(), 73);
fn costs_720p_5s() {
// 28 credits/s × 5s = 140 credits; 14000/193 = 72.54 → 73¢
let costs = r720(5).calculate_costs();
assert_eq!(costs.kinovi_credits, 140);
assert_eq!(costs.usd_cents_rounded_up, 73);
}
#[test]
fn usd_cents_480p_5s() {
// 70 credits / 193 * 100 = 36.27 → 36¢
assert_eq!(r480(5).estimate_cost_in_usd_cents(), 36);
fn costs_720p_15s() {
// 28 credits/s × 15s = 420 credits; 42000/193 = 217.62 → 218¢
let costs = r720(15).calculate_costs();
assert_eq!(costs.kinovi_credits, 420);
assert_eq!(costs.usd_cents_rounded_up, 218);
}
#[test]
fn usd_cents_720p_15s() {
// 420 credits / 193 * 100 = 217.62 → 218¢
assert_eq!(r720(15).estimate_cost_in_usd_cents(), 218);
fn costs_480p_15s() {
// 14 credits/s × 15s = 210 credits; 21000/193 = 108.81 → 109¢
let costs = r480(15).calculate_costs();
assert_eq!(costs.kinovi_credits, 210);
assert_eq!(costs.usd_cents_rounded_up, 109);
}
#[test]
fn usd_cents_480p_15s() {
// 210 credits / 193 * 100 = 108.81 → 109¢
assert_eq!(r480(15).estimate_cost_in_usd_cents(), 109);
fn costs_batch_2_720p_5s() {
// 140 credits × 2 = 280 credits; 28000/193 = 145.07 → 146¢
let costs = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Two)).calculate_costs();
assert_eq!(costs.kinovi_credits, 280);
assert_eq!(costs.usd_cents_rounded_up, 146);
}
/// The deprecated shims return the corresponding struct fields.
#[test]
#[allow(deprecated)]
fn deprecated_methods_delegate() {
let request = r720(5);
let costs = request.calculate_costs();
assert_eq!(u64::from(request.estimate_credits()), costs.kinovi_credits);
assert_eq!(request.estimate_cost_in_usd_cents(), costs.usd_cents_rounded_up);
}
}
// ── Video-reference surcharge ──
//
// Per Kinovi's pricing page ("Seedance 2.0 Fast With Video Ref"),
// attaching a reference video adds a per-output-second surcharge
// (480p: +4/s, 720p: +6/s).
mod video_reference_surcharge_tests {
use super::*;
fn with_video_ref(mut request: GenerateSeedance2p0FastRequest) -> GenerateSeedance2p0FastRequest {
request.reference_video_urls = Some(vec!["https://example.com/ref.mp4".to_string()]);
request
}
/// The full base + surcharge table from Kinovi's pricing page
/// ("Seedance 2.0 Fast With Video Ref", 10 sec video ref).
#[test]
fn kinovi_pricing_table_with_video_reference() {
let cases: &[(fn(u8) -> GenerateSeedance2p0FastRequest, u8, u64)] = &[
// 480p: base + 4/s
(r480, 4, 56 + 16),
(r480, 5, 70 + 20),
(r480, 10, 140 + 40),
(r480, 15, 210 + 60),
// 720p: base + 6/s
(r720, 4, 112 + 24),
(r720, 5, 140 + 30),
(r720, 10, 280 + 60),
(r720, 15, 420 + 90),
];
for (make, duration, expected_credits) in cases {
let costs = with_video_ref(make(*duration)).calculate_costs();
assert_eq!(costs.kinovi_credits, *expected_credits,
"duration {duration}s should cost {expected_credits} credits with a video reference");
}
}
#[test]
fn batch_multiplies_usd_cents() {
let base = r720(5).estimate_cost_in_usd_cents();
let batch2 = build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Two)).estimate_cost_in_usd_cents();
assert!(batch2 >= base * 2 - 1 && batch2 <= base * 2 + 1,
"batch 2 ({}) should be ~2× base ({})", batch2, base);
fn surcharge_includes_usd_cents() {
// 720p 5s + video ref = 170 credits; 17000/193 = 88.08 → 89¢
let costs = with_video_ref(r720(5)).calculate_costs();
assert_eq!(costs.kinovi_credits, 170);
assert_eq!(costs.usd_cents_rounded_up, 89);
}
#[test]
fn empty_video_reference_list_has_no_surcharge() {
let mut request = r720(5);
request.reference_video_urls = Some(vec![]);
assert_eq!(request.calculate_costs().kinovi_credits, 140);
}
/// Surcharge is flat per generation regardless of how many reference
/// videos are attached (assumption — Kinovi's page only shows one).
#[test]
fn multiple_video_references_charge_once() {
let mut request = r720(5);
request.reference_video_urls = Some(vec![
"https://example.com/a.mp4".to_string(),
"https://example.com/b.mp4".to_string(),
]);
assert_eq!(request.calculate_costs().kinovi_credits, 170);
}
/// The surcharge applies per generated video, so batches multiply it.
#[test]
fn batch_multiplies_surcharge() {
let request = with_video_ref(build_request(5, None, Some(KinoviSeedance2p0FastBatchCount::Two)));
// (140 base + 30 surcharge) × 2 = 340 credits
assert_eq!(request.calculate_costs().kinovi_credits, 340);
}
}
@@ -380,7 +501,7 @@ mod tests {
#[test]
fn aspect_ratio_does_not_affect_credits() {
let baseline = r720(5).estimate_credits();
let baseline = r720(5).calculate_costs().kinovi_credits;
let ratios = [
KinoviSeedance2p0FastAspectRatio::Landscape16x9,
@@ -407,7 +528,7 @@ mod tests {
use_face_blur_hack: None,
};
assert_eq!(
req.estimate_credits(), baseline,
req.calculate_costs().kinovi_credits, baseline,
"Aspect ratio {:?} should not change credits from baseline {}", ar, baseline,
);
}
@@ -1,3 +1,4 @@
use crate::generate::cost::KinoviGenerationCost;
use crate::creds::seedance2pro_session::Seedance2ProSession;
use crate::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError;
use crate::error::seedance2pro_client_error::Seedance2ProClientError;
@@ -22,7 +23,6 @@ const CREDITS_PER_MIDJOURNEY_TASK: u32 = 12;
/// Midjourney image generation flow.
///
/// 22,000 credits / $114 = 192.98 ≈ 193 credits per dollar.
const CREDITS_PER_DOLLAR: f64 = 193.0;
/// Kinovi only currently exposes a single Midjourney resolution preset.
const MIDJOURNEY_RESOLUTION: &str = "1k";
@@ -121,19 +121,27 @@ impl std::fmt::Debug for GenerateImageArgs<'_> {
}
impl KinoviGenerateImageRequest {
/// Estimates the Kinovi credit cost for this request.
/// Calculate the cost of this request, in Kinovi credits and USD cents
/// (rounded up).
///
/// Pricing for Midjourney is flat: 12 credits per task, regardless of
/// model or aspect ratio. Batches multiply the cost by `batch_count`.
pub fn estimate_credits(&self) -> u32 {
CREDITS_PER_MIDJOURNEY_TASK * u32::from(self.batch_count.as_u8())
pub fn calculate_costs(&self) -> KinoviGenerationCost {
let credits = u64::from(CREDITS_PER_MIDJOURNEY_TASK) * u64::from(self.batch_count.as_u8());
KinoviGenerationCost::from_kinovi_credits(credits)
}
/// Estimates the dollar cost (in USD cents, rounded to the nearest cent).
/// Estimates the Kinovi credit cost for this request.
#[deprecated(note = "Use calculate_costs() instead")]
pub fn estimate_credits(&self) -> u32 {
self.calculate_costs().kinovi_credits as u32
}
/// Estimates the dollar cost in USD cents.
/// NB: Rounds UP fractional cents (the historical behavior rounded to nearest).
#[deprecated(note = "Use calculate_costs() instead")]
pub fn estimate_cost_in_usd_cents(&self) -> u64 {
let credits = self.estimate_credits() as f64;
let cost = credits / CREDITS_PER_DOLLAR * 100.0;
cost.round() as u64
self.calculate_costs().usd_cents_rounded_up
}
}
@@ -348,50 +356,50 @@ mod tests {
#[test]
fn batch_one_is_twelve_credits() {
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).estimate_credits(), 12);
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().kinovi_credits, 12);
}
#[test]
fn batch_two_is_twentyfour_credits() {
assert_eq!(make_request(KinoviMidjourneyBatchCount::Two).estimate_credits(), 24);
assert_eq!(make_request(KinoviMidjourneyBatchCount::Two).calculate_costs().kinovi_credits, 24);
}
#[test]
fn batch_four_is_fortyeight_credits() {
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).estimate_credits(), 48);
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().kinovi_credits, 48);
}
/// Pricing is identical across all three Midjourney models.
#[test]
fn pricing_is_model_agnostic() {
let baseline = make_request(KinoviMidjourneyBatchCount::One).estimate_credits();
let baseline = make_request(KinoviMidjourneyBatchCount::One).calculate_costs().kinovi_credits;
for model in [
KinoviMidjourneyModel::V7,
KinoviMidjourneyModel::V7Niji,
KinoviMidjourneyModel::V8,
] {
let req = KinoviGenerateImageRequest { model, ..make_request(KinoviMidjourneyBatchCount::One) };
assert_eq!(req.estimate_credits(), baseline, "model={:?}", model);
assert_eq!(req.calculate_costs().kinovi_credits, baseline, "model={:?}", model);
}
}
/// Pricing does not vary by aspect ratio.
#[test]
fn pricing_is_aspect_ratio_agnostic() {
let baseline = make_request(KinoviMidjourneyBatchCount::One).estimate_credits();
let baseline = make_request(KinoviMidjourneyBatchCount::One).calculate_costs().kinovi_credits;
for ar in ["1:1", "16:9", "9:16", "21:9", "9:21", "4:3", "3:4", "5:4", "4:5", "3:2", "2:3"] {
let req = KinoviGenerateImageRequest {
aspect_ratio: ar.to_string(),
..make_request(KinoviMidjourneyBatchCount::One)
};
assert_eq!(req.estimate_credits(), baseline, "aspect_ratio={:?}", ar);
assert_eq!(req.calculate_costs().kinovi_credits, baseline, "aspect_ratio={:?}", ar);
}
}
/// Pricing does not vary with the Midjourney style knobs.
#[test]
fn pricing_is_independent_of_style_knobs() {
let baseline = make_request(KinoviMidjourneyBatchCount::One).estimate_credits();
let baseline = make_request(KinoviMidjourneyBatchCount::One).calculate_costs().kinovi_credits;
let req = KinoviGenerateImageRequest {
stylize: Some(1000),
weird: Some(3000),
@@ -401,7 +409,7 @@ mod tests {
negative_prompt: Some("ugly, blurry".to_string()),
..make_request(KinoviMidjourneyBatchCount::One)
};
assert_eq!(req.estimate_credits(), baseline);
assert_eq!(req.calculate_costs().kinovi_credits, baseline);
}
// ── USD cents conversion ──
@@ -409,19 +417,19 @@ mod tests {
#[test]
fn usd_cents_batch_one() {
// 12 / 193 × 100 = 6.21¢ → 6¢
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).estimate_cost_in_usd_cents(), 6);
assert_eq!(make_request(KinoviMidjourneyBatchCount::One).calculate_costs().usd_cents_rounded_up, 7); // 1200/193 = 6.22 -> rounds UP
}
#[test]
fn usd_cents_batch_two() {
// 24 / 193 × 100 = 12.43¢ → 12¢
assert_eq!(make_request(KinoviMidjourneyBatchCount::Two).estimate_cost_in_usd_cents(), 12);
assert_eq!(make_request(KinoviMidjourneyBatchCount::Two).calculate_costs().usd_cents_rounded_up, 13); // 2400/193 = 12.44 -> rounds UP
}
#[test]
fn usd_cents_batch_four() {
// 48 / 193 × 100 = 24.87¢ → 25¢
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).estimate_cost_in_usd_cents(), 25);
assert_eq!(make_request(KinoviMidjourneyBatchCount::Four).calculate_costs().usd_cents_rounded_up, 25); // 4800/193 = 24.87 -> rounds UP
}
/// Batch 4 should be exactly 4× the credit cost of batch 1, and the USD
@@ -431,13 +439,16 @@ mod tests {
let one = make_request(KinoviMidjourneyBatchCount::One);
let four = make_request(KinoviMidjourneyBatchCount::Four);
assert_eq!(four.estimate_credits(), one.estimate_credits() * 4);
assert_eq!(four.calculate_costs().kinovi_credits, one.calculate_costs().kinovi_credits * 4);
let one_cents = one.estimate_cost_in_usd_cents();
let four_cents = four.estimate_cost_in_usd_cents();
let expected = one_cents * 4;
let delta = (four_cents as i64 - expected as i64).abs();
assert!(delta <= 1, "expected ~{}¢, got {}¢", expected, four_cents);
// With round-up pricing, a batch rounds up ONCE while four singles
// round up four times — so the batch price is at most 4× the single
// price, and within 4¢ of it.
let one_cents = one.calculate_costs().usd_cents_rounded_up;
let four_cents = four.calculate_costs().usd_cents_rounded_up;
let four_singles = one_cents * 4;
assert!(four_cents <= four_singles && four_cents + 4 > four_singles,
"expected within 4¢ under {}¢, got {}¢", four_singles, four_cents);
}
}
@@ -137,6 +137,10 @@ pub struct OrderStatus {
/// polling responses that didn't include the field — those came from the
/// video-only era and can be treated as video by callers that need to.
pub media_type: Option<OrderMediaType>,
/// The Kinovi credits charged for the order (the API's `totalCredits`).
/// `None` for older polling responses that didn't include the field.
pub total_credits: Option<u32>,
}
// --- Implementation ---
@@ -239,6 +243,7 @@ pub async fn poll_orders(args: PollOrdersArgs<'_>) -> Result<PollOrdersResponse,
created_at: o.created_at,
created_at_utc,
media_type,
total_credits: o.total_credits,
}
})
.collect();
@@ -274,6 +279,48 @@ mod tests {
Ok(Seedance2ProSession::from_cookies_string(cookies))
}
/// Diagnostic: fetch the raw getOrders JSON (no typed parsing) and print
/// the complete field set per order, so schema additions can be checked
/// against reality. Requires real cookies.
/// cargo test -p seedance2pro_client live_dump_raw_orders_json -- --ignored --nocapture
#[tokio::test]
#[ignore]
async fn live_dump_raw_orders_json() -> AnyhowResult<()> {
let session = test_session()?;
let host = resolve_host(None);
let base_url = host.api_base_url();
let get_orders_url = format!("{}/api/trpc/userOrder.getOrders", base_url);
let input_json = build_input_json(None);
let client = Client::builder()
.emulation(Emulation::Firefox143)
.build()?;
let response = client.get(&get_orders_url)
.query(&[("batch", "1"), ("input", input_json.as_str())])
.header("User-Agent", FIREFOX_USER_AGENT)
.header("Accept", "*/*")
.header("Referer", format!("{}/app/gallery", base_url))
.header("content-type", "application/json")
.header("x-trpc-source", "client")
.header("Cookie", session.cookies.as_str())
.send()
.await?;
let body = response.text().await?;
let parsed: serde_json::Value = serde_json::from_str(&body)?;
let orders = &parsed[0]["result"]["data"]["json"]["orders"];
if let Some(first) = orders.get(0) {
let keys: Vec<&str> = first.as_object().map(|o| o.keys().map(|k| k.as_str()).collect()).unwrap_or_default();
println!("FIELDS ON AN ORDER: {:?}", keys);
println!("FIRST ORDER RAW: {}", serde_json::to_string_pretty(first)?);
} else {
println!("no orders; raw body head: {}", &body[..body.len().min(500)]);
}
Ok(())
}
// ── Offline parsing tests against captured responses ──
//
// These verify the deserializer + mapping handle the Midjourney image
@@ -385,6 +432,7 @@ mod tests {
created_at: o.created_at,
created_at_utc,
media_type,
total_credits: o.total_credits,
}
}).collect()
}
@@ -36,6 +36,11 @@ pub(super) struct RawOrder {
/// orders. `None` for older response shapes that didn't include the field.
#[serde(default)]
pub media_type: Option<String>,
/// The Kinovi credits charged for the order. `None` for older response
/// shapes that didn't include the field.
#[serde(default)]
pub total_credits: Option<u32>,
}
#[derive(Deserialize, Debug)]
@@ -88,6 +88,7 @@ pub async fn insert_generic_inference_job_for_beeble_queue_with_apriori_job_toke
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
maybe_cost_estimates: None,
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
maybe_frontend_failure_category: args.maybe_frontend_failure_category,
maybe_failure_reason: args.maybe_failure_reason,
@@ -36,6 +36,7 @@ use tokens::tokens::users::UserToken;
use tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken;
use crate::errors::database_query_error::DatabaseQueryError;
use crate::queries::generic_inference::common::job_cost_estimates::JobCostEstimates;
use crate::payloads::generic_inference_args::generic_inference_args::GenericInferenceArgs;
use crate::queries::generic_inference::common::insert_full_generic_inference_job_record::{
insert_full_generic_inference_job_record,
@@ -75,6 +76,9 @@ pub(crate) struct InsertGenericInferenceJobForProviderArgs<'e, 'c, E>
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
/// Estimated system (user-facing) and provider-side costs.
pub maybe_cost_estimates: Option<JobCostEstimates>,
pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>,
pub maybe_frontend_failure_category: Option<FrontendFailureCategory>,
@@ -120,6 +124,8 @@ pub(crate) async fn insert_generic_inference_job_for_provider<'e, 'c: 'e, E>(
maybe_platform_type: args.maybe_platform_type,
maybe_cost_estimates: args.maybe_cost_estimates,
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
maybe_frontend_failure_category: args.maybe_frontend_failure_category,
@@ -71,6 +71,7 @@ pub async fn insert_generic_inference_job_for_fal_queue<'e, 'c : 'e, E>(args: In
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
maybe_cost_estimates: None,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -74,6 +74,7 @@ pub async fn insert_generic_inference_job_for_fal_queue_mock_failure<'e, 'c: 'e,
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
maybe_cost_estimates: None,
starting_job_status_override: Some(JobStatusPlus::CompleteFailure),
maybe_frontend_failure_category: args.maybe_frontend_failure_category,
maybe_failure_reason: args.maybe_failure_reason,
@@ -18,6 +18,7 @@ use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
use crate::errors::database_query_error::DatabaseQueryError;
use crate::queries::generic_inference::common::job_cost_estimates::JobCostEstimates;
use crate::payloads::generic_inference_args::generic_inference_args::GenericInferenceArgs;
use crate::queries::generic_inference::api_providers::common::insert_generic_inference_job_for_provider::{
insert_generic_inference_job_for_provider,
@@ -53,6 +54,9 @@ pub struct InsertGenericInferenceForFalWithAprioriJobTokenArgs<'e, 'c, E>
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
/// Estimated system (user-facing) and provider-side costs.
pub maybe_cost_estimates: Option<JobCostEstimates>,
/// Override the initial job status. Defaults to `Pending` when `None`.
/// Set to `Some(JobStatusPlus::CompleteFailure)` for mock/test failure jobs.
pub starting_job_status_override: Option<JobStatusPlus>,
@@ -110,6 +114,7 @@ pub async fn insert_generic_inference_job_for_fal_queue_with_apriori_job_token<'
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
maybe_cost_estimates: args.maybe_cost_estimates,
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
maybe_frontend_failure_category: args.maybe_frontend_failure_category,
maybe_failure_reason: args.maybe_failure_reason,
@@ -17,6 +17,7 @@ use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
use crate::errors::database_query_error::DatabaseQueryError;
use crate::queries::generic_inference::common::job_cost_estimates::JobCostEstimates;
use crate::queries::generic_inference::api_providers::common::insert_generic_inference_job_for_provider::{
insert_generic_inference_job_for_provider,
InsertGenericInferenceJobForProviderArgs,
@@ -35,6 +36,9 @@ where
pub maybe_avt_token: Option<&'e AnonymousVisitorTrackingToken>,
pub creator_ip_address: &'e str,
pub maybe_platform_type: Option<PlatformType>,
/// Estimated system (user-facing) and provider-side costs.
pub maybe_cost_estimates: Option<JobCostEstimates>,
pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
@@ -63,6 +67,7 @@ where
creator_ip_address: args.creator_ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: args.maybe_platform_type,
maybe_cost_estimates: args.maybe_cost_estimates,
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -17,6 +17,7 @@ use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
use crate::errors::database_query_error::DatabaseQueryError;
use crate::queries::generic_inference::common::job_cost_estimates::JobCostEstimates;
use crate::queries::generic_inference::api_providers::common::insert_generic_inference_job_for_provider::{
insert_generic_inference_job_for_provider,
InsertGenericInferenceJobForProviderArgs,
@@ -35,6 +36,9 @@ where
pub maybe_avt_token: Option<&'e AnonymousVisitorTrackingToken>,
pub creator_ip_address: &'e str,
pub maybe_platform_type: Option<PlatformType>,
/// Estimated system (user-facing) and provider-side costs.
pub maybe_cost_estimates: Option<JobCostEstimates>,
pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>,
pub mysql_executor: E,
pub phantom: PhantomData<&'c E>,
@@ -63,6 +67,7 @@ where
creator_ip_address: args.creator_ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: args.maybe_platform_type,
maybe_cost_estimates: args.maybe_cost_estimates,
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -64,6 +64,7 @@ pub async fn insert_generic_inference_job_for_seedance2pro_character_with_aprior
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: None,
maybe_cost_estimates: None,
maybe_debug_log_event_token: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -18,6 +18,7 @@ use tokens::tokens::users::UserToken;
use tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken;
use crate::errors::database_query_error::DatabaseQueryError;
use crate::queries::generic_inference::common::job_cost_estimates::JobCostEstimates;
use crate::payloads::generic_inference_args::generic_inference_args::GenericInferenceArgs;
use crate::queries::generic_inference::api_providers::common::insert_generic_inference_job_for_provider::{
insert_generic_inference_job_for_provider,
@@ -54,6 +55,9 @@ pub struct InsertGenericInferenceForSeedance2ProWithAprioriJobTokenArgs<'e, 'c,
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
/// Estimated system (user-facing) and provider-side costs.
pub maybe_cost_estimates: Option<JobCostEstimates>,
pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>,
pub mysql_executor: E,
@@ -131,6 +135,7 @@ pub async fn insert_generic_inference_job_for_seedance2pro_queue_with_apriori_jo
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
maybe_cost_estimates: args.maybe_cost_estimates,
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -76,6 +76,7 @@ pub async fn insert_generic_inference_job_for_worldlabs_queue_with_apriori_job_t
creator_ip_address: args.creator_ip_address,
creator_set_visibility: args.creator_set_visibility,
maybe_platform_type: args.maybe_platform_type,
maybe_cost_estimates: None,
maybe_debug_log_event_token: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -38,6 +38,7 @@ use tokens::tokens::users::UserToken;
use tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken;
use crate::errors::database_query_error::DatabaseQueryError;
use crate::queries::generic_inference::common::job_cost_estimates::JobCostEstimates;
use crate::payloads::generic_inference_args::generic_inference_args::GenericInferenceArgs;
pub(crate) struct InsertFullGenericInferenceJobRecordArgs<'e, 'c, E>
@@ -80,6 +81,10 @@ pub(crate) struct InsertFullGenericInferenceJobRecordArgs<'e, 'c, E>
/// The platform the enqueuing request came from, inferred from its User-Agent.
pub maybe_platform_type: Option<PlatformType>,
/// Estimated system (user-facing) and provider-side costs. `None` writes
/// DB NULL for all four columns.
pub maybe_cost_estimates: Option<JobCostEstimates>,
pub priority_level: u8,
pub requires_keepalive: bool,
pub max_duration_seconds: i32,
@@ -163,6 +168,11 @@ SET
platform_type = ?,
maybe_external_third_party_cost_credits = ?,
maybe_external_third_party_cost_usd_cents = ?,
maybe_system_cost_credits = ?,
maybe_system_cost_usd_cents = ?,
priority_level = ?,
is_keepalive_required = ?,
max_duration_seconds = ?,
@@ -211,6 +221,11 @@ SET
args.maybe_platform_type.map(|p| p.to_str()),
args.maybe_cost_estimates.and_then(|c| c.maybe_external_third_party_cost_credits),
args.maybe_cost_estimates.and_then(|c| c.maybe_external_third_party_cost_usd_cents),
args.maybe_cost_estimates.and_then(|c| c.maybe_system_cost_credits),
args.maybe_cost_estimates.and_then(|c| c.maybe_system_cost_usd_cents),
args.priority_level,
args.requires_keepalive,
args.max_duration_seconds,
@@ -0,0 +1,20 @@
/// Estimated costs for an inference job, captured at enqueue time.
///
/// "System" costs are what we charge the user (Artcraft pricing).
/// "External third party" costs are what the fulfilling provider (Kinovi,
/// Fal, GmiCloud, Grok, etc.) charges us, in the provider's own credit
/// denomination and/or USD cents.
#[derive(Clone, Copy, Debug, Default)]
pub struct JobCostEstimates {
/// Provider-side cost in the provider's own credits (if it bills in credits).
pub maybe_external_third_party_cost_credits: Option<u32>,
/// Provider-side cost in USD cents.
pub maybe_external_third_party_cost_usd_cents: Option<u32>,
/// User-facing cost in our own credits.
pub maybe_system_cost_credits: Option<u32>,
/// User-facing cost in USD cents.
pub maybe_system_cost_usd_cents: Option<u32>,
}
@@ -1 +1,2 @@
pub(crate) mod insert_full_generic_inference_job_record;
pub mod job_cost_estimates;
@@ -1,5 +1,5 @@
pub mod api_providers;
pub(crate) mod common;
pub mod common;
pub mod job;
pub mod moderation;
pub mod web;
@@ -123,6 +123,8 @@ pub async fn insert_generic_inference_job(args: InsertGenericInferenceArgs<'_>)
maybe_platform_type: None,
maybe_cost_estimates: None,
maybe_debug_log_event_token: None,
maybe_frontend_failure_category: None,
maybe_failure_reason: None,
@@ -241,6 +241,7 @@ pub async fn flux_2_lora_edit_image_angle_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -242,6 +242,7 @@ pub async fn qwen_edit_2511_edit_image_angle_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -266,6 +266,7 @@ pub async fn gemini_25_flash_edit_image_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -298,6 +298,7 @@ pub async fn gpt_image_1_edit_image_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -271,6 +271,7 @@ pub async fn qwen_edit_image_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -320,6 +320,7 @@ pub async fn bytedance_seedream_5_lite_multi_function_image_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -341,6 +341,7 @@ pub async fn bytedance_seedream_v4_multi_function_image_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -338,6 +338,7 @@ pub async fn bytedance_seedream_v4p5_multi_function_image_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -389,6 +389,7 @@ pub async fn gpt_image_1p5_multi_function_image_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -350,6 +350,7 @@ pub async fn nano_banana_2_multi_function_image_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -338,6 +338,7 @@ pub async fn nano_banana_multi_function_image_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -359,6 +359,7 @@ pub async fn nano_banana_pro_multi_function_image_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -218,6 +218,7 @@ pub async fn generate_flux_pro_11_text_to_image_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -226,6 +226,7 @@ pub async fn generate_flux_pro_11_ultra_text_to_image_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -232,6 +232,7 @@ pub async fn generate_gpt_image_1_text_to_image_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -267,6 +267,7 @@ pub async fn generate_kling_1_6_pro_video_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -254,6 +254,7 @@ pub async fn generate_kling_2_1_master_video_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -270,6 +270,7 @@ pub async fn generate_kling_2_1_pro_video_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -288,6 +288,7 @@ pub async fn generate_seedance_1_0_lite_image_to_video_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -262,6 +262,7 @@ pub async fn generate_seedance_1_0_pro_image_to_video_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -259,6 +259,7 @@ pub async fn generate_veo_2_image_to_video_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -264,6 +264,7 @@ pub async fn generate_veo_3_fast_image_to_video_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -268,6 +268,7 @@ pub async fn generate_veo_3_image_to_video_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -347,6 +347,7 @@ pub async fn kling_2p5_turbo_pro_multi_function_video_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -344,6 +344,7 @@ pub async fn kling_2p6_pro_multi_function_video_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -349,6 +349,7 @@ pub async fn kling_3p0_pro_multi_function_video_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -349,6 +349,7 @@ pub async fn kling_3p0_standard_multi_function_video_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -336,6 +336,7 @@ pub async fn seedance_1p5_pro_multi_function_video_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
mysql_executor: &mut *transaction,
starting_job_status_override: None,
maybe_frontend_failure_category: None,
@@ -436,6 +436,7 @@ pub async fn seedance_2p0_multi_function_video_gen_handler(
creator_ip_address: &ip_address,
creator_set_visibility: Visibility::Public,
maybe_platform_type: get_request_platform_type(&http_request),
maybe_cost_estimates: None,
maybe_debug_log_event_token: None,
mysql_executor: &mut *transaction,
phantom: Default::default(),

Some files were not shown because too many files have changed in this diff Show More