69 lines
2.9 KiB
Python
69 lines
2.9 KiB
Python
"""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,
|
|
}
|