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 侧在请求上打标,留列后接
This commit is contained in:
1 parent
18381f65fd
commit
be38651ba4
32 files changed
+2233
-44
No files matched your search
@@ -0,0 +1,46 @@
|
||||
-- 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);
|
||||
Reference in new issue
Block a user