- 窗口声明:models.json 组级 contextWindow + env LLM_CONTEXT_WINDOW(per-group materialize 播种,0 不写盘、负数报错)。 - 预算三态:CONTEXT_MAX_TOKENS 未设置 ⇒ 按窗口推导;>0 显式覆盖;0|负 关闭压缩(不变)。推导 budget = min(窗口×比例, 窗口−输出保留),比例默认 2/3(CONTEXT_BUDGET_RATIO,1M ⇒ 699050 tokens ≈ 683Ki);输出保留 = max(32768, 该组 maxTokens) 且封顶窗口/2(防配置矛盾时退化成 1 token);窗口未声明 ⇒ 回落 131072 + 恰好一条 WARN。接线在组合根 wire_context.go(唯一决策点)。 - usage/缓存可观测(此前**完全不可观测**):Response 增 CacheHit/CacheMiss/Reasoning tokens + CacheHitRate;解析 OpenAI 兼容(prompt_cache_hit_tokens/prompt_cache_miss_tokens/cached_tokens/reasoning_tokens)与 anthropic(cache_read_input_tokens/cache_creation_input_tokens)两种形状;记录点在适配器 record(换入口不丢成本日志),日志无凭据;单次输入 ≥预算 90% 记 WARN(不阻断)。 - 根治清单「续读」假承诺:抽取期不再按注入预算截断(sidecar 存全文,只受 MaxSidecarBytes=8 MiB 安全上限,ExtractorVersion v1→v2 旧 sidecar 失效重抽);注入预算改在装配期生效 ⇒ ref.Chars/Truncated 是注入侧事实、游标真是继续读的起点;document-read 的 limit 上限改为独立具名常量。 - 顺手修越界:config.go 505 行(超 500)拆出 llm.go。 - 主会话独立验收:隔离 worktree(HEAD + 本切片 39 文件,排除在途 gateway)build/vet 绿、全树 59 包 0 FAIL、linux+darwin 交叉编译绿;变异独立复现(比例改 0.25 ⇒ 推导用例红 262144 vs 699050)。
48 lines
1.9 KiB
Go
48 lines
1.9 KiB
Go
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(),
|
||
)
|
||
}
|