-- 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);