feat(model): llm_usage.session_id 真正落库(补完 U1 缺口)
通道:新增 model/session_ctx.go 的 WithSessionID / SessionIDFrom(与 tool.WithTurnPersona
同族的 ctx 范式,避开 model 反向 import loop 的环),loop 在 Run 顶部播种;适配器
record(ctx,...) 接会话;usageSessionArg 空串映射为 NULL。
兼容:sink 未注入或会话未播种时行为逐字不变;既有 model 测试零断言改动即全绿。
文件:model/{session_ctx.go(新), usage.go, usage_store.go, openai.go, anthropic.go,
usage_session_test.go(新)} + loop/{run.go, usage_session_test.go(新)}。
变异三处:去 loop 打标 / 写侧不绑会话 / 适配器不读 ctx ⇒ 各自红行,还原复绿。
契约与日志字段集未动;读侧不按 session 过滤,面板汇总口径不变。
This commit is contained in:
1 parent
cb564fe981
commit
59a1ac4056
9 files changed
+359
-18
No files matched your search
@@ -1,6 +1,7 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
)
|
||||
@@ -36,6 +37,10 @@ const usageLogEvent = "model usage"
|
||||
type UsageSample struct {
|
||||
// Adapter 是服务该次调用的适配器/提供商分组标识。
|
||||
Adapter string
|
||||
// SessionID 是本次调用所属的会话(loop 经 ctx 播种,见 session_ctx.go)。
|
||||
// 空串表示**没有会话归属**(管理面板试连、未接循环的后台流程)——落库时
|
||||
// 写 NULL,绝不写 "",否则「无归属」与「属于某会话」在列里混成同一个值。
|
||||
SessionID string
|
||||
// Model 是产出该响应的模型名。
|
||||
Model string
|
||||
// Purpose 是调用用途标签(如 "chat" / "memory_extract" / "engage_judge")。
|
||||
@@ -64,10 +69,12 @@ type UsageSink interface {
|
||||
}
|
||||
|
||||
// usageSampleOf 把一次调用归一化成 UsageSample。它是日志与落库**唯一**的字段来源,
|
||||
// 两处口径因此不可能分叉。
|
||||
func usageSampleOf(adapter string, req Request, resp Response, took time.Duration) UsageSample {
|
||||
// 两处口径因此不可能分叉。会话标识从 ctx 解析(session_ctx.go):适配器没有会话
|
||||
// 概念,loop 是唯一把它播种进来的地方;未播种时为 "",落库写 NULL。
|
||||
func usageSampleOf(ctx context.Context, adapter string, req Request, resp Response, took time.Duration) UsageSample {
|
||||
return UsageSample{
|
||||
Adapter: adapter,
|
||||
SessionID: SessionIDFrom(ctx),
|
||||
Model: resp.Model,
|
||||
Purpose: req.Purpose,
|
||||
PromptTokens: resp.PromptTokens,
|
||||
@@ -86,9 +93,12 @@ func usageSampleOf(adapter string, req Request, resp Response, took time.Duratio
|
||||
//
|
||||
// log 可以为 nil(适配器在未接日志时用的是丢弃 logger),这里仍做一次防御:
|
||||
// 缺字段容错、绝不 panic。sink 为 nil 时**这一步是空操作** —— 逐字等于引入落库
|
||||
// 之前的行为。
|
||||
func logUsage(log *slog.Logger, sink UsageSink, adapter string, req Request, resp Response, took time.Duration) {
|
||||
sample := usageSampleOf(adapter, req, resp, took)
|
||||
// 之前的行为(日志字段集也不含会话:会话只进落库面,见 UsageSample.SessionID)。
|
||||
//
|
||||
// ctx 只为解析会话标识而来(session_ctx.go)—— 适配器的 record 拿不到会话,
|
||||
// 循环在发起调用时把它播种在 ctx 上。
|
||||
func logUsage(ctx context.Context, log *slog.Logger, sink UsageSink, adapter string, req Request, resp Response, took time.Duration) {
|
||||
sample := usageSampleOf(ctx, adapter, req, resp, took)
|
||||
if log != nil {
|
||||
log.Info(usageLogEvent,
|
||||
"adapter", sample.Adapter,
|
||||
|
||||
Reference in new issue
Block a user