feat(config,runtime): 上下文预算按模型窗口推导 + usage/缓存可观测 + sidecar 存全文(T1)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 窗口声明: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)。
This commit is contained in:
AskaEth committed 2026-10-04 17:57:08 +08:00
1 parent 5b2b385524
commit 72b95df22b
39 files changed
+2496 -377

No files matched your search

+34 -8
View File
@@ -88,11 +88,15 @@ func TestExtract_TSVPrependsSummary(t *testing.T) {
}
}
// TestExtract_TruncatesHonestly 钉住「绝不静默截断」(design/24 §4.3):超预算时
// 截到预算、置 Truncated,且截断是 rune 安全的(多字节字符不被切成两半)。
func TestExtract_TruncatesHonestly(t *testing.T) {
// TestExtract_TruncatesAtSafetyCap 钉住「绝不静默截断」(design/24 §4.3):
// 撞上**安全上限**(MaxSidecarBytes,8 MiB 文本)时截到上限并置 Truncated,且截断
// 是 rune 安全的(多字节字符不被切成两半)。
//
// 注意它钉的不再是「注入预算」:注入预算由装配期按上下文预算决定(切片 T1 §7.2),
// 抽取器只保证 sidecar 里存得下全文、且撞上上限时如实记账。
func TestExtract_TruncatesAtSafetyCap(t *testing.T) {
t.Parallel()
body := strings.Repeat("你好", MaxExtractChars)
body := strings.Repeat("你好", MaxSidecarBytes/len("你好")+16)
res, err := Extract(txtName, "text/plain", []byte(body))
if err != nil {
t.Fatalf("Extract: %v", err)
@@ -100,15 +104,37 @@ func TestExtract_TruncatesHonestly(t *testing.T) {
if !res.Truncated {
t.Fatal("Truncated = false, want true")
}
if res.Chars != MaxExtractChars {
t.Fatalf("Chars = %d, want %d", res.Chars, MaxExtractChars)
if len(res.Text) > MaxSidecarBytes {
t.Fatalf("截断后正文 %d 字节,超出安全上限 %d", len(res.Text), MaxSidecarBytes)
}
if want := strings.Repeat("你好", MaxExtractChars/2); res.Text != want {
t.Fatalf("Text 不是预算内的 rune 前缀(len=%d)", len(res.Text))
if res.Chars != utf8.RuneCountInString(res.Text) {
t.Fatalf("Chars = %d, want 正文 rune 数 %d", res.Chars, utf8.RuneCountInString(res.Text))
}
if !utf8.ValidString(res.Text) {
t.Fatal("截断后的正文不是合法 UTF-8")
}
// 上限内的正文逐字无损:截断只砍尾巴,不改写已有内容。
if !strings.HasPrefix(body, res.Text) {
t.Fatal("截断后的正文不是原文的前缀")
}
}
// TestExtract_KeepsFullTextBelowSafetyCap 钉住切片 T1 §7.2 的核心:抽取期**不再**
// 按注入预算截断,因此一份远超旧 64K 字符、但仍在安全上限内的文档必须整份存下 ——
// 否则 document-read 永远读不到注入预算之外的正文(S4 实测的断裂)。
func TestExtract_KeepsFullTextBelowSafetyCap(t *testing.T) {
t.Parallel()
body := strings.Repeat("宽", 200*1024) // 20 万字符:远超旧的 64K 注入预算
res, err := Extract(txtName, "text/plain", []byte(body))
if err != nil {
t.Fatalf("Extract: %v", err)
}
if res.Truncated {
t.Fatal("Truncated = true, want false(安全上限内的正文不该被截断)")
}
if res.Chars != 200*1024 || res.Text != body {
t.Fatalf("Chars = %d(want %d),正文是否逐字无损 = %v", res.Chars, 200*1024, res.Text == body)
}
}
// TestExtract_SanitizesInvalidUTF8 钉住「非法 UTF-8 要处理,不可 panic」: