use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use crate::json::js_f64;
pub type TimestampMs = i64;
#[derive(
Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
)]
#[serde(rename_all = "lowercase")]
pub enum ThinkingLevel {
#[default]
Off,
Minimal,
Low,
Medium,
High,
Xhigh,
Max,
}
impl ThinkingLevel {
pub const ALL: [Self; 7] = [
Self::Off,
Self::Minimal,
Self::Low,
Self::Medium,
Self::High,
Self::Xhigh,
Self::Max,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Off => "off",
Self::Minimal => "minimal",
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::Xhigh => "xhigh",
Self::Max => "max",
}
}
#[must_use]
pub fn clamp(self, available: &[Self]) -> Self {
if available.contains(&self) {
return self;
}
let requested = self as usize;
Self::ALL[requested..]
.iter()
.chain(Self::ALL[..requested].iter().rev())
.copied()
.find(|level| available.contains(level))
.unwrap_or_else(|| available.first().copied().unwrap_or(Self::Off))
}
}
impl fmt::Display for ThinkingLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("unknown thinking level: {0}")]
pub struct UnknownThinkingLevel(pub String);
impl FromStr for ThinkingLevel {
type Err = UnknownThinkingLevel;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::ALL
.into_iter()
.find(|level| level.as_str() == s)
.ok_or_else(|| UnknownThinkingLevel(s.to_owned()))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum StopReason {
Pending,
Stop,
Length,
ToolUse,
Error,
Aborted,
Deferred,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum InputModality {
Text,
Image,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CacheRetention {
None,
Short,
Long,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UsageCost {
#[serde(with = "js_f64")]
pub input: f64,
#[serde(with = "js_f64")]
pub output: f64,
#[serde(with = "js_f64")]
pub cache_read: f64,
#[serde(with = "js_f64")]
pub cache_write: f64,
#[serde(with = "js_f64")]
pub total: f64,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Usage {
pub input: u64,
pub output: u64,
pub cache_read: u64,
pub cache_write: u64,
#[serde(
rename = "cacheWrite1h",
default,
skip_serializing_if = "Option::is_none"
)]
pub cache_write_1h: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning: Option<u64>,
pub total_tokens: u64,
#[serde(default)]
pub cost: UsageCost,
}
impl Usage {
pub fn accumulate(&mut self, other: &Self) {
self.input += other.input;
self.output += other.output;
self.cache_read += other.cache_read;
self.cache_write += other.cache_write;
self.cache_write_1h = add_opt(self.cache_write_1h, other.cache_write_1h);
self.reasoning = add_opt(self.reasoning, other.reasoning);
self.total_tokens += other.total_tokens;
self.cost.input += other.cost.input;
self.cost.output += other.cost.output;
self.cost.cache_read += other.cost.cache_read;
self.cost.cache_write += other.cost.cache_write;
self.cost.total += other.cost.total;
}
}
fn add_opt(a: Option<u64>, b: Option<u64>) -> Option<u64> {
match (a, b) {
(None, None) => None,
(a, b) => Some(a.unwrap_or(0) + b.unwrap_or(0)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn thinking_level_wire_values() {
assert_eq!(
serde_json::to_value(ThinkingLevel::Xhigh).ok(),
Some(json!("xhigh"))
);
assert_eq!("max".parse::<ThinkingLevel>(), Ok(ThinkingLevel::Max));
assert!("x_high".parse::<ThinkingLevel>().is_err());
}
#[test]
fn clamp_prefers_higher_then_lower() {
use ThinkingLevel::{High, Low, Max, Medium, Minimal, Off, Xhigh};
let available = [Off, Minimal, Low, Medium, High];
assert_eq!(Xhigh.clamp(&available), High);
assert_eq!(Max.clamp(&available), High);
assert_eq!(Low.clamp(&available), Low);
assert_eq!(Minimal.clamp(&[Off, Medium]), Medium);
assert_eq!(High.clamp(&[Off]), Off);
assert_eq!(High.clamp(&[]), Off);
}
#[test]
fn usage_round_trips_pi_shape() {
let raw = json!({"input":0,"output":0,"cacheRead":50000,"cacheWrite":0,"totalTokens":50000,
"cost":{"input":0,"output":0,"cacheRead":0.015,"cacheWrite":0,"total":0.015}});
let usage: Usage = serde_json::from_value(raw.clone()).unwrap_or_default();
assert_eq!(usage.cache_read, 50_000);
assert_eq!(serde_json::to_value(usage).ok(), Some(raw));
let text = serde_json::to_string(&usage).unwrap_or_default();
assert!(text.contains("\"input\":0,"), "{text}");
}
}