Files
AskaEth be38651ba4
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(llm): 用量落库 + 只读汇总端点 + 面板用量小卡(契约 v0.64)
- model 层新增可选 UsageSink(nil ⇒ 逐字不变,既有用例零改动)
- 组合根注入 UsageWriter:有界队列 1024 + 单写者 + 批量事务(128/批、2s 兜底),
  队列满丢弃 + 限流 WARN,绝不阻塞模型调用;sink 挂 RuntimeLLM(热重载不断)
- 迁移 0017:表 llm_usage(13 列,session_id 可空)+ idx_created_at + idx_session_id
- 新端点 GET /api/v1/admin/llm/usage(owner|admin 读;缺省近 24h、上限 90 天、
  超限 400 不静默裁剪;hitRate = cacheHit/(cacheHit+cacheMiss),分母 0 ⇒ 0;
  绝不含凭据与正文;未接线/查库失败 ⇒ 500 usage_unavailable)
- LLM_USAGE_RETENTION(默认 720h,≤0 = 关闭清理):启动清一次 + 每小时
- 面板 LLM 页「用量(近 24h)」小卡 + Docs 两份同步 v0.64
- 已知缺口(如实登记):session_id 恒 NULL,需 loop 侧在请求上打标,留列后接
2026-10-04 22:16:26 +08:00

47 lines
2.9 KiB
SQL
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 0017_llm_usage: 每次成功模型调用的用量落库(切片 U1,主人裁决「2.4 落」)。
--
-- 背景:T1 已在每次模型调用后记一条结构化日志(prompt / completion / total /
-- reasoning / cacheHit / cacheMiss / hitRate / tookMs),但**只落日志** ——
-- 看不见趋势、算不出按用途/模型的成本,面板也没有数据源。本表把同一条样本
-- 落进库里,供只读汇总端点(GET /api/v1/admin/llm/usage)聚合。
--
-- 口径:
-- * **无凭据、无正文**:本表只有 token 计数、模型名、用途标签与耗时 ——
-- apiKey / token / 消息正文一个字都不入库(写入方 model.UsageWriter 连
-- 参数都拿不到凭据)。
-- * cache_hit_tokens / cache_miss_tokens 是**输入侧两档**(提供商按命中/未命中
-- 两档计价,命中价约为未命中的 1/50),命中率 = hit / (hit + miss),分母为 0
-- 时按 0 处理(口径与 model.Response.CacheHitRate 同源)。
-- * session_id 可空。**当前尚无写入方**(恒为 NULL):模型适配器跑在 loop 之下、
-- 没有会话概念(见 internal/runtime/model/stream.go 的 StreamEvent.SessionID
-- 注释),把会话标识带到适配器边界需要 loop 侧在请求上打标 —— 那份文件本轮
-- 不在本切片改动范围内,故先留列、后接线。
-- * created_at 由 SQLite 以 UTC ISO-8601(毫秒)写入,与 0001 起的口径一致;
-- 汇总端点的时间窗按同一格式做**字典序**比较(定宽格式下等价于时间序)。
-- * 表名用复数,援引 AGENTS §10 已登记的例外(既有迁移口径一致)。
CREATE TABLE IF NOT EXISTS llm_usage (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT,
purpose TEXT NOT NULL DEFAULT '',
adapter TEXT NOT NULL DEFAULT '',
model TEXT NOT NULL DEFAULT '',
prompt_tokens INTEGER NOT NULL DEFAULT 0,
completion_tokens INTEGER NOT NULL DEFAULT 0,
cache_hit_tokens INTEGER NOT NULL DEFAULT 0,
cache_miss_tokens INTEGER NOT NULL DEFAULT 0,
reasoning_tokens INTEGER NOT NULL DEFAULT 0,
total_tokens INTEGER NOT NULL DEFAULT 0,
took_ms INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
);
-- 汇总端点与保留期清理都按时间窗扫表(`created_at >= ? AND created_at <= ?` /
-- `created_at < ?`),这条索引是两者的唯一依赖。
CREATE INDEX IF NOT EXISTS idx_llm_usage_created_at
ON llm_usage (created_at);
-- 按会话下钻(面板将来要「这个会话烧了多少」)。当前 session_id 恒为 NULL,
-- 索引仍然建好 —— 它不影响写入正确性,接线后立刻可用。
CREATE INDEX IF NOT EXISTS idx_llm_usage_session_id
ON llm_usage (session_id);