feat(llm): 用量落库 + 只读汇总端点 + 面板用量小卡(契约 v0.64)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 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:
AskaEth committed 2026-10-04 22:16:26 +08:00
1 parent 18381f65fd
commit be38651ba4
32 files changed
+2233 -44

No files matched your search

+10 -1
View File
@@ -133,6 +133,9 @@ type AnthropicChatAdapter struct {
model string
client *http.Client
recorder Recorder
// usageSink 是可选的用量落库出口(切片 U1)。为 nil 时只记结构化日志,
// 行为逐字等于引入落库之前。
usageSink UsageSink
// log 上报推理调整与被忽略的指令。它永不为 nil:
// 调用方未提供时,会装入一个带有本适配器来源标记的丢弃式日志器,
// 因此提供商的每个决定都可追溯。
@@ -153,6 +156,12 @@ func WithAnthropicRecorder(r Recorder) AnthropicOption {
return func(a *AnthropicChatAdapter) { a.recorder = r }
}
// WithAnthropicUsageSink 装入用量落库出口(切片 U1)。nil 会被忽略,从而保留
// 「只记日志」的默认行为;实现方必须非阻塞(见 UsageSink)。
func WithAnthropicUsageSink(s UsageSink) AnthropicOption {
return func(a *AnthropicChatAdapter) { a.usageSink = s }
}
// WithAnthropicModel 覆盖适配器的默认模型名。
func WithAnthropicModel(m string) AnthropicOption {
return func(a *AnthropicChatAdapter) { a.model = m }
@@ -403,7 +412,7 @@ func (a *AnthropicChatAdapter) upstreamError(resp *http.Response) error {
// record 上报一次成功调用的用量:先记一条结构化用量日志(含缓存命中率,
// 所有调用点都覆盖 —— 见 usage.go),再喂给可选的 CallRecord 计账出口。
func (a *AnthropicChatAdapter) record(req Request, start time.Time, resp Response) {
logUsage(a.log, a.name, req, resp, time.Since(start))
logUsage(a.log, a.usageSink, a.name, req, resp, time.Since(start))
if a.recorder == nil {
return
}