use serde_json::{Value, json};
use typesafe::Usage;
use crate::mock;
use crate::session::{Session, turns_to_json};
pub const PRICE_ENV: &str = "JEV_PRICE";
const ASSUMED_ANSWER: &str = r#"{"name":{"type":"noul","noul":0.123}}"#;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rates {
pub input: f64,
pub output: f64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QuestionEstimate {
pub name: String,
pub kind: String,
pub input_tokens: usize,
pub output_tokens: usize,
pub assumed: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Estimate {
pub model: String,
pub state_tokens: usize,
pub envelope_tokens: usize,
pub answer_envelope_tokens: usize,
pub questions: Vec<QuestionEstimate>,
pub input_tokens: usize,
pub output_tokens: usize,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Cost {
pub input: f64,
pub output: f64,
pub total: f64,
}
pub fn estimate_tokens(text: &str) -> usize {
#[derive(Default)]
struct Runs {
letters: usize,
digits: usize,
marks: usize,
}
impl Runs {
fn flush(&mut self) -> usize {
let tokens =
self.letters.div_ceil(4) + self.digits.div_ceil(3) + self.marks.div_ceil(2);
*self = Runs::default();
tokens
}
}
let mut tokens = 0;
let mut runs = Runs::default();
for ch in text.chars() {
if ch.is_whitespace() {
tokens += runs.flush();
} else if ch as u32 >= 0x2e80 {
tokens += runs.flush() + 1;
} else if ch.is_ascii_digit() {
if runs.letters > 0 || runs.marks > 0 {
tokens += runs.flush();
}
runs.digits += 1;
} else if ch.is_alphabetic() {
if runs.digits > 0 || runs.marks > 0 {
tokens += runs.flush();
}
runs.letters += 1;
} else {
if runs.letters > 0 || runs.digits > 0 {
tokens += runs.flush();
}
runs.marks += 1;
}
}
tokens + runs.flush()
}
pub fn estimate_json_tokens(value: &Value) -> usize {
estimate_tokens(&value.to_string())
}
pub fn estimate(session: &Session, model: &str) -> Estimate {
let state_tokens = estimate_json_tokens(&session.state);
let envelope_tokens =
estimate_json_tokens(&json!({"state": "", "model": model, "questions": {}}));
let answer_envelope_tokens = estimate_json_tokens(&json!({"model": model, "answers": {}}));
let questions: Vec<QuestionEstimate> = session
.questions
.iter()
.map(|(name, question)| {
let json = serde_json::to_value(question).unwrap_or(Value::Null);
let kind = json
.get("type")
.and_then(Value::as_str)
.unwrap_or("raw")
.to_owned();
let shape = mock::answer(&session.state, name, &json).map(|answer| {
let mut map = serde_json::Map::new();
map.insert(name.clone(), mock::answer_json(&answer));
Value::Object(map)
});
QuestionEstimate {
name: name.clone(),
kind,
input_tokens: estimate_tokens(&format!("{}:{}", json!(name), json)),
output_tokens: match &shape {
Some(value) => estimate_json_tokens(value),
None => estimate_tokens(ASSUMED_ANSWER),
},
assumed: shape.is_none(),
}
})
.collect();
let input_tokens =
state_tokens + envelope_tokens + questions.iter().map(|q| q.input_tokens).sum::<usize>();
let output_tokens =
answer_envelope_tokens + questions.iter().map(|q| q.output_tokens).sum::<usize>();
Estimate {
model: model.to_owned(),
state_tokens,
envelope_tokens,
answer_envelope_tokens,
questions,
input_tokens,
output_tokens,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Thread {
pub turns: usize,
pub calls: Vec<Estimate>,
pub input_tokens: usize,
pub output_tokens: usize,
}
pub fn thread(session: &Session, model: &str) -> Option<Thread> {
let turns = session.turns()?;
let mut so_far = session.clone();
let calls: Vec<Estimate> = (1..=turns.len())
.map(|n| {
so_far.state = turns_to_json(&turns[..n]);
estimate(&so_far, model)
})
.collect();
Some(Thread {
turns: turns.len(),
input_tokens: calls.iter().map(|c| c.input_tokens).sum(),
output_tokens: calls.iter().map(|c| c.output_tokens).sum(),
calls,
})
}
pub fn price(input_tokens: u64, output_tokens: u64, rates: Rates) -> Cost {
let input = input_tokens as f64 / 1_000_000.0 * rates.input;
let output = output_tokens as f64 / 1_000_000.0 * rates.output;
Cost {
input,
output,
total: input + output,
}
}
pub fn price_estimate(estimate: &Estimate, rates: Rates) -> Cost {
price(
estimate.input_tokens as u64,
estimate.output_tokens as u64,
rates,
)
}
pub fn price_usage(usage: &Usage, rates: Rates) -> Option<Cost> {
Some(price(usage.input_tokens?, usage.output_tokens?, rates))
}
pub fn parse_rates(text: &str) -> Result<Rates, String> {
let parts: Vec<&str> = text
.split(|c: char| c.is_whitespace() || c == '/' || c == ',')
.map(|p| p.trim().trim_start_matches('$'))
.filter(|p| !p.is_empty())
.collect();
if parts.len() != 2 {
return Err(
"Two rates, input then output, in dollars per million tokens: :cost 0.20/1.00"
.to_owned(),
);
}
let mut values = [0.0f64; 2];
for (slot, part) in values.iter_mut().zip(parts) {
match part.parse::<f64>() {
Ok(n) if n.is_finite() && n >= 0.0 => *slot = n,
_ => return Err(format!("{text:?} is not a pair of dollar amounts.")),
}
}
Ok(Rates {
input: values[0],
output: values[1],
})
}
pub fn rates_from_env() -> Option<Rates> {
rates_from_str(&std::env::var(PRICE_ENV).ok()?)
}
pub fn rates_from_str(value: &str) -> Option<Rates> {
if value.trim().is_empty() {
return None;
}
parse_rates(value).ok()
}
pub fn format_rates(rates: Rates) -> String {
format!(
"${}/${} per Mtok",
amount(rates.input),
amount(rates.output)
)
}
pub fn rates_value(rates: Rates) -> String {
format!("{}/{}", amount(rates.input), amount(rates.output))
}
pub fn usd(amount: f64) -> String {
if amount == 0.0 {
return "$0".to_owned();
}
if amount < 0.000_001 {
return "<$0.000001".to_owned();
}
let digits: usize = if amount >= 1.0 {
2
} else if amount >= 0.01 {
4
} else {
6
};
format!("${amount:.digits$}")
}
fn amount(n: f64) -> String {
if (n * 100.0).fract() == 0.0 {
format!("{n:.2}")
} else {
format!("{n}")
}
}