use std::any::Any;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::{AnyEvent, Event, register_event_deserializer};
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[cfg_attr(feature = "tsify", derive(tsify_next::Tsify))]
#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))]
pub enum Modality {
#[default]
Llm,
Embedding,
ImageGen,
AudioTts,
AudioStt,
Video,
ThreeD,
BackgroundRemoval,
Custom(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "tsify", derive(tsify_next::Tsify))]
#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))]
pub struct UsageEvent {
pub provider: String,
pub model: String,
pub modality: Modality,
#[serde(default)]
pub prompt_tokens: u32,
#[serde(default)]
pub completion_tokens: u32,
#[serde(default)]
pub total_tokens: u32,
#[serde(default)]
pub reasoning_tokens: u32,
#[serde(default)]
pub cached_input_tokens: u32,
#[serde(default)]
pub audio_input_tokens: u32,
#[serde(default)]
pub audio_output_tokens: u32,
#[serde(default)]
pub image_count: u32,
#[serde(default)]
pub audio_seconds: f64,
#[serde(default)]
pub video_seconds: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub cost_usd: Option<f64>,
#[serde(default)]
pub latency_ms: u64,
pub run_id: Uuid,
}
impl Event for UsageEvent {
fn event_type() -> &'static str {
static REGISTER: std::sync::Once = std::sync::Once::new();
REGISTER.call_once(|| {
register_event_deserializer("blazen::UsageEvent", |value| {
serde_json::from_value::<UsageEvent>(value)
.ok()
.map(|e| Box::new(e) as _)
});
});
"blazen::UsageEvent"
}
fn event_type_id(&self) -> &'static str {
"blazen::UsageEvent"
}
fn as_any(&self) -> &dyn Any {
self
}
fn clone_boxed(&self) -> Box<dyn AnyEvent> {
Box::new(self.clone())
}
fn to_json(&self) -> serde_json::Value {
serde_json::to_value(self).expect("UsageEvent serialization should never fail")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_event() -> UsageEvent {
UsageEvent {
provider: "openai".to_string(),
model: "gpt-4o-mini".to_string(),
modality: Modality::Llm,
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
reasoning_tokens: 0,
cached_input_tokens: 0,
audio_input_tokens: 0,
audio_output_tokens: 0,
image_count: 0,
audio_seconds: 0.0,
video_seconds: 0.0,
cost_usd: Some(0.000_25),
latency_ms: 432,
run_id: Uuid::new_v4(),
}
}
#[test]
fn usage_event_type_id() {
assert_eq!(UsageEvent::event_type(), "blazen::UsageEvent");
let evt = sample_event();
assert_eq!(Event::event_type_id(&evt), "blazen::UsageEvent");
}
#[test]
fn usage_event_roundtrip() {
let evt = sample_event();
let json = Event::to_json(&evt);
let deserialized: UsageEvent = serde_json::from_value(json).unwrap();
assert_eq!(evt.provider, deserialized.provider);
assert_eq!(evt.model, deserialized.model);
assert_eq!(evt.modality, deserialized.modality);
assert_eq!(evt.prompt_tokens, deserialized.prompt_tokens);
assert_eq!(evt.completion_tokens, deserialized.completion_tokens);
assert_eq!(evt.total_tokens, deserialized.total_tokens);
assert_eq!(evt.reasoning_tokens, deserialized.reasoning_tokens);
assert_eq!(evt.cached_input_tokens, deserialized.cached_input_tokens);
assert_eq!(evt.audio_input_tokens, deserialized.audio_input_tokens);
assert_eq!(evt.audio_output_tokens, deserialized.audio_output_tokens);
assert_eq!(evt.image_count, deserialized.image_count);
assert!((evt.audio_seconds - deserialized.audio_seconds).abs() < f64::EPSILON);
assert!((evt.video_seconds - deserialized.video_seconds).abs() < f64::EPSILON);
assert_eq!(evt.cost_usd, deserialized.cost_usd);
assert_eq!(evt.latency_ms, deserialized.latency_ms);
assert_eq!(evt.run_id, deserialized.run_id);
}
#[test]
fn usage_event_downcast() {
let evt = sample_event();
let boxed: Box<dyn AnyEvent> = Box::new(evt.clone());
let downcasted = boxed.downcast_ref::<UsageEvent>().unwrap();
assert_eq!(downcasted.provider, evt.provider);
assert_eq!(downcasted.run_id, evt.run_id);
}
#[test]
fn modality_serializes() {
let json = serde_json::to_value(Modality::ImageGen).unwrap();
assert_eq!(json, serde_json::json!("ImageGen"));
let json = serde_json::to_value(Modality::Custom("foo".into())).unwrap();
assert_eq!(json, serde_json::json!({"Custom": "foo"}));
}
}