use serde::{Deserialize, Serialize};
use super::EvalQuestion;
use super::stats::count_f64;
const MILLION: f64 = 1_000_000.0;
const REQUESTS: f64 = 1_000.0;
const HOUR: f64 = 3_600.0;
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct Prices {
pub parent_in: Option<f64>,
pub parent_out: Option<f64>,
pub child_per_hour: Option<f64>,
}
#[must_use]
pub fn prices(compare: &crate::config::Compare, pod_price_per_hour: Option<f64>) -> (Prices, bool) {
let from_pod = compare.child_price_per_hour.is_none() && pod_price_per_hour.is_some();
let prices = Prices {
parent_in: compare.parent_price_in,
parent_out: compare.parent_price_out,
child_per_hour: compare.child_price_per_hour.or(pod_price_per_hour),
};
(prices, from_pod)
}
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
pub struct Costs {
pub parent_per_1k: Option<f64>,
pub child_per_1k: Option<f64>,
pub ratio: Option<f64>,
}
#[must_use]
pub fn costs(prices: &Prices, questions: &[EvalQuestion], mean_seconds: Option<f64>) -> Costs {
let parent_per_1k = match (prices.parent_in, prices.parent_out, questions.len()) {
(Some(price_in), Some(price_out), count) if count > 0 => {
let tokens = |pick: fn(&EvalQuestion) -> u64| {
let total: u64 = questions.iter().map(pick).sum();
f64::from(u32::try_from(total).unwrap_or(u32::MAX)) / count_f64(count)
};
let input = tokens(|question| question.parent_input_tokens);
let output = tokens(|question| question.parent_output_tokens);
Some(round6(
(input * price_in + output * price_out) / MILLION * REQUESTS,
))
},
_ => None,
};
let child_per_1k = match (prices.child_per_hour, mean_seconds) {
(Some(per_hour), Some(seconds)) => Some(round6(per_hour * seconds / HOUR * REQUESTS)),
_ => None,
};
let ratio = match (child_per_1k, parent_per_1k) {
(Some(child), Some(parent)) if parent > 0.0 => Some(child / parent),
_ => None,
};
Costs {
parent_per_1k,
child_per_1k,
ratio,
}
}
fn round6(value: f64) -> f64 {
(value * MILLION).round() / MILLION
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Compare;
fn question(input: u64, output: u64) -> EvalQuestion {
EvalQuestion {
id: "q".into(),
topic: "t".into(),
messages: Vec::new(),
parent: String::new(),
parent_input_tokens: input,
parent_output_tokens: output,
}
}
fn close(value: Option<f64>, expected: f64) -> bool {
value.is_some_and(|value| (value - expected).abs() < 1e-9)
}
#[test]
fn costs_per_thousand_requests() {
let questions = [question(100, 1_000), question(300, 3_000)];
let prices = Prices {
parent_in: Some(1.0),
parent_out: Some(2.0),
child_per_hour: Some(3.6),
};
let costs = costs(&prices, &questions, Some(0.5));
assert!(close(costs.parent_per_1k, 4.2));
assert!(close(costs.child_per_1k, 0.5));
assert_eq!(
costs.ratio.map(|ratio| (ratio * 1000.0).round()),
Some(119.0)
);
}
#[test]
fn missing_prices_leave_rows_out() {
let costs = costs(&Prices::default(), &[question(1, 1)], Some(1.0));
assert_eq!(costs, Costs::default());
let half = Prices {
parent_in: Some(1.0),
..Prices::default()
};
assert_eq!(costs_of(&half), None);
}
fn costs_of(prices: &Prices) -> Option<f64> {
costs(prices, &[question(1, 1)], None).parent_per_1k
}
fn config(child: Option<f64>) -> Compare {
Compare {
parent_price_in: Some(3.0),
parent_price_out: Some(15.0),
child_price_per_hour: child,
..Compare::default()
}
}
#[test]
fn the_configured_child_price_wins_over_the_pod() {
let (prices, from_pod) = prices(&config(Some(1.5)), Some(0.7));
assert_eq!(prices.child_per_hour, Some(1.5));
assert!(!from_pod);
assert_eq!(prices.parent_in, Some(3.0));
assert_eq!(prices.parent_out, Some(15.0));
}
#[test]
fn the_pod_price_is_the_default() {
let (prices, from_pod) = prices(&config(None), Some(0.7));
assert_eq!(prices.child_per_hour, Some(0.7));
assert!(from_pod);
}
#[test]
fn no_price_at_all_leaves_the_child_free() {
let (prices, from_pod) = prices(&config(None), None);
assert_eq!(prices.child_per_hour, None);
assert!(!from_pod);
}
}