Files
ARTEX/server/commands.go
T
Autumn-27 def7649baa feat(tokens): 新增 llm_usage 计量账本,按模型统计 token + 仪表盘新旧版切换
- llmrec: 拆分轻量计量与重量 body 录制——token+model+task+worker 永远写入
  新表 llm_usage(不含 body,不受 llm_record 开关控制),body 仍受开关控制
- 修复 token 累加 bug:usage 从覆盖改为 Add(Anthropic 把 input/output 拆到
  message_start/message_delta 两个事件,覆盖会把 input 冲成 0;OpenAI 不受影响)
- 任务总览新增「LLM Token 用量」卡片:按模型统计 + 缓存命中率(缓存读/输入)
- 仪表盘 Token 卡片加「新版/旧版」开关:新版走 llm_usage(逐次精确、含中断消耗、
  真实每日消耗),旧版沿用 activity 统计
- llm_usage 随任务删除保留(历史计量账本)

Claude-Session: https://claude.ai/code/session_014TfZWeVGhjXqZD3vcj44mz
2026-08-20 11:16:48 -04:00

138 lines
4.0 KiB
Go

package server
import (
"net/http"
"strconv"
"strings"
"github.com/Autumn-27/artex/db"
)
// pgListCommands returns tool executions (any tool) from the activity table.
func (s *Server) pgListCommands(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
page := atoiDefault(q.Get("page"), 0)
size := atoiDefault(q.Get("size"), 50)
keyword := q.Get("q")
var expID *int64
if tv := q.Get("task"); tv != "" {
if n, err := strconv.ParseInt(tv, 10, 64); err == nil {
expID = &n
}
}
records, total, err := s.m.PG().ListCommands(expID, keyword, page, size)
if err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, map[string]any{"commands": records, "total": total})
}
// pgListLLMRecords returns paginated LLM call records.
func (s *Server) pgListLLMRecords(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
page := atoiDefault(q.Get("page"), 0)
size := atoiDefault(q.Get("size"), 50)
model := q.Get("model")
session := q.Get("session")
task := q.Get("task")
records, total, err := s.m.PG().ListLLMRecords(model, session, task, page, size)
if err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, map[string]any{"records": records, "total": total})
}
// pgTokenByModel returns a task's LLM token usage grouped by model, from the
// always-on llm_usage metering ledger. Accurate even with per-agent model bindings,
// pool rotation/failover, and interrupted runs (every call is metered on success or
// error). No recording toggle required.
func (s *Server) pgTokenByModel(w http.ResponseWriter, r *http.Request) {
id := strings.TrimSpace(r.URL.Query().Get("task"))
if id == "" {
writeErr(w, 400, "missing task")
return
}
pg := s.m.PG()
if pg == nil {
writeJSON(w, 200, map[string]any{"models": []db.ModelTokenStat{}})
return
}
models, err := pg.TokenByModel(id)
if err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, map[string]any{"models": models})
}
// pgUsageStats returns global llm_usage aggregates for the dashboard's "new" token
// view: per-profile totals (all-time) + per-(profile, day) buckets for the daily
// chart. Sourced from the always-on metering ledger, so it is accurate across
// per-agent bindings, pool rotation, and interrupted runs.
func (s *Server) pgUsageStats(w http.ResponseWriter, r *http.Request) {
pg := s.m.PG()
if pg == nil {
writeJSON(w, 200, map[string]any{"by_profile": []db.ProfileUsage{}, "daily": []db.ProfileDayUsage{}})
return
}
days := atoiDefault(r.URL.Query().Get("days"), 365)
byProfile, err := pg.UsageByProfile()
if err != nil {
writeErr(w, 500, err.Error())
return
}
daily, err := pg.UsageDaily(days)
if err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, map[string]any{"by_profile": byProfile, "daily": daily})
}
// pgLLMTasks returns distinct recorded tasks with counts, for the page's task
// picker (pick a task → filter the list, then delete its conversations).
func (s *Server) pgLLMTasks(w http.ResponseWriter, r *http.Request) {
tasks, err := s.m.PG().LLMTasks()
if err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, map[string]any{"tasks": tasks})
}
// pgDeleteLLMRecords removes all LLM records for one exact task_id (the same
// match the task picker uses). Empty task → 400. Returns rows deleted.
func (s *Server) pgDeleteLLMRecords(w http.ResponseWriter, r *http.Request) {
task := strings.TrimSpace(r.URL.Query().Get("task"))
if task == "" {
writeErr(w, 400, "missing task")
return
}
n, err := s.m.PG().DeleteLLMRecords(task)
if err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, map[string]any{"deleted": n})
}
// pgGetLLMRecord returns a single LLM record with full request/response bodies.
func (s *Server) pgGetLLMRecord(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
writeErr(w, 400, "invalid id")
return
}
rec, err := s.m.PG().GetLLMRecord(id)
if err != nil {
writeErr(w, 404, "not found")
return
}
writeJSON(w, 200, rec)
}