91 lines
3.1 KiB
Python
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
|