use std::sync::Arc;
use serde_json::{Map, Value};
use crate::chat::UsageRecorder;
use crate::config::Config;
use crate::cost::{Cost, Tier};
use crate::embedding::Embedding;
use crate::error::Error;
use crate::image::Image;
use crate::message::{Message, Operation, UsageEntry, UsageStatus};
use crate::model::Model;
use crate::moderation::Moderation;
use crate::rerank::Rerank;
use crate::speech::Speech;
use crate::tokens::Tokens;
use crate::transcription::Transcription;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UsageOwner {
Record { owner_type: String, owner_id: i64 },
Other(String),
}
impl UsageOwner {
pub fn record(owner_type: impl Into<String>, owner_id: i64) -> UsageOwner {
UsageOwner::Record {
owner_type: owner_type.into(),
owner_id,
}
}
pub fn to_value(&self) -> Value {
match self {
UsageOwner::Record {
owner_type,
owner_id,
} => serde_json::json!({ "type": owner_type, "id": owner_id }),
UsageOwner::Other(s) => Value::String(s.clone()),
}
}
}
impl From<&str> for UsageOwner {
fn from(s: &str) -> Self {
UsageOwner::Other(s.to_string())
}
}
impl From<String> for UsageOwner {
fn from(s: String) -> Self {
UsageOwner::Other(s)
}
}
tokio::task_local! {
static OWNER: Option<UsageOwner>;
}
pub fn usage_owner() -> Option<UsageOwner> {
OWNER.try_with(Clone::clone).ok().flatten()
}
pub async fn with_usage_owner<F: std::future::Future>(
owner: impl Into<Option<UsageOwner>>,
future: F,
) -> F::Output {
OWNER.scope(owner.into(), future).await
}
pub fn with_current_owner<F: std::future::Future>(
future: F,
) -> impl std::future::Future<Output = F::Output> {
OWNER.scope(usage_owner(), future)
}
pub(crate) async fn owned_by<F: std::future::Future>(
owner: Option<UsageOwner>,
future: F,
) -> F::Output {
match owner {
Some(owner) => OWNER.scope(Some(owner), future).await,
None => future.await,
}
}
#[async_trait::async_trait]
pub trait UsageLedger: Send + Sync {
async fn record(&self, entry: &UsageEntry);
}
pub(crate) async fn record(config: &Config, entry: &UsageEntry) {
if let Some(ledger) = &config.usage_ledger {
ledger.record(entry).await;
}
}
pub(crate) async fn report(config: &Arc<Config>, entries: &[UsageEntry]) {
for entry in entries {
record(config, entry).await;
crate::instrumentation::usage(config, entry);
}
}
impl UsageEntry {
pub fn new(
operation: Operation,
provider: impl Into<String>,
model: Option<&str>,
) -> UsageEntry {
let tokens = Tokens::default();
UsageEntry {
id: UsageEntry::next_id(),
operation,
provider: provider.into(),
model: model.unwrap_or_default().to_string(),
status: UsageStatus::Pending,
cost: Cost::new(&tokens, None, Tier::Standard),
tokens,
owner: usage_owner(),
}
}
pub fn is_pending(&self) -> bool {
self.status == UsageStatus::Pending
}
pub fn is_succeeded(&self) -> bool {
self.status == UsageStatus::Succeeded
}
pub fn is_failed(&self) -> bool {
self.status == UsageStatus::Failed
}
pub fn is_cancelled(&self) -> bool {
self.status == UsageStatus::Cancelled
}
pub fn usage_available(&self) -> bool {
!self.tokens.is_empty()
}
pub fn to_h(&self) -> Value {
let mut h = Map::new();
h.insert("operation".into(), self.operation.as_str().into());
h.insert("provider".into(), self.provider.clone().into());
h.insert(
"model".into(),
if self.model.is_empty() {
Value::Null
} else {
self.model.clone().into()
},
);
h.insert("status".into(), self.status.as_str().into());
h.insert(
"tokens".into(),
crate::instrumentation::tokens_h(&self.tokens),
);
h.insert("cost".into(), crate::instrumentation::cost_h(&self.cost));
Value::Object(h)
}
}
pub trait UsageResult {
fn set_usage_entries(&mut self, entries: Vec<UsageEntry>);
fn usage_tokens(&self) -> Tokens;
fn usage_cost(&self) -> Cost;
fn assign_model_info(&mut self, _resolve: &dyn Fn(Option<&str>) -> Option<Model>) {}
}
fn model_id(model: &str) -> Option<&str> {
(!model.is_empty()).then_some(model)
}
impl UsageResult for Message {
fn set_usage_entries(&mut self, entries: Vec<UsageEntry>) {
self.usage_entries = entries;
}
fn usage_tokens(&self) -> Tokens {
self.tokens()
}
fn usage_cost(&self) -> Cost {
self.cost(None)
}
fn assign_model_info(&mut self, resolve: &dyn Fn(Option<&str>) -> Option<Model>) {
self.model_info = resolve(self.model.as_deref());
}
}
impl UsageResult for Transcription {
fn set_usage_entries(&mut self, entries: Vec<UsageEntry>) {
self.usage_entries = entries;
}
fn usage_tokens(&self) -> Tokens {
self.tokens()
}
fn usage_cost(&self) -> Cost {
self.cost()
}
fn assign_model_info(&mut self, resolve: &dyn Fn(Option<&str>) -> Option<Model>) {
self.model_info = resolve(model_id(&self.model));
}
}
impl UsageResult for Embedding {
fn set_usage_entries(&mut self, entries: Vec<UsageEntry>) {
self.usage_entries = entries;
}
fn usage_tokens(&self) -> Tokens {
self.tokens()
}
fn usage_cost(&self) -> Cost {
self.cost()
}
fn assign_model_info(&mut self, resolve: &dyn Fn(Option<&str>) -> Option<Model>) {
self.model_info = resolve(model_id(&self.model));
}
}
impl UsageResult for Speech {
fn set_usage_entries(&mut self, entries: Vec<UsageEntry>) {
self.usage_entries = entries;
}
fn usage_tokens(&self) -> Tokens {
self.tokens()
}
fn usage_cost(&self) -> Cost {
self.cost()
}
fn assign_model_info(&mut self, resolve: &dyn Fn(Option<&str>) -> Option<Model>) {
self.model_info = resolve(model_id(&self.model));
}
}
impl UsageResult for Rerank {
fn set_usage_entries(&mut self, entries: Vec<UsageEntry>) {
self.usage_entries = entries;
}
fn usage_tokens(&self) -> Tokens {
self.tokens()
}
fn usage_cost(&self) -> Cost {
self.cost()
}
fn assign_model_info(&mut self, resolve: &dyn Fn(Option<&str>) -> Option<Model>) {
self.model_info = resolve(model_id(&self.model));
}
}
impl UsageResult for Image {
fn set_usage_entries(&mut self, entries: Vec<UsageEntry>) {
self.usage_entries = entries;
}
fn usage_tokens(&self) -> Tokens {
self.tokens()
}
fn usage_cost(&self) -> Cost {
self.cost()
}
fn assign_model_info(&mut self, resolve: &dyn Fn(Option<&str>) -> Option<Model>) {
self.model_info = resolve(model_id(&self.model));
}
}
impl UsageResult for Vec<Image> {
fn set_usage_entries(&mut self, entries: Vec<UsageEntry>) {
if let Some(first) = self.first_mut() {
first.set_usage_entries(entries);
}
}
fn usage_tokens(&self) -> Tokens {
self.first().map(Image::tokens).unwrap_or_default()
}
fn usage_cost(&self) -> Cost {
self.first()
.map(Image::cost)
.unwrap_or_else(|| Cost::new(&Tokens::default(), None, Tier::Standard))
}
fn assign_model_info(&mut self, resolve: &dyn Fn(Option<&str>) -> Option<Model>) {
for image in self {
image.assign_model_info(resolve);
}
}
}
impl UsageResult for Moderation {
fn set_usage_entries(&mut self, entries: Vec<UsageEntry>) {
self.usage_entries = entries;
}
fn usage_tokens(&self) -> Tokens {
self.tokens()
}
fn usage_cost(&self) -> Cost {
self.cost()
}
}
pub struct Tracker {
operation: Operation,
provider: String,
model_info: Option<Model>,
config: Arc<Config>,
on_finish: Option<UsageRecorder>,
entries: Vec<UsageEntry>,
pending: Vec<u64>,
}
impl Tracker {
pub fn new(
operation: Operation,
provider: impl Into<String>,
model: Option<Model>,
config: Arc<Config>,
on_finish: Option<UsageRecorder>,
) -> Tracker {
Tracker {
operation,
provider: provider.into(),
model_info: model,
config,
on_finish,
entries: Vec::new(),
pending: Vec::new(),
}
}
pub fn entries(&self) -> &[UsageEntry] {
&self.entries
}
pub fn entry(&self, id: u64) -> Option<&UsageEntry> {
self.entries.iter().find(|e| e.id == id)
}
fn entry_mut(&mut self, id: u64) -> Option<&mut UsageEntry> {
self.entries.iter_mut().find(|e| e.id == id)
}
pub fn start(&mut self) -> u64 {
let entry = UsageEntry::new(
self.operation,
self.provider.clone(),
self.model_info.as_ref().map(|m| m.id.as_str()),
);
let id = entry.id;
self.entries.push(entry);
self.pending.push(id);
id
}
pub fn observe(&mut self, chunk: &Message) {
self.observe_tokens(&chunk.tokens());
}
pub fn observe_tokens(&mut self, tokens: &Tokens) {
let Some(&id) = self.pending.last() else {
return;
};
let Some(entry) = self.entry_mut(id) else {
return;
};
entry.tokens.merge_latest(tokens);
entry.cost = Cost::new(&entry.tokens, None, Tier::Standard);
}
pub fn fail_attempt(&mut self, entry: Option<u64>, error: &Error) {
let Some(id) = entry else { return };
let Some(entry) = self.entry(id).filter(|e| e.is_pending()) else {
return;
};
let status = if matches!(error, Error::Cancelled) {
UsageStatus::Cancelled
} else {
UsageStatus::Failed
};
let tokens = self.failure_tokens(entry, error);
self.finish(id, status, tokens, None);
}
pub fn fail_pending(&mut self, error: &Error) {
for id in self.pending.clone() {
self.fail_attempt(Some(id), error);
}
}
pub fn succeed<R: UsageResult>(&mut self, result: &mut R) {
result.assign_model_info(&|id| self.result_model(id));
let pending = self.pending.clone();
let Some((&last, earlier)) = pending.split_last() else {
result.set_usage_entries(self.entries.clone());
return;
};
let tokens = result.usage_tokens();
let cost = result.usage_cost();
for &id in earlier {
self.finish(id, UsageStatus::Succeeded, Tokens::default(), None);
}
self.finish(last, UsageStatus::Succeeded, tokens, Some(cost));
result.set_usage_entries(self.entries.clone());
}
fn result_model(&self, id: Option<&str>) -> Option<Model> {
let Some(id) = id else {
return self.model_info.clone();
};
if self.model_info.as_ref().is_some_and(|m| m.id == id) {
return self.model_info.clone();
}
crate::models::models()
.find(id, Some(&self.provider))
.ok()
.or_else(|| self.model_info.clone())
}
fn failure_tokens(&self, entry: &UsageEntry, error: &Error) -> Tokens {
if self.model_info.is_none() {
return entry.tokens.clone();
}
crate::chat::failure_tokens(error, entry.usage_available().then(|| entry.tokens.clone()))
}
fn finish(&mut self, id: u64, status: UsageStatus, tokens: Tokens, cost: Option<Cost>) {
let cost = cost
.filter(|c| c.total().is_some())
.unwrap_or_else(|| self.price(&tokens));
let Some(entry) = self.entries.iter_mut().find(|e| e.id == id) else {
return;
};
entry.status = status;
entry.tokens = tokens;
entry.cost = cost;
self.pending.retain(|&p| p != id);
let entry = entry.clone();
if let Some(on_finish) = &mut self.on_finish {
on_finish(&entry);
}
crate::instrumentation::usage(&self.config, &entry);
}
fn price(&self, tokens: &Tokens) -> Cost {
let model = self.model_info.as_ref();
match self.operation {
Operation::Chat
| Operation::Moderation
| Operation::Ocr
| Operation::Judgment
| Operation::Video
| Operation::Research => Cost::new(tokens, model, Tier::Standard),
Operation::Embedding | Operation::Rerank => {
crate::rerank::embeddings_cost(tokens, model)
}
Operation::Image => Cost::images(tokens, model, None),
Operation::Speech | Operation::Transcription => Cost::audio(tokens, model),
}
}
}