Files
Cyrene_v2/cmd/cyrene/wire_context.go
T
AskaEth 72b95df22b
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(config,runtime): 上下文预算按模型窗口推导 + usage/缓存可观测 + sidecar 存全文(T1)
- 窗口声明: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)。
2026-10-04 17:57:08 +08:00

84 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"log/slog"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/loop"
)
// 本文件是**上下文预算的唯一决策点**(组合根):把「配置 + 生效模型组」摊平成
// config.ContextBudgetInput,喂给纯函数 config.DeriveContextBudget,再把结论交给
// loop.Options.MaxContextTokens 与 loop.Options.DocumentMaxInjectChars。
//
// 为什么推导放在这里而不是配置层:窗口与 maxTokens 都在 models.json 的**生效组**
// 上(面板可编辑、热重载),而 CONTEXT_MAX_TOKENS 在 .env。两个来源的会合点只有
// 组合根,推导函数本身因此保持纯粹、可表驱动测试。
//
// 语义(design/04 §1.3 + 附录 A.3):
// - CONTEXT_MAX_TOKENS 未设置 ⇒ 按窗口推导 budget = min(窗口 × 比例, 窗口 − 输出保留);
// - > 0 ⇒ 显式覆盖;0 / 负数 ⇒ 关闭压缩(与接线前一致);
// - 窗口未声明 ⇒ 回落 config.DefaultModelContextWindow 并记**恰好一条** WARN;
// - 比例非法(CONTEXT_BUDGET_RATIO)⇒ 记一条带原始取值的 WARN 并回落 2/3。
// ctxBudget 是组合根推导出的两项预算:循环的溢出压缩预算与单份附件注入预算。
type ctxBudget struct {
// Budget 是压缩预算的完整结论(含推导来源,供日志与断言)。
Budget config.ContextBudget
// DocumentInjectChars 是单份附件注入上下文的字符上限(T1 §7.2)。
DocumentInjectChars int
}
// contextBudget 计算本次运行生效的上下文预算。envPath 是启动时合并的 .env 路径
// (比例键的读取与其余配置同一套优先级;空串表示不看 .env)。
//
// 模型组取**生效组**(models.json 下标 0,或 .env 合成的组):压缩预算与注入预算
// 都是循环级的单一值,而生效组才是新请求默认用的那个。
func (a *app) contextBudget(envPath string) ctxBudget {
in := config.ContextBudgetInput{}
docOverride := 0
if a.cfg != nil {
in.Override = a.cfg.Context.MaxTokens
in.OverrideConfigured = a.cfg.Context.MaxTokensSet
docOverride = a.cfg.Context.DocumentMaxInjectChars
}
if a.llm != nil {
g := a.llm.Group()
in.Window = g.ContextWindow
in.GroupMaxTokens = g.MaxTokens
}
return deriveContextBudget(sourcedWiringLog(a.log), envPath, in, docOverride)
}
// deriveContextBudget 是 contextBudget 的可测内核:取值来源收成显式参数,
// 日志与「恰好一条 WARN」也在这里(测试用捕获型 handler 断言条数)。
func deriveContextBudget(log *slog.Logger, envPath string, in config.ContextBudgetInput, docOverride int) ctxBudget {
ratio, err := config.LoadContextBudgetRatio(envPath)
if err != nil {
// 非法比例:记一条带原始取值的 WARN 并回落默认(AGENTS §10:不许静默失效)。
log.Warn("context budget ratio invalid, falling back",
"env", config.ContextBudgetRatioEnv, "error", err,
"fallback_ratio", config.DefaultContextBudgetRatio)
ratio = config.DefaultContextBudgetRatio
}
in.Ratio = ratio
budget := config.DeriveContextBudget(in)
// 单份附件注入预算:显式覆盖优先,否则随推导出的预算走。
docChars := docOverride
if docChars <= 0 {
docChars = loop.DocumentInjectCharsFor(budget.Tokens)
}
if budget.Mode == config.ContextBudgetDerived && !budget.WindowDeclared {
// 窗口未声明:**恰好一条** WARN(每次启动只推导一次)。
log.Warn("model context window not declared, falling back",
"env", "LLM_CONTEXT_WINDOW", "json_key", "contextWindow",
"window", budget.Window, "hint", "声明窗口才能吃满模型的真实窗口")
}
log.Info("context budget",
"mode", string(budget.Mode), "tokens", budget.Tokens,
"window", budget.Window, "window_declared", budget.WindowDeclared,
"ratio", budget.Ratio, "output_reserve", budget.OutputReserve,
"document_inject_chars", docChars)
return ctxBudget{Budget: budget, DocumentInjectChars: docChars}
}