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
This commit is contained in:
1 parent
6b79043d82
commit
def7649baa
12 files changed
+892
-370
No files matched your search
@@ -4,6 +4,8 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Autumn-27/artex/db"
|
||||
)
|
||||
|
||||
// pgListCommands returns tool executions (any tool) from the activity table.
|
||||
@@ -45,6 +47,53 @@ func (s *Server) pgListLLMRecords(w http.ResponseWriter, r *http.Request) {
|
||||
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) {
|
||||
|
||||
@@ -250,6 +250,9 @@ func NewManager(dir, proxyAddr string) (*Manager, error) {
|
||||
if err := pg.EnsureLLMRecordsTable(); err != nil {
|
||||
log.Printf("[llmrec] create table: %v", err)
|
||||
}
|
||||
if err := pg.EnsureLLMUsageTable(); err != nil {
|
||||
log.Printf("[llmusage] create table: %v", err)
|
||||
}
|
||||
m := &Manager{dir: dir, pg: pg, assets: pg.Assets(), tasks: map[string]*Task{}, interceptor: intercept.New(pg)}
|
||||
if proxyAddr != "" {
|
||||
tr, err := traffic.Open(filepath.Join(dir, "traffic"), proxyAddr)
|
||||
|
||||
@@ -690,6 +690,7 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("GET /api/exploration/tokens", s.tokenStats)
|
||||
mux.HandleFunc("GET /api/tokens/daily", s.tokenDailyStats)
|
||||
mux.HandleFunc("GET /api/tokens/conversations", s.conversationTokens)
|
||||
mux.HandleFunc("GET /api/tokens/usage", s.pgUsageStats) // 全局 llm_usage 聚合(仪表盘新版视图)
|
||||
|
||||
mux.HandleFunc("GET /api/audit", s.getAudit)
|
||||
mux.HandleFunc("POST /api/gc", s.gc)
|
||||
@@ -702,6 +703,7 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("GET /api/llm/records", s.pgListLLMRecords)
|
||||
mux.HandleFunc("DELETE /api/llm/records", s.pgDeleteLLMRecords)
|
||||
mux.HandleFunc("GET /api/llm/records/tasks", s.pgLLMTasks)
|
||||
mux.HandleFunc("GET /api/llm/records/by-model", s.pgTokenByModel) // 按模型聚合本任务 token 用量
|
||||
mux.HandleFunc("GET /api/llm/records/{id}", s.pgGetLLMRecord)
|
||||
mux.HandleFunc("GET /api/settings", s.getSettings)
|
||||
mux.HandleFunc("PUT /api/settings", s.putSettings)
|
||||
|
||||
Reference in new issue
Block a user