Files
Agentic-OS/backend/app/services/llm_usage.py
nearxos 6185b9b85a Initial commit: Agentic OS troubleshooting platform
Self-hosted, Docker-based agentic troubleshooting platform: FastAPI backend +
LangGraph agent, Next.js UI, tiered LLM routing (local Ollama -> Gemini ->
DeepSeek -> OpenRouter), MCP server manager, encrypted device credentials,
RBAC, audit log, project-memory + Obsidian integrations, and editable
troubleshooting decision rules tuned for the GeneseasX vessel stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 22:11:07 +03:00

141 lines
4.9 KiB
Python

"""Track LLM calls per task: provider, model, tokens, and estimated cost."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
from langchain_core.messages import BaseMessage
from langchain_openai import ChatOpenAI
# USD per 1M tokens (input, output). Local/Ollama is always zero.
MODEL_PRICING: dict[str, tuple[float, float]] = {
"deepseek-chat": (0.14, 0.28),
"deepseek/deepseek-chat": (0.14, 0.28),
"qwen2.5:7b-instruct": (0.0, 0.0),
"qwen2.5-coder:14b": (0.0, 0.0),
# OpenRouter common fallbacks (approximate)
"anthropic/claude-3.5-sonnet": (3.0, 15.0),
"anthropic/claude-3.7-sonnet": (3.0, 15.0),
"openai/gpt-4o-mini": (0.15, 0.60),
"meta-llama/llama-3.1-70b-instruct": (0.52, 0.75),
}
@dataclass
class ModelInfo:
step: str
provider: str # local | api
backend: str # ollama | gemini | deepseek | openrouter
model: str
display: str
@dataclass
class LlmUsageTracker:
"""Collects LLM call records for one agent run."""
calls: list[dict[str, Any]] = field(default_factory=list)
async def invoke(
self,
step: str,
model: ChatOpenAI,
info: ModelInfo,
messages: list[BaseMessage],
) -> Any:
response = await model.ainvoke(messages)
usage = extract_usage(response, info)
usage["step"] = step
self.calls.append(usage)
return response
def summary(self) -> dict[str, Any]:
run_cost = round(sum(c.get("cost_usd", 0.0) for c in self.calls), 6)
return {
"llm_usage": self.calls,
"run_cost_usd": run_cost,
"total_input_tokens": sum(c.get("input_tokens", 0) for c in self.calls),
"total_output_tokens": sum(c.get("output_tokens", 0) for c in self.calls),
}
def extract_usage(response: Any, info: ModelInfo) -> dict[str, Any]:
meta = getattr(response, "response_metadata", None) or {}
token_usage = meta.get("token_usage") or {}
if not token_usage:
usage_meta = getattr(response, "usage_metadata", None)
if usage_meta:
token_usage = {
"prompt_tokens": usage_meta.get("input_tokens", 0),
"completion_tokens": usage_meta.get("output_tokens", 0),
"total_tokens": usage_meta.get("total_tokens", 0),
}
input_tokens = int(token_usage.get("prompt_tokens") or token_usage.get("input_tokens") or 0)
output_tokens = int(
token_usage.get("completion_tokens") or token_usage.get("output_tokens") or 0
)
model_name = meta.get("model_name") or info.model
cost = estimate_cost(model_name, info.backend, input_tokens, output_tokens)
return {
"provider": info.provider,
"backend": info.backend,
"model": model_name,
"display": info.display,
"input_tokens": input_tokens,
"output_tokens": output_tokens,
"total_tokens": input_tokens + output_tokens,
"cost_usd": cost,
}
def estimate_cost(model: str, backend: str, input_tokens: int, output_tokens: int) -> float:
if backend == "ollama":
return 0.0
from app.services.model_catalog import CATALOG, entry_for_model
key = model.removeprefix("openrouter/")
for e in CATALOG:
if e.backend == backend and (e.model == model or e.model == key):
in_price, out_price = e.input_per_m, e.output_per_m
return round((input_tokens * in_price + output_tokens * out_price) / 1_000_000, 6)
entry = entry_for_model(backend, model, "economy")
in_price, out_price = entry.input_per_m, entry.output_per_m
return round((input_tokens * in_price + output_tokens * out_price) / 1_000_000, 6)
def merge_run_history(
prior_runs: list[dict],
run_number: int,
run_report: dict,
llm_summary: dict,
) -> dict:
"""Build cumulative report with per-run and total cost."""
run_entry = {
"run_number": run_number,
"follow_up": run_report.get("follow_up"),
"summary": run_report.get("summary"),
"resolved": run_report.get("resolved"),
"devices_checked": run_report.get("devices_checked"),
"llm_usage": llm_summary.get("llm_usage", []),
"run_cost_usd": llm_summary.get("run_cost_usd", 0.0),
"generated_at": run_report.get("generated_at"),
}
all_runs = prior_runs + [run_entry]
total_cost = round(sum(r.get("run_cost_usd", 0.0) for r in all_runs), 6)
merged = {**run_report}
merged["run_number"] = run_number
merged["runs"] = all_runs
merged["llm_usage"] = llm_summary.get("llm_usage", [])
merged["run_cost_usd"] = llm_summary.get("run_cost_usd", 0.0)
merged["total_cost_usd"] = total_cost
merged["total_input_tokens"] = sum(
t.get("input_tokens", 0) for r in all_runs for t in r.get("llm_usage", [])
)
merged["total_output_tokens"] = sum(
t.get("output_tokens", 0) for r in all_runs for t in r.get("llm_usage", [])
)
return merged