TSNLWeb / src /nlw_codex /usage.py
rvguha's picture
Deploy tsnlweb
4d991af verified
Raw History Blame Contribute Delete
4.28 kB
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
def _mapping(value: Any) -> dict[str, Any]:
if value is None:
return {}
if isinstance(value, dict):
return value
if hasattr(value, "model_dump"):
return value.model_dump()
return {
name: getattr(value, name)
for name in (
"prompt_tokens",
"completion_tokens",
"total_tokens",
"input_tokens",
"output_tokens",
"cost",
"prompt_tokens_details",
"completion_tokens_details",
"input_tokens_details",
"output_tokens_details",
)
if hasattr(value, name)
}
def _integer(value: Any) -> int:
try:
return int(value or 0)
except (TypeError, ValueError):
return 0
def _number(value: Any) -> float | None:
try:
return None if value is None else float(value)
except (TypeError, ValueError):
return None
@dataclass(slots=True)
class ModelUsage:
model: str
calls: int = 0
prompt_tokens: int = 0
completion_tokens: int = 0
total_tokens: int = 0
reasoning_tokens: int = 0
cached_tokens: int = 0
cost: float = 0.0
unpriced_calls: int = 0
phases: dict[str, int] = field(default_factory=dict)
def wire(self) -> dict[str, Any]:
return {
"model": self.model,
"calls": self.calls,
"prompt_tokens": self.prompt_tokens,
"completion_tokens": self.completion_tokens,
"total_tokens": self.total_tokens,
"reasoning_tokens": self.reasoning_tokens,
"cached_tokens": self.cached_tokens,
"cost": round(self.cost, 12),
"unpriced_calls": self.unpriced_calls,
"phases": dict(sorted(self.phases.items())),
}
@dataclass(slots=True)
class UsageLedger:
query_id: str
by_model: dict[str, ModelUsage] = field(default_factory=dict)
def record(self, model: str, phase: str, usage: Any) -> None:
data = _mapping(usage)
prompt = _integer(data.get("prompt_tokens", data.get("input_tokens")))
completion = _integer(data.get("completion_tokens", data.get("output_tokens")))
total = _integer(data.get("total_tokens")) or prompt + completion
prompt_details = _mapping(
data.get("prompt_tokens_details", data.get("input_tokens_details"))
)
completion_details = _mapping(
data.get("completion_tokens_details", data.get("output_tokens_details"))
)
cached = _integer(prompt_details.get("cached_tokens"))
reasoning = _integer(completion_details.get("reasoning_tokens"))
if not reasoning:
reasoning = _integer(data.get("reasoning_tokens"))
cost = _number(data.get("cost"))
item = self.by_model.setdefault(model, ModelUsage(model))
item.calls += 1
item.prompt_tokens += prompt
item.completion_tokens += completion
item.total_tokens += total
item.cached_tokens += cached
item.reasoning_tokens += reasoning
item.phases[phase] = item.phases.get(phase, 0) + 1
if cost is None:
item.unpriced_calls += 1
else:
item.cost += cost
def wire(self) -> dict[str, Any]:
models = [item.wire() for item in sorted(self.by_model.values(), key=lambda x: x.model)]
return {
"query_id": self.query_id,
"models": models,
"calls": sum(item.calls for item in self.by_model.values()),
"prompt_tokens": sum(item.prompt_tokens for item in self.by_model.values()),
"completion_tokens": sum(item.completion_tokens for item in self.by_model.values()),
"total_tokens": sum(item.total_tokens for item in self.by_model.values()),
"reasoning_tokens": sum(item.reasoning_tokens for item in self.by_model.values()),
"cached_tokens": sum(item.cached_tokens for item in self.by_model.values()),
"cost": round(sum(item.cost for item in self.by_model.values()), 12),
"unpriced_calls": sum(item.unpriced_calls for item in self.by_model.values()),
}