cost calculators

This commit is contained in:
Brandon Thomas committed 2026-05-23 21:25:46 -04:00
1 parent bdfa8bc729
commit 4ab13bb11a
14 files changed
+1341

No files matched your search

@@ -1,2 +1,3 @@
pub mod requests;
pub mod traits;
pub mod types;
@@ -0,0 +1,299 @@
use crate::api::requests::images::image_edit::image_edit::ImageEditRequest;
use crate::api::requests::images::image_generation::cost::output_mills_per_image;
use crate::api::traits::grok_request_cost_calculator_trait::{GrokRequestCostCalculator, UsdMills};
use crate::api::types::image_types::image_model::ImageModel;
use crate::api::types::image_types::image_resolution::ImageResolution;
// `image_edit` bills BOTH input (per source image fed in) AND output (per
// generated image). Per <https://docs.x.ai/developers/pricing>:
//
// grok-imagine-image input $0.002/img → 2 mills/img
// grok-imagine-image-quality input $0.010/img → 10 mills/img
//
// Output rates match `image_generation` (shared helper `output_mills_per_image`).
impl GrokRequestCostCalculator for ImageEditRequest {
fn calculate_cost_in_mills(&self) -> UsdMills {
let model = self.model.clone().unwrap_or(ImageModel::GrokImagineImageQuality);
let resolution = self.resolution.unwrap_or(ImageResolution::OneK);
let input_mills_per_img = input_mills_per_image(&model);
let output_mills_per_img = output_mills_per_image(&model, resolution);
let n = self.number_images.unwrap_or(1) as u64;
let source_count = self.source_images.len() as u64;
(input_mills_per_img * source_count) + (output_mills_per_img * n)
}
}
/// Input rate in mills per source image, by model.
fn input_mills_per_image(model: &ImageModel) -> UsdMills {
match model {
ImageModel::GrokImagineImage => 2,
ImageModel::GrokImagineImageQuality => 10,
// Custom: conservatively assume the more expensive `-quality` tier.
ImageModel::Custom(_) => 10,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::requests::images::image_edit::image_edit::ImageSource;
use crate::api::types::image_types::image_aspect_ratio::ImageAspectRatio;
fn make_request(
model: Option<ImageModel>,
resolution: Option<ImageResolution>,
number_images: Option<u32>,
source_count: usize,
) -> ImageEditRequest {
ImageEditRequest {
prompt: "test".to_string(),
source_images: (0..source_count)
.map(|i| ImageSource::Url(format!("https://example.com/{i}.png")))
.collect(),
model,
number_images,
aspect_ratio: None,
resolution,
response_format: None,
user: None,
}
}
// ── Per-rate components ──
mod per_rate_components {
use super::*;
#[test]
fn standard_input_is_2_mills_per_source() {
assert_eq!(input_mills_per_image(&ImageModel::GrokImagineImage), 2);
}
#[test]
fn quality_input_is_10_mills_per_source() {
assert_eq!(input_mills_per_image(&ImageModel::GrokImagineImageQuality), 10);
}
#[test]
fn custom_input_defaults_to_quality_rate() {
assert_eq!(input_mills_per_image(&ImageModel::Custom("anything".to_string())), 10);
}
}
// ── Single source + single output, every (model, resolution) pair ──
mod single_source_single_output {
use super::*;
#[test]
fn standard_1k() {
// input 2 + output 20 = 22 mills, 3¢ (ceil)
let req = make_request(Some(ImageModel::GrokImagineImage), Some(ImageResolution::OneK), Some(1), 1);
assert_eq!(req.calculate_cost_in_mills(), 22);
assert_eq!(req.calculate_cost_in_cents(), 3);
}
#[test]
fn standard_2k() {
// input 2 + output 20 = 22 mills, 3¢
let req = make_request(Some(ImageModel::GrokImagineImage), Some(ImageResolution::TwoK), Some(1), 1);
assert_eq!(req.calculate_cost_in_mills(), 22);
assert_eq!(req.calculate_cost_in_cents(), 3);
}
#[test]
fn quality_1k() {
// input 10 + output 50 = 60 mills, 6¢
let req = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::OneK), Some(1), 1);
assert_eq!(req.calculate_cost_in_mills(), 60);
assert_eq!(req.calculate_cost_in_cents(), 6);
}
#[test]
fn quality_2k() {
// input 10 + output 70 = 80 mills, 8¢
let req = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::TwoK), Some(1), 1);
assert_eq!(req.calculate_cost_in_mills(), 80);
assert_eq!(req.calculate_cost_in_cents(), 8);
}
}
// ── Multi-source edit (up to 3 per xAI's docs) ──
mod multi_source {
use super::*;
#[test]
fn quality_1k_three_sources_one_output() {
// input 10 × 3 = 30, output 50, total 80 mills, 8¢
let req = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::OneK), Some(1), 3);
assert_eq!(req.calculate_cost_in_mills(), 80);
assert_eq!(req.calculate_cost_in_cents(), 8);
}
#[test]
fn standard_1k_three_sources_two_outputs() {
// input 2 × 3 = 6, output 20 × 2 = 40, total 46 mills, 5¢ (ceil)
let req = make_request(Some(ImageModel::GrokImagineImage), Some(ImageResolution::OneK), Some(2), 3);
assert_eq!(req.calculate_cost_in_mills(), 46);
assert_eq!(req.calculate_cost_in_cents(), 5);
}
}
// ── Batch output scaling (n > 1) ──
mod batch_scaling {
use super::*;
#[test]
fn batch_scales_only_output_not_input() {
let single = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::TwoK), Some(1), 1)
.calculate_cost_in_mills(); // 10 + 70 = 80
let quad = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::TwoK), Some(4), 1)
.calculate_cost_in_mills(); // 10 + 70 × 4 = 290
assert_eq!(single, 80);
assert_eq!(quad, 290);
// Input doesn't scale with n.
assert_ne!(quad, single * 4);
}
#[test]
fn quality_1k_scaling_1_through_8() {
for n in 1u32..=8 {
let req = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::OneK), Some(n), 1);
assert_eq!(req.calculate_cost_in_mills(), 10 + 50 * n as u64, "n={n}");
}
}
}
// ── Defaults ──
mod defaults {
use super::*;
#[test]
fn fully_default_with_one_source() {
// model=quality, res=1k, n=1, sources=1 → 10 + 50 = 60 mills
let req = make_request(None, None, None, 1);
assert_eq!(req.calculate_cost_in_mills(), 60);
assert_eq!(req.calculate_cost_in_cents(), 6);
}
}
// ── Independence from non-pricing fields ──
mod independence {
use super::*;
#[test]
fn cost_is_independent_of_aspect_ratio() {
let mut base = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::OneK), Some(1), 1);
let base_cost = base.calculate_cost_in_mills();
for ar in [
ImageAspectRatio::Square,
ImageAspectRatio::Landscape16x9,
ImageAspectRatio::Portrait9x16,
ImageAspectRatio::Auto,
] {
base.aspect_ratio = Some(ar);
assert_eq!(base.calculate_cost_in_mills(), base_cost, "{ar:?}");
}
}
#[test]
fn cost_is_independent_of_source_url_content() {
let mut req = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::OneK), Some(1), 0);
req.source_images = vec![
ImageSource::Url("https://example.com/a.png".to_string()),
ImageSource::FileId("file_abc".to_string()),
];
// 10 × 2 + 50 = 70 mills
assert_eq!(req.calculate_cost_in_mills(), 70);
}
}
// ── Exhaustive matrix ──
mod exhaustive_matrix {
use super::*;
// (model, resolution, n, sources, expected_mills)
const CASES: &[(ImageModelKind, ImageResolution, u32, usize, u64)] = &[
// grok-imagine-image (standard) — input 2/img, output 20/img regardless of resolution
(ImageModelKind::Standard, ImageResolution::OneK, 1, 1, 22),
(ImageModelKind::Standard, ImageResolution::OneK, 1, 2, 24),
(ImageModelKind::Standard, ImageResolution::OneK, 1, 3, 26),
(ImageModelKind::Standard, ImageResolution::OneK, 2, 1, 42),
(ImageModelKind::Standard, ImageResolution::OneK, 4, 1, 82),
(ImageModelKind::Standard, ImageResolution::TwoK, 1, 1, 22),
(ImageModelKind::Standard, ImageResolution::TwoK, 2, 2, 44),
(ImageModelKind::Standard, ImageResolution::TwoK, 3, 3, 66),
// grok-imagine-image-quality — input 10/img, output 50/img (1k) or 70/img (2k)
(ImageModelKind::Quality, ImageResolution::OneK, 1, 1, 60),
(ImageModelKind::Quality, ImageResolution::OneK, 1, 2, 70),
(ImageModelKind::Quality, ImageResolution::OneK, 1, 3, 80),
(ImageModelKind::Quality, ImageResolution::OneK, 2, 1, 110),
(ImageModelKind::Quality, ImageResolution::OneK, 4, 1, 210),
(ImageModelKind::Quality, ImageResolution::TwoK, 1, 1, 80),
(ImageModelKind::Quality, ImageResolution::TwoK, 2, 2, 160),
(ImageModelKind::Quality, ImageResolution::TwoK, 3, 3, 240),
(ImageModelKind::Quality, ImageResolution::TwoK, 4, 3, 310),
];
#[derive(Copy, Clone, Debug)]
enum ImageModelKind { Standard, Quality }
fn to_model(kind: ImageModelKind) -> ImageModel {
match kind {
ImageModelKind::Standard => ImageModel::GrokImagineImage,
ImageModelKind::Quality => ImageModel::GrokImagineImageQuality,
}
}
#[test]
fn all_matrix_cases() {
for &(kind, res, n, sources, expected) in CASES {
let req = make_request(Some(to_model(kind)), Some(res), Some(n), sources);
assert_eq!(req.calculate_cost_in_mills(), expected,
"model={:?} res={:?} n={} sources={}", kind, res, n, sources);
}
}
}
// ── Monotonicity ──
mod monotonicity {
use super::*;
#[test]
fn more_sources_cost_more() {
let one = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::OneK), Some(1), 1)
.calculate_cost_in_mills();
let three = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::OneK), Some(1), 3)
.calculate_cost_in_mills();
assert!(one < three);
}
#[test]
fn more_outputs_cost_more() {
let one = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::OneK), Some(1), 1)
.calculate_cost_in_mills();
let four = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::OneK), Some(4), 1)
.calculate_cost_in_mills();
assert!(one < four);
}
#[test]
fn quality_costs_more_than_standard() {
for res in [ImageResolution::OneK, ImageResolution::TwoK] {
let std = make_request(Some(ImageModel::GrokImagineImage), Some(res), Some(2), 2).calculate_cost_in_mills();
let qua = make_request(Some(ImageModel::GrokImagineImageQuality), Some(res), Some(2), 2).calculate_cost_in_mills();
assert!(std < qua, "standard ({std}) should be < quality ({qua}) at {res:?}");
}
}
}
}
@@ -1,2 +1,3 @@
pub mod cost;
pub mod image_edit;
pub(crate) mod request_types;
@@ -0,0 +1,249 @@
use crate::api::requests::images::image_generation::image_generation::ImageGenerationRequest;
use crate::api::traits::grok_request_cost_calculator_trait::{GrokRequestCostCalculator, UsdMills};
use crate::api::types::image_types::image_model::ImageModel;
use crate::api::types::image_types::image_resolution::ImageResolution;
// xAI published rates as of these docs: <https://docs.x.ai/developers/pricing>
//
// grok-imagine-image 1K $0.02/img, 2K $0.02/img (input $0.002/img)
// grok-imagine-image-quality 1K $0.05/img, 2K $0.07/img (input $0.010/img)
//
// In mills (1¢ = 10 mills):
//
// grok-imagine-image: 1K 20 mills/img, 2K 20 mills/img
// grok-imagine-image-quality: 1K 50 mills/img, 2K 70 mills/img
//
// `image_generation` has no INPUT cost (it's pure text → image) — only output.
impl GrokRequestCostCalculator for ImageGenerationRequest {
fn calculate_cost_in_mills(&self) -> UsdMills {
let model = self.model.clone().unwrap_or(ImageModel::GrokImagineImageQuality);
let resolution = self.resolution.unwrap_or(ImageResolution::OneK);
let mills_per_image = output_mills_per_image(&model, resolution);
// Server default for `n` is 1.
let n = self.number_images.unwrap_or(1) as u64;
mills_per_image * n
}
}
/// Output rate in mills per generated image. Used by both
/// `image_generation` and `image_edit` (same output side).
pub(crate) fn output_mills_per_image(model: &ImageModel, resolution: ImageResolution) -> UsdMills {
match (model, resolution) {
(ImageModel::GrokImagineImage, ImageResolution::OneK) => 20,
(ImageModel::GrokImagineImage, ImageResolution::TwoK) => 20,
(ImageModel::GrokImagineImageQuality, ImageResolution::OneK) => 50,
(ImageModel::GrokImagineImageQuality, ImageResolution::TwoK) => 70,
// Custom: conservatively assume the more expensive `-quality` tier.
(ImageModel::Custom(_), ImageResolution::OneK) => 50,
(ImageModel::Custom(_), ImageResolution::TwoK) => 70,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::types::image_types::image_aspect_ratio::ImageAspectRatio;
use crate::api::types::image_types::image_response_format::ImageResponseFormat;
fn make_request(
model: Option<ImageModel>,
resolution: Option<ImageResolution>,
number_images: Option<u32>,
) -> ImageGenerationRequest {
ImageGenerationRequest {
prompt: "test".to_string(),
model,
number_images,
aspect_ratio: None,
resolution,
response_format: None,
user: None,
}
}
// ── Per-image rate at each (model, resolution) combo ──
mod per_image_rates {
use super::*;
#[test]
fn standard_1k_is_20_mills() {
let req = make_request(Some(ImageModel::GrokImagineImage), Some(ImageResolution::OneK), Some(1));
assert_eq!(req.calculate_cost_in_mills(), 20);
// 20 mills = 2¢ (ceiling)
assert_eq!(req.calculate_cost_in_cents(), 2);
}
#[test]
fn standard_2k_is_20_mills() {
let req = make_request(Some(ImageModel::GrokImagineImage), Some(ImageResolution::TwoK), Some(1));
assert_eq!(req.calculate_cost_in_mills(), 20);
assert_eq!(req.calculate_cost_in_cents(), 2);
}
#[test]
fn quality_1k_is_50_mills() {
let req = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::OneK), Some(1));
assert_eq!(req.calculate_cost_in_mills(), 50);
assert_eq!(req.calculate_cost_in_cents(), 5);
}
#[test]
fn quality_2k_is_70_mills() {
let req = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::TwoK), Some(1));
assert_eq!(req.calculate_cost_in_mills(), 70);
assert_eq!(req.calculate_cost_in_cents(), 7);
}
}
// ── Defaults ──
mod defaults {
use super::*;
#[test]
fn default_model_is_quality() {
let r1 = make_request(None, Some(ImageResolution::OneK), Some(1));
let r2 = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::OneK), Some(1));
assert_eq!(r1.calculate_cost_in_mills(), r2.calculate_cost_in_mills());
}
#[test]
fn default_resolution_is_one_k() {
let r1 = make_request(Some(ImageModel::GrokImagineImageQuality), None, Some(1));
let r2 = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::OneK), Some(1));
assert_eq!(r1.calculate_cost_in_mills(), r2.calculate_cost_in_mills());
}
#[test]
fn default_n_is_one() {
let r1 = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::OneK), None);
let r2 = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::OneK), Some(1));
assert_eq!(r1.calculate_cost_in_mills(), r2.calculate_cost_in_mills());
}
#[test]
fn fully_default_request_costs_50_mills() {
// All defaults = grok-imagine-image-quality, 1k, n=1 → 50 mills
let req = make_request(None, None, None);
assert_eq!(req.calculate_cost_in_mills(), 50);
assert_eq!(req.calculate_cost_in_cents(), 5);
}
}
// ── Batch scaling ──
mod batch_scaling {
use super::*;
#[test]
fn n_scales_linearly_standard_1k() {
for n in 1u32..=10 {
let req = make_request(Some(ImageModel::GrokImagineImage), Some(ImageResolution::OneK), Some(n));
assert_eq!(req.calculate_cost_in_mills(), 20 * n as u64, "n={n}");
}
}
#[test]
fn n_scales_linearly_quality_2k() {
for n in 1u32..=10 {
let req = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::TwoK), Some(n));
assert_eq!(req.calculate_cost_in_mills(), 70 * n as u64, "n={n}");
}
}
#[test]
fn batch_of_four_quality_2k() {
let req = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::TwoK), Some(4));
// 70 mills × 4 = 280 mills = 28¢
assert_eq!(req.calculate_cost_in_mills(), 280);
assert_eq!(req.calculate_cost_in_cents(), 28);
}
}
// ── Custom model (forward-compat) ──
mod custom_model {
use super::*;
#[test]
fn custom_model_uses_quality_rate() {
let custom = ImageModel::Custom("grok-imagine-image-future".to_string());
let r_custom_1k = make_request(Some(custom.clone()), Some(ImageResolution::OneK), Some(1));
let r_quality_1k = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::OneK), Some(1));
assert_eq!(r_custom_1k.calculate_cost_in_mills(), r_quality_1k.calculate_cost_in_mills());
let r_custom_2k = make_request(Some(custom), Some(ImageResolution::TwoK), Some(1));
let r_quality_2k = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::TwoK), Some(1));
assert_eq!(r_custom_2k.calculate_cost_in_mills(), r_quality_2k.calculate_cost_in_mills());
}
}
// ── Cost doesn't depend on irrelevant fields ──
mod independence {
use super::*;
#[test]
fn cost_is_independent_of_aspect_ratio() {
let mut base = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::OneK), Some(1));
let base_cost = base.calculate_cost_in_mills();
for ar in [
ImageAspectRatio::Square,
ImageAspectRatio::Landscape16x9,
ImageAspectRatio::Portrait9x16,
ImageAspectRatio::Landscape20x9,
ImageAspectRatio::Auto,
] {
base.aspect_ratio = Some(ar);
assert_eq!(base.calculate_cost_in_mills(), base_cost, "{ar:?}");
}
}
#[test]
fn cost_is_independent_of_response_format() {
let mut base = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::OneK), Some(1));
let base_cost = base.calculate_cost_in_mills();
base.response_format = Some(ImageResponseFormat::Url);
assert_eq!(base.calculate_cost_in_mills(), base_cost);
base.response_format = Some(ImageResponseFormat::B64Json);
assert_eq!(base.calculate_cost_in_mills(), base_cost);
}
#[test]
fn cost_is_independent_of_user_field() {
let mut base = make_request(Some(ImageModel::GrokImagineImageQuality), Some(ImageResolution::OneK), Some(1));
let base_cost = base.calculate_cost_in_mills();
base.user = Some("any-user-id".to_string());
assert_eq!(base.calculate_cost_in_mills(), base_cost);
}
}
// ── Monotonicity sanity checks ──
mod monotonicity {
use super::*;
#[test]
fn quality_is_at_least_as_expensive_as_standard() {
for res in [ImageResolution::OneK, ImageResolution::TwoK] {
let std = make_request(Some(ImageModel::GrokImagineImage), Some(res), Some(1)).calculate_cost_in_mills();
let q = make_request(Some(ImageModel::GrokImagineImageQuality), Some(res), Some(1)).calculate_cost_in_mills();
assert!(std <= q, "standard ({std}) should be <= quality ({q}) at {res:?}");
}
}
#[test]
fn two_k_is_at_least_as_expensive_as_one_k() {
for model in [ImageModel::GrokImagineImage, ImageModel::GrokImagineImageQuality] {
let lo = make_request(Some(model.clone()), Some(ImageResolution::OneK), Some(1)).calculate_cost_in_mills();
let hi = make_request(Some(model.clone()), Some(ImageResolution::TwoK), Some(1)).calculate_cost_in_mills();
assert!(lo <= hi, "1k ({lo}) should be <= 2k ({hi}) for {:?}", model.as_str());
}
}
}
}
@@ -1,2 +1,3 @@
pub mod cost;
pub mod image_generation;
pub(crate) mod request_types;
@@ -0,0 +1,157 @@
use crate::api::requests::videos::video_edit::video_edit::VideoEditRequest;
use crate::api::requests::videos::video_generation::cost::{
output_mills_per_second,
INPUT_MILLS_PER_SECOND_OF_SOURCE_VIDEO,
};
use crate::api::traits::grok_request_cost_calculator_trait::{GrokRequestCostCalculator, UsdMills};
use crate::api::traits::grok_request_source_duration_cost_calculator_trait::GrokRequestSourceDurationCostCalculator;
use crate::api::types::video_types::video_resolution::VideoResolution;
// `video_edit` cost depends entirely on the source video's duration:
//
// Input: $0.01/sec × source_duration (10 mills/sec)
// Output: $0.07/sec × source_duration (70 mills/sec, since
// output mirrors the source resolution capped at 720p, and
// output duration mirrors source duration per xAI docs)
//
// Neither piece is knowable from the `VideoEditRequest` alone — the
// `source_video` field only points at a URL/file_id, not at metadata. The
// trait impl therefore returns 0 so callers don't get a misleading bound.
//
// If you need a real estimate, fetch the source's metadata out-of-band
// (e.g. `ffprobe` for a local file, or HEAD + Range read for a remote MP4)
// and compute:
//
// mills = source_duration_seconds * (INPUT_MILLS_PER_SECOND_OF_SOURCE_VIDEO
// + output_mills_per_second(VideoResolution::SevenTwentyP))
// = source_duration_seconds * (10 + 70)
// = source_duration_seconds * 80
impl GrokRequestCostCalculator for VideoEditRequest {
/// **Always returns 0.** Cost cannot be computed from the request body
/// — see the module-level comment for the formula you'd apply if you
/// have the source video's duration available out-of-band.
///
/// If you DO have the source duration, use
/// [`GrokRequestSourceDurationCostCalculator::calculate_cost_in_mills_with_source_duration`]
/// instead.
fn calculate_cost_in_mills(&self) -> UsdMills {
0
}
}
impl GrokRequestSourceDurationCostCalculator for VideoEditRequest {
/// Output duration mirrors the source, output resolution mirrors the
/// source capped at 720p. Both input and output are billed per second of
/// source duration:
///
/// total = source_duration_seconds × (INPUT + OUTPUT_720P)
/// = source_duration_seconds × (10 + 70) = 80 mills/sec
fn calculate_cost_in_mills_with_source_duration(&self, source_duration_seconds: u32) -> UsdMills {
let secs = source_duration_seconds as u64;
let per_second = INPUT_MILLS_PER_SECOND_OF_SOURCE_VIDEO + output_mills_per_second(VideoResolution::SevenTwentyP);
per_second * secs
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::requests::videos::video_edit::video_edit::{VideoEditRequest, VideoSource};
use crate::api::types::video_types::video_model::VideoModel;
fn make_request(source_video: VideoSource, model: Option<VideoModel>) -> VideoEditRequest {
VideoEditRequest {
prompt: "test edit".to_string(),
source_video,
model,
user: None,
}
}
#[test]
fn always_returns_zero() {
let cases = [
VideoSource::Url("https://example.com/v.mp4".to_string()),
VideoSource::FileId("file_abc".to_string()),
];
for source in cases {
let req = make_request(source, None);
assert_eq!(req.calculate_cost_in_mills(), 0);
assert_eq!(req.calculate_cost_in_cents(), 0);
}
}
#[test]
fn zero_regardless_of_model() {
let req_default = make_request(VideoSource::Url("u".to_string()), None);
let req_known = make_request(VideoSource::Url("u".to_string()), Some(VideoModel::GrokImagineVideo));
let req_custom = make_request(VideoSource::Url("u".to_string()),
Some(VideoModel::Custom("future".to_string())));
assert_eq!(req_default.calculate_cost_in_mills(), 0);
assert_eq!(req_known.calculate_cost_in_mills(), 0);
assert_eq!(req_custom.calculate_cost_in_mills(), 0);
}
#[test]
fn zero_regardless_of_prompt_or_user() {
let mut req = make_request(VideoSource::Url("u".to_string()), None);
req.prompt = "an enormously long prompt with many tokens".repeat(100);
req.user = Some("user-id".to_string());
assert_eq!(req.calculate_cost_in_mills(), 0);
}
// ── Source-duration-aware variant ──
mod with_source_duration {
use super::*;
#[test]
fn five_second_source() {
// 80 × 5 = 400 mills = 40¢
let req = make_request(VideoSource::Url("u".to_string()), None);
assert_eq!(req.calculate_cost_in_mills_with_source_duration(5), 400);
assert_eq!(req.calculate_cost_in_cents_with_source_duration(5), 40);
}
#[test]
fn one_second_source() {
// 80 × 1 = 80 mills = 8¢
let req = make_request(VideoSource::Url("u".to_string()), None);
assert_eq!(req.calculate_cost_in_mills_with_source_duration(1), 80);
assert_eq!(req.calculate_cost_in_cents_with_source_duration(1), 8);
}
#[test]
fn zero_second_source_costs_zero() {
let req = make_request(VideoSource::Url("u".to_string()), None);
assert_eq!(req.calculate_cost_in_mills_with_source_duration(0), 0);
}
#[test]
fn scales_linearly_with_source_duration() {
let req = make_request(VideoSource::Url("u".to_string()), None);
for secs in 1u32..=60 {
assert_eq!(req.calculate_cost_in_mills_with_source_duration(secs), 80 * secs as u64, "secs={secs}");
}
}
#[test]
fn ten_second_source_is_one_dollar() {
// 80 × 10 = 800 mills = 80¢
let req = make_request(VideoSource::Url("u".to_string()), None);
assert_eq!(req.calculate_cost_in_mills_with_source_duration(10), 800);
assert_eq!(req.calculate_cost_in_cents_with_source_duration(10), 80);
}
#[test]
fn independent_of_source_kind() {
let url_req = make_request(VideoSource::Url("u".to_string()), None);
let file_req = make_request(VideoSource::FileId("f".to_string()), None);
assert_eq!(
url_req.calculate_cost_in_mills_with_source_duration(7),
file_req.calculate_cost_in_mills_with_source_duration(7),
);
}
}
}
@@ -1,2 +1,3 @@
pub mod cost;
pub mod video_edit;
pub(crate) mod request_types;
@@ -0,0 +1,233 @@
use crate::api::requests::videos::video_extension::video_extension::VideoExtensionRequest;
use crate::api::requests::videos::video_generation::cost::{
output_mills_per_second,
INPUT_MILLS_PER_SECOND_OF_SOURCE_VIDEO,
};
use crate::api::traits::grok_request_cost_calculator_trait::{GrokRequestCostCalculator, UsdMills};
use crate::api::traits::grok_request_source_duration_cost_calculator_trait::GrokRequestSourceDurationCostCalculator;
use crate::api::types::video_types::video_resolution::VideoResolution;
// `video_extension` charges:
//
// Output: $0.07/sec × extension_duration (70 mills/sec, since output
// inherits the source's resolution capped at 720p)
// Input: $0.01/sec × source_duration (10 mills/sec, UNKNOWN to us)
//
// `extension_duration` is the `duration` field on the request (xAI default
// is 6 seconds; range 1–10). `source_duration` is NOT in the request.
//
// Our `calculate_cost_in_mills` therefore returns ONLY the output portion.
// Callers who know the source duration should add:
//
// source_duration_seconds * INPUT_MILLS_PER_SECOND_OF_SOURCE_VIDEO
//
// to get the true total.
/// xAI default duration for video_extension when `duration` is omitted.
const DEFAULT_EXTENSION_DURATION_SECONDS: u32 = 6;
impl GrokRequestCostCalculator for VideoExtensionRequest {
/// Estimates the **output** portion only. The input portion depends on the
/// source video's duration, which isn't in the request; callers should
/// add it separately if they need a true total.
///
/// If you DO have the source duration, use
/// [`GrokRequestSourceDurationCostCalculator::calculate_cost_in_mills_with_source_duration`]
/// instead — that returns the true total.
fn calculate_cost_in_mills(&self) -> UsdMills {
let extension_duration = self.duration.unwrap_or(DEFAULT_EXTENSION_DURATION_SECONDS) as u64;
// Output inherits source resolution capped at 720p. Use 720p — both the
// worst case (most expensive) and the most common output for source
// videos generated by /v1/videos/generations at 720p.
output_mills_per_second(VideoResolution::SevenTwentyP) * extension_duration
}
}
impl GrokRequestSourceDurationCostCalculator for VideoExtensionRequest {
/// Full cost = output (extension portion, known from request) + input
/// (source duration × 10 mills/sec).
fn calculate_cost_in_mills_with_source_duration(&self, source_duration_seconds: u32) -> UsdMills {
let output_mills = self.calculate_cost_in_mills();
let input_mills = INPUT_MILLS_PER_SECOND_OF_SOURCE_VIDEO * source_duration_seconds as u64;
output_mills + input_mills
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::requests::videos::video_extension::video_extension::{
VideoExtensionRequest, VideoExtensionSource,
};
use crate::api::types::video_types::video_model::VideoModel;
fn make_request(
duration: Option<u32>,
source: VideoExtensionSource,
model: Option<VideoModel>,
) -> VideoExtensionRequest {
VideoExtensionRequest {
prompt: "test".to_string(),
source_video: source,
model,
duration,
}
}
fn url_source() -> VideoExtensionSource {
VideoExtensionSource::Url("https://example.com/v.mp4".to_string())
}
// ── Output portion (the part we CAN compute) ──
mod output_portion {
use super::*;
#[test]
fn default_duration_is_6_seconds() {
// 70 × 6 = 420 mills = 42¢
let req = make_request(None, url_source(), None);
assert_eq!(req.calculate_cost_in_mills(), 420);
assert_eq!(req.calculate_cost_in_cents(), 42);
}
#[test]
fn one_second_extension() {
// 70 × 1 = 70 mills = 7¢
let req = make_request(Some(1), url_source(), None);
assert_eq!(req.calculate_cost_in_mills(), 70);
assert_eq!(req.calculate_cost_in_cents(), 7);
}
#[test]
fn five_second_extension() {
// 70 × 5 = 350 mills = 35¢
let req = make_request(Some(5), url_source(), None);
assert_eq!(req.calculate_cost_in_mills(), 350);
assert_eq!(req.calculate_cost_in_cents(), 35);
}
#[test]
fn ten_second_extension_is_max() {
// 70 × 10 = 700 mills = 70¢
let req = make_request(Some(10), url_source(), None);
assert_eq!(req.calculate_cost_in_mills(), 700);
assert_eq!(req.calculate_cost_in_cents(), 70);
}
}
// ── Scaling ──
mod scaling {
use super::*;
#[test]
fn duration_scales_linearly() {
for d in 1u32..=10 {
let req = make_request(Some(d), url_source(), None);
assert_eq!(req.calculate_cost_in_mills(), 70 * d as u64, "d={d}");
}
}
}
// ── Independence from non-pricing fields ──
mod independence {
use super::*;
#[test]
fn cost_is_independent_of_source_kind() {
let url = make_request(Some(5), VideoExtensionSource::Url("u".to_string()), None);
let file = make_request(Some(5), VideoExtensionSource::FileId("f".to_string()), None);
assert_eq!(url.calculate_cost_in_mills(), file.calculate_cost_in_mills());
}
#[test]
fn cost_is_independent_of_model_variant() {
let mut base = make_request(Some(5), url_source(), None);
let base_cost = base.calculate_cost_in_mills();
base.model = Some(VideoModel::GrokImagineVideo);
assert_eq!(base.calculate_cost_in_mills(), base_cost);
base.model = Some(VideoModel::Custom("future".to_string()));
assert_eq!(base.calculate_cost_in_mills(), base_cost);
}
#[test]
fn cost_is_independent_of_prompt_length() {
let mut base = make_request(Some(5), url_source(), None);
let base_cost = base.calculate_cost_in_mills();
base.prompt = "a much longer prompt with many more words to influence pricing... or not".to_string();
assert_eq!(base.calculate_cost_in_mills(), base_cost);
}
}
// ── Source-duration-aware variant ──
mod with_source_duration {
use super::*;
#[test]
fn five_extension_ten_source() {
// output: 70 × 5 = 350 mills; input: 10 × 10 = 100 mills; total: 450
let req = make_request(Some(5), url_source(), None);
assert_eq!(req.calculate_cost_in_mills_with_source_duration(10), 450);
assert_eq!(req.calculate_cost_in_cents_with_source_duration(10), 45);
}
#[test]
fn default_extension_5_source() {
// default ext=6s: output 70 × 6 = 420; input 10 × 5 = 50; total 470
let req = make_request(None, url_source(), None);
assert_eq!(req.calculate_cost_in_mills_with_source_duration(5), 470);
assert_eq!(req.calculate_cost_in_cents_with_source_duration(5), 47);
}
#[test]
fn ten_extension_zero_source() {
// output 70 × 10 = 700; input 10 × 0 = 0; total 700
let req = make_request(Some(10), url_source(), None);
assert_eq!(req.calculate_cost_in_mills_with_source_duration(0), 700);
}
#[test]
fn source_duration_scales_linearly() {
let req = make_request(Some(5), url_source(), None); // output = 350
for secs in 0u32..=30 {
let expected = 350 + 10 * secs as u64;
assert_eq!(req.calculate_cost_in_mills_with_source_duration(secs), expected, "secs={secs}");
}
}
#[test]
fn extension_duration_scales_linearly() {
// Fix source at 10s (input portion = 100 mills).
for ext in 1u32..=10 {
let req = make_request(Some(ext), url_source(), None);
let expected = 70 * ext as u64 + 100;
assert_eq!(req.calculate_cost_in_mills_with_source_duration(10), expected, "ext={ext}");
}
}
#[test]
fn full_method_equals_output_only_plus_input() {
// The source-duration variant should always equal the base + input portion.
let req = make_request(Some(7), url_source(), None);
let base = req.calculate_cost_in_mills();
for secs in 0u32..=20 {
let full = req.calculate_cost_in_mills_with_source_duration(secs);
assert_eq!(full, base + 10 * secs as u64, "secs={secs}");
}
}
#[test]
fn cents_is_ceil_of_mills_over_ten() {
// 75 mills (ext=1s, source=1/2s rounding edge case)
// Make sure ceiling division behaves right.
let req = make_request(Some(1), url_source(), None); // output = 70 mills
// input 10 × 1 = 10; total = 80 mills = 8¢
assert_eq!(req.calculate_cost_in_mills_with_source_duration(1), 80);
assert_eq!(req.calculate_cost_in_cents_with_source_duration(1), 8);
}
}
}
@@ -1,2 +1,3 @@
pub mod cost;
pub mod video_extension;
pub(crate) mod request_types;
@@ -0,0 +1,328 @@
use crate::api::requests::videos::video_generation::video_generation::VideoGenerationRequest;
use crate::api::traits::grok_request_cost_calculator_trait::{GrokRequestCostCalculator, UsdMills};
use crate::api::types::video_types::video_resolution::VideoResolution;
// xAI pricing per <https://docs.x.ai/developers/pricing>:
//
// grok-imagine-video
// Output: 480p $0.05/sec, 720p $0.07/sec
// Input: $0.01/sec (source video) + $0.002/img (source images)
//
// In mills (1¢ = 10 mills):
//
// Output: 480p 50 mills/sec, 720p 70 mills/sec
// Input: 10 mills/sec (video), 2 mills/img (per source image)
//
// video_generation has NO input video — only optional input images. xAI
// defaults: `duration` = 8 seconds, `resolution` = "480p".
/// xAI default duration for video_generation when `duration` is omitted.
pub(crate) const DEFAULT_VIDEO_DURATION_SECONDS: u32 = 8;
/// Per-source-image input rate for video endpoints, in mills.
pub(crate) const INPUT_MILLS_PER_IMAGE: UsdMills = 2;
/// Per-source-second input rate (when a source video is supplied) for video
/// endpoints, in mills. Not used by `video_generation` (which has no source
/// video) but referenced by `video_edit` and `video_extension`.
pub(crate) const INPUT_MILLS_PER_SECOND_OF_SOURCE_VIDEO: UsdMills = 10;
impl GrokRequestCostCalculator for VideoGenerationRequest {
fn calculate_cost_in_mills(&self) -> UsdMills {
let duration = self.duration.unwrap_or(DEFAULT_VIDEO_DURATION_SECONDS) as u64;
let resolution = self.resolution.unwrap_or(VideoResolution::SevenTwentyP);
let output_mills = output_mills_per_second(resolution) * duration;
// Source image count = `image` (image-to-video) + len(`reference_images`)
// (reference-to-video). These are mutually exclusive at call time but the
// cost math doesn't need to enforce that — it just counts.
let source_image_count: u64 =
(self.image.is_some() as u64)
+ (self.reference_images.as_ref().map(|v| v.len() as u64).unwrap_or(0));
let input_mills = INPUT_MILLS_PER_IMAGE * source_image_count;
output_mills + input_mills
}
}
/// Output rate in mills per second of generated video, by resolution.
pub(crate) fn output_mills_per_second(resolution: VideoResolution) -> UsdMills {
match resolution {
VideoResolution::FourEightyP => 50,
VideoResolution::SevenTwentyP => 70,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::requests::videos::video_generation::video_generation::VideoImageSource;
use crate::api::types::video_types::video_aspect_ratio::VideoAspectRatio;
use crate::api::types::video_types::video_model::VideoModel;
fn make_request(
duration: Option<u32>,
resolution: Option<VideoResolution>,
image: Option<VideoImageSource>,
reference_images: Option<Vec<VideoImageSource>>,
) -> VideoGenerationRequest {
VideoGenerationRequest {
prompt: "test".to_string(),
model: None,
image,
reference_images,
aspect_ratio: None,
duration,
resolution,
user: None,
}
}
fn url_source() -> VideoImageSource {
VideoImageSource::Url("https://example.com/img.png".to_string())
}
// ── Output rate sanity ──
mod output_rate {
use super::*;
#[test]
fn rate_480p_is_50_mills_per_sec() {
assert_eq!(output_mills_per_second(VideoResolution::FourEightyP), 50);
}
#[test]
fn rate_720p_is_70_mills_per_sec() {
assert_eq!(output_mills_per_second(VideoResolution::SevenTwentyP), 70);
}
}
// ── Pure text-to-video (no input images) ──
mod text_to_video {
use super::*;
#[test]
fn fully_default_is_8s_480p() {
// 50 × 8 = 400 mills = 40¢
let req = make_request(None, None, None, None);
assert_eq!(req.calculate_cost_in_mills(), 400);
assert_eq!(req.calculate_cost_in_cents(), 40);
}
#[test]
fn five_seconds_480p() {
// 50 × 5 = 250 mills = 25¢
let req = make_request(Some(5), Some(VideoResolution::FourEightyP), None, None);
assert_eq!(req.calculate_cost_in_mills(), 250);
assert_eq!(req.calculate_cost_in_cents(), 25);
}
#[test]
fn five_seconds_720p() {
// 70 × 5 = 350 mills = 35¢
let req = make_request(Some(5), Some(VideoResolution::SevenTwentyP), None, None);
assert_eq!(req.calculate_cost_in_mills(), 350);
assert_eq!(req.calculate_cost_in_cents(), 35);
}
#[test]
fn fifteen_seconds_720p_is_max_duration() {
// 70 × 15 = 1050 mills = $1.05
let req = make_request(Some(15), Some(VideoResolution::SevenTwentyP), None, None);
assert_eq!(req.calculate_cost_in_mills(), 1050);
assert_eq!(req.calculate_cost_in_cents(), 105);
}
}
// ── Image-to-video (single source image) ──
mod image_to_video {
use super::*;
#[test]
fn five_seconds_480p_one_source_image() {
// 50 × 5 + 2 × 1 = 252 mills = 26¢ (ceil)
let req = make_request(Some(5), Some(VideoResolution::FourEightyP), Some(url_source()), None);
assert_eq!(req.calculate_cost_in_mills(), 252);
assert_eq!(req.calculate_cost_in_cents(), 26);
}
#[test]
fn five_seconds_720p_one_source_image() {
// 70 × 5 + 2 = 352 mills = 36¢ (ceil)
let req = make_request(Some(5), Some(VideoResolution::SevenTwentyP), Some(url_source()), None);
assert_eq!(req.calculate_cost_in_mills(), 352);
assert_eq!(req.calculate_cost_in_cents(), 36);
}
}
// ── Reference-to-video (multiple source images) ──
mod reference_to_video {
use super::*;
#[test]
fn five_seconds_720p_two_reference_images() {
// 70 × 5 + 2 × 2 = 354 mills = 36¢ (ceil)
let req = make_request(Some(5), Some(VideoResolution::SevenTwentyP), None, Some(vec![url_source(), url_source()]));
assert_eq!(req.calculate_cost_in_mills(), 354);
assert_eq!(req.calculate_cost_in_cents(), 36);
}
#[test]
fn five_seconds_720p_three_reference_images() {
// 70 × 5 + 2 × 3 = 356 mills
let req = make_request(
Some(5), Some(VideoResolution::SevenTwentyP),
None, Some(vec![url_source(), url_source(), url_source()]),
);
assert_eq!(req.calculate_cost_in_mills(), 356);
}
#[test]
fn input_image_count_scales_linearly() {
let base = make_request(Some(5), Some(VideoResolution::SevenTwentyP), None, None).calculate_cost_in_mills();
for n in 1usize..=5 {
let imgs: Vec<_> = (0..n).map(|_| url_source()).collect();
let req = make_request(Some(5), Some(VideoResolution::SevenTwentyP), None, Some(imgs));
assert_eq!(req.calculate_cost_in_mills(), base + INPUT_MILLS_PER_IMAGE * n as u64, "n={n}");
}
}
}
// ── Defaults ──
mod defaults {
use super::*;
#[test]
fn default_duration_is_8_seconds() {
let r1 = make_request(None, Some(VideoResolution::FourEightyP), None, None);
let r2 = make_request(Some(8), Some(VideoResolution::FourEightyP), None, None);
assert_eq!(r1.calculate_cost_in_mills(), r2.calculate_cost_in_mills());
}
#[test]
fn default_resolution_is_480p() {
let r1 = make_request(Some(5), None, None, None);
let r2 = make_request(Some(5), Some(VideoResolution::FourEightyP), None, None);
assert_eq!(r1.calculate_cost_in_mills(), r2.calculate_cost_in_mills());
}
}
// ── Independence from non-pricing fields ──
mod independence {
use super::*;
#[test]
fn cost_is_independent_of_aspect_ratio() {
let mut base = make_request(Some(5), Some(VideoResolution::FourEightyP), None, None);
let base_cost = base.calculate_cost_in_mills();
for ar in [
VideoAspectRatio::Square,
VideoAspectRatio::Landscape16x9,
VideoAspectRatio::Portrait9x16,
VideoAspectRatio::Portrait2x3,
] {
base.aspect_ratio = Some(ar);
assert_eq!(base.calculate_cost_in_mills(), base_cost, "{ar:?}");
}
}
#[test]
fn cost_is_independent_of_model_variant() {
// All `model` enum variants map to the same pricing (only one video model).
let mut base = make_request(Some(5), Some(VideoResolution::FourEightyP), None, None);
let base_cost = base.calculate_cost_in_mills();
for m in [
VideoModel::GrokImagineVideo,
VideoModel::Custom("grok-imagine-video-future".to_string()),
] {
base.model = Some(m.clone());
assert_eq!(base.calculate_cost_in_mills(), base_cost, "{:?}", m.as_str());
}
}
}
// ── Exhaustive matrix ──
mod exhaustive_matrix {
use super::*;
// (duration_s, resolution, image_to_video_source, reference_image_count, expected_mills)
const CASES: &[(u32, VideoResolution, bool, usize, u64)] = &[
// 480p, no input images
( 1, VideoResolution::FourEightyP, false, 0, 50),
( 5, VideoResolution::FourEightyP, false, 0, 250),
( 8, VideoResolution::FourEightyP, false, 0, 400),
(15, VideoResolution::FourEightyP, false, 0, 750),
// 720p, no input images
( 1, VideoResolution::SevenTwentyP, false, 0, 70),
( 5, VideoResolution::SevenTwentyP, false, 0, 350),
( 8, VideoResolution::SevenTwentyP, false, 0, 560),
(15, VideoResolution::SevenTwentyP, false, 0, 1050),
// 480p with image-to-video source (1 input image)
( 5, VideoResolution::FourEightyP, true, 0, 252),
(10, VideoResolution::FourEightyP, true, 0, 502),
// 720p with image-to-video source (1 input image)
( 5, VideoResolution::SevenTwentyP, true, 0, 352),
(10, VideoResolution::SevenTwentyP, true, 0, 702),
// 480p with reference_images (no image-to-video)
( 5, VideoResolution::FourEightyP, false, 1, 252),
( 5, VideoResolution::FourEightyP, false, 2, 254),
( 5, VideoResolution::FourEightyP, false, 3, 256),
// 720p with reference_images
( 5, VideoResolution::SevenTwentyP, false, 1, 352),
( 5, VideoResolution::SevenTwentyP, false, 2, 354),
( 5, VideoResolution::SevenTwentyP, false, 3, 356),
];
#[test]
fn all_matrix_cases() {
for &(duration, res, has_image, ref_count, expected) in CASES {
let image = if has_image { Some(url_source()) } else { None };
let refs = if ref_count > 0 {
Some((0..ref_count).map(|_| url_source()).collect())
} else { None };
let req = make_request(Some(duration), Some(res), image, refs);
assert_eq!(
req.calculate_cost_in_mills(), expected,
"duration={duration} res={res:?} has_image={has_image} ref_count={ref_count}",
);
}
}
}
// ── Monotonicity ──
mod monotonicity {
use super::*;
#[test]
fn longer_duration_costs_more() {
let short = make_request(Some(1), Some(VideoResolution::FourEightyP), None, None).calculate_cost_in_mills();
let long = make_request(Some(15), Some(VideoResolution::FourEightyP), None, None).calculate_cost_in_mills();
assert!(short < long);
}
#[test]
fn higher_resolution_costs_more() {
let p480 = make_request(Some(8), Some(VideoResolution::FourEightyP), None, None).calculate_cost_in_mills();
let p720 = make_request(Some(8), Some(VideoResolution::SevenTwentyP), None, None).calculate_cost_in_mills();
assert!(p480 < p720);
}
#[test]
fn more_reference_images_cost_more() {
let zero = make_request(Some(5), Some(VideoResolution::SevenTwentyP), None, None).calculate_cost_in_mills();
let three = make_request(Some(5), Some(VideoResolution::SevenTwentyP), None,
Some(vec![url_source(), url_source(), url_source()])).calculate_cost_in_mills();
assert!(zero < three);
}
}
}
@@ -1,2 +1,3 @@
pub mod cost;
pub mod video_generation;
pub(crate) mod request_types;
@@ -0,0 +1,36 @@
//! Cost-estimation trait for xAI (Grok) API requests.
//!
//! Implementations live next to each Request type in
//! `api/requests/<category>/<endpoint>/cost.rs`. Implemented on the public
//! `*Request` type (NOT the `*Args` wrapper), since the request body is what
//! drives the bill.
//!
//! All math is performed in mills (tenths of a US cent), which is the
//! natural unit for xAI's published rates — an image edit at the cheap tier
//! is $0.002/img = 2 mills, well below cent resolution.
/// Cost in US cents (1¢ = 1 unit).
pub type UsdCents = u64;
/// Cost in mills (1¢ = 10 mills). Use this when sub-cent precision matters
/// — many xAI rates are below 1 cent.
pub type UsdMills = u64;
/// Implemented on `*Request` types to estimate the cost of a Grok API call
/// without actually making it.
///
/// The estimate is derived from fields present in the request body. Some
/// endpoints (notably `video_edit` and the input-cost portion of
/// `video_extension`) depend on the source video's duration, which is NOT
/// part of the request. Those impls document their limitations.
pub trait GrokRequestCostCalculator {
/// Estimated cost rounded up to the nearest whole cent.
fn calculate_cost_in_cents(&self) -> UsdCents {
// Default impl: derive from mills via ceiling division.
self.calculate_cost_in_mills().div_ceil(10)
}
/// Estimated cost in mills (tenths of a cent). This is the canonical
/// method — implementations should override this one.
fn calculate_cost_in_mills(&self) -> UsdMills;
}
@@ -0,0 +1,31 @@
//! Source-duration-aware cost calculator for video endpoints that
//! reference an existing video (whose duration is NOT in the request body).
//!
//! Implemented by:
//! - [`crate::api::requests::videos::video_edit::video_edit::VideoEditRequest`]
//! - [`crate::api::requests::videos::video_extension::video_extension::VideoExtensionRequest`]
//!
//! Callers who have the source video's duration (from ffprobe, an upstream
//! generate request that produced it, or similar) can use this to get a
//! true total cost. Callers who don't can fall back to
//! [`crate::api::traits::grok_request_cost_calculator_trait::GrokRequestCostCalculator`]
//! — `video_edit` returns 0 there; `video_extension` returns the output
//! portion only.
use crate::api::traits::grok_request_cost_calculator_trait::{UsdCents, UsdMills};
/// Companion to `GrokRequestCostCalculator` for requests whose true cost
/// depends on a source video duration that isn't part of the request body.
pub trait GrokRequestSourceDurationCostCalculator {
/// Estimated total cost rounded up to the nearest whole cent.
fn calculate_cost_in_cents_with_source_duration(&self, source_duration_seconds: u32) -> UsdCents {
self.calculate_cost_in_mills_with_source_duration(source_duration_seconds).div_ceil(10)
}
/// Estimated total cost in mills. Implementations should override this.
///
/// `source_duration_seconds` is the runtime of the source video at the
/// `source_video` field's URL/file_id, in whole seconds. xAI bills source
/// video as input at 10 mills/sec regardless of resolution.
fn calculate_cost_in_mills_with_source_duration(&self, source_duration_seconds: u32) -> UsdMills;
}
@@ -0,0 +1,2 @@
pub mod grok_request_cost_calculator_trait;
pub mod grok_request_source_duration_cost_calculator_trait;