"""ORM model for per-LLM-call metrics.""" from __future__ import annotations from datetime import UTC, datetime from sqlalchemy import Float, Index, Integer, String, Text from sqlalchemy.orm import Mapped, mapped_column from deerflow.persistence.base import Base from deerflow.persistence.types import BeijingDateTime class LlmCallMetricRow(Base): """One row per completed (or failed) LLM provider invocation.""" __tablename__ = "llm_call_metrics" __table_args__ = ( Index("ix_llm_call_metrics_created_user", "created_at", "user_id"), Index("ix_llm_call_metrics_run_created", "run_id", "created_at"), Index("ix_llm_call_metrics_created_model_user", "created_at", "model_name", "user_id"), ) id: Mapped[str] = mapped_column(String(64), primary_key=True) # Wall-clock time the LLM call ended. Indexed because admin views always # sort/filter by recency. created_at: Mapped[datetime] = mapped_column(BeijingDateTime(), default=lambda: datetime.now(UTC), index=True) user_id: Mapped[str | None] = mapped_column(String(64), index=True) thread_id: Mapped[str | None] = mapped_column(String(64), index=True) run_id: Mapped[str | None] = mapped_column(String(64), index=True) agent_name: Mapped[str | None] = mapped_column(String(128), index=True) model_name: Mapped[str | None] = mapped_column(String(128), index=True) # Wall-clock duration of the provider invocation in milliseconds. Always set. duration_ms: Mapped[int] = mapped_column(Integer, default=0) input_tokens: Mapped[int | None] = mapped_column(Integer) output_tokens: Mapped[int | None] = mapped_column(Integer) total_tokens: Mapped[int | None] = mapped_column(Integer) # Output tokens / seconds; null when there is no completion (errors). tokens_per_sec: Mapped[float | None] = mapped_column(Float) # "success" or "error". Errors store the classification and original message # so admins can scan for transient vs auth/quota issues. status: Mapped[str] = mapped_column(String(16), default="success", index=True) error_type: Mapped[str | None] = mapped_column(String(64)) error_message: Mapped[str | None] = mapped_column(Text) def to_dict(self) -> dict: return { "id": self.id, "created_at": self.created_at.isoformat() if isinstance(self.created_at, datetime) else self.created_at, "user_id": self.user_id, "thread_id": self.thread_id, "run_id": self.run_id, "agent_name": self.agent_name, "model_name": self.model_name, "duration_ms": self.duration_ms, "input_tokens": self.input_tokens, "output_tokens": self.output_tokens, "total_tokens": self.total_tokens, "tokens_per_sec": self.tokens_per_sec, "status": self.status, "error_type": self.error_type, "error_message": self.error_message, }