package model import ( "log/slog" "time" ) // 本文件是**用量与缓存的可观测出口**(design/04 附录 A.3 第 4 条)。 // // 背景(附录 A.2 第 3 条):在此之前输入侧用量从未被记录 —— 全仓只消费了 // PromptTokens / TotalTokens,`prompt_cache_hit_tokens` / `miss` 一律没解析、没落 // 日志,于是「大上下文到底贵不贵」根本无法实测。而提供商按命中/未命中两档计价, // 命中价约为未命中的 1/50:看不见命中率,就等于看不见成本。 // // 记录点选在**适配器内部**(每个适配器的 record 都调它一次),而不是循环里: // 这样流式、非流式、后台思考、子代理、管理面板试连等**所有**模型调用点都覆盖到, // 不会有「换个调用入口就没了成本日志」的缺口。 // // 红线:日志里只出现模型名、用途、token 数与耗时 —— **凭据(apiKey / token)绝不 // 出现在这里**,它连参数都进不来。 // usageLogEvent 是每条用量日志的事件名,便于按事件过滤与统计。 const usageLogEvent = "model usage" // logUsage 记一条结构化用量日志:prompt / completion / total / reasoning / // cacheHit / cacheMiss / hitRate,外加模型名、用途、适配器与耗时。 // // log 可以为 nil(适配器在未接日志时用的是丢弃 logger),这里仍做一次防御: // 缺字段容错、绝不 panic。 func logUsage(log *slog.Logger, adapter string, req Request, resp Response, took time.Duration) { if log == nil { return } log.Info(usageLogEvent, "adapter", adapter, "model", resp.Model, "purpose", req.Purpose, "prompt", resp.PromptTokens, "completion", resp.CompletionTokens, "total", resp.TotalTokens, "reasoning", resp.ReasoningTokens, "cacheHit", resp.CacheHitTokens, "cacheMiss", resp.CacheMissTokens, "hitRate", resp.CacheHitRate(), "tookMs", took.Milliseconds(), ) }