Files
IPSExtractor/services/extraction/domain/usage_tracking.py
T

91 lines
3.1 KiB
Python

"""Persistencia compacta del uso real de APIs LLM dentro del resultado de una tarea."""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
from domain.costs import MODEL_PRICING, compute_cost_from_usage
def _number(value: Any) -> float:
try:
return float(value or 0)
except (TypeError, ValueError):
return 0.0
def _tokens(usage: dict[str, Any], key: str) -> int:
try:
return int(usage.get(key) or 0)
except (TypeError, ValueError):
return 0
def record_api_usage(
result: dict[str, Any],
*,
phase: str,
model: str,
usage: dict[str, Any] | None,
radicado: str | None = None,
filename: str | None = None,
) -> dict[str, Any]:
"""Agrega una llamada facturable a ``result.api_usage`` y devuelve una copia.
El proveedor es la fuente de los tokens. El costo se calcula con la tabla de
precios configurada para el modelo y se conserva separado por fase y llamada.
"""
if not isinstance(usage, dict):
return dict(result)
next_result = dict(result)
ledger = dict(next_result.get("api_usage") or {})
summary = dict(ledger.get("summary") or {})
phases = dict(ledger.get("phases") or {})
phase_summary = dict(phases.get(phase) or {})
details = list(ledger.get("details") or [])
prompt_tokens = _tokens(usage, "prompt_tokens")
completion_tokens = _tokens(usage, "completion_tokens")
token_details = usage.get("prompt_tokens_details") or {}
cached_tokens = _tokens(token_details, "cached_tokens") if isinstance(token_details, dict) else 0
total_tokens = _tokens(usage, "total_tokens") or prompt_tokens + completion_tokens
cost = compute_cost_from_usage(usage, phase=phase, model=model) or {}
values: dict[str, int | float] = {
"requests": 1,
"prompt_tokens": prompt_tokens,
"cached_tokens": cached_tokens,
"completion_tokens": completion_tokens,
"total_tokens": total_tokens,
"input_usd": _number(cost.get("input_usd")),
"cached_input_usd": _number(cost.get("cached_input_usd")),
"output_usd": _number(cost.get("output_usd")),
"total_cost_usd": _number(cost.get("total_usd")),
"unpriced_requests": 0 if model in MODEL_PRICING else 1,
}
for key, value in values.items():
precision = 6 if key.endswith("usd") else None
summary[key] = round(_number(summary.get(key)) + value, precision) if precision else int(_number(summary.get(key)) + value)
phase_summary[key] = round(_number(phase_summary.get(key)) + value, precision) if precision else int(_number(phase_summary.get(key)) + value)
phase_summary["model"] = model
phases[phase] = phase_summary
details.append({
"timestamp": datetime.now(timezone.utc).isoformat(),
"phase": phase,
"model": model,
"radicado": radicado,
"filename": filename,
"prompt_tokens": prompt_tokens,
"cached_tokens": cached_tokens,
"completion_tokens": completion_tokens,
"total_tokens": total_tokens,
"cost": cost,
"pricing_known": model in MODEL_PRICING,
})
ledger.update({"summary": summary, "phases": phases, "details": details})
next_result["api_usage"] = ledger
return next_result