迁移到 ContextChef v4
v4 是压缩管道 v2 版本:压缩现在提前触发("腐烂前")、可以固定(pin)、可以校验、可以归档并可逆、可以后台运行,也可以整体委托给 provider 的服务端上下文管理(Anthropic compact_20260112、OpenAI /responses/compact)。它还带来了新的 openai-responses target adapter、面向 @tanstack/ai ^0.44 的 @context-chef/tanstack-ai 完全重写,以及结构清理:废弃的 namespace 包装器被删除,src/index.ts 变成纯 barrel(facade 移到 src/chef.ts —— 公共 API 表面除此之外没有变化)。
| 包 | v3 线 | v4 版本 |
|---|---|---|
@context-chef/core | 3.9.0 | 4.0.0 |
@context-chef/ai-sdk-middleware | 2.1.0 | 3.0.0 |
@context-chef/tanstack-ai | 0.6.0 | 1.0.0 |
破坏性变更
1. 移除的 API
废弃的 namespace 包装器被删除。底层函数本来就有直接导出;唯一要改的是 import。
| 移除 | 替代 |
|---|---|
TokenUtils | estimate、estimateObject |
XmlGenerator | objectToXml |
AdapterFactory | getAdapter、adapterRegistry |
JanitorConfig.onBudgetExceeded | JanitorConfig.onBeforeCompress |
// v3
import { TokenUtils, XmlGenerator, AdapterFactory } from '@context-chef/core';
TokenUtils.estimate(text);
XmlGenerator.objectToXml(obj);
AdapterFactory.getAdapter('anthropic');
// v4
import { estimate, objectToXml, getAdapter } from '@context-chef/core';
estimate(text);
objectToXml(obj);
getAdapter('anthropic');// v3
janitor: { onBudgetExceeded: (history, info) => firstPassCompact(history) }
// v4 — same signature, same semantics, new name
janitor: { onBeforeCompress: (history, info) => firstPassCompact(history) }2. 压缩在窗口 70% 处触发(triggerRatio 默认 0.7)
原因:模型质量早在硬窗口上限之前就开始退化,而且在 100% 处压缩容易引发死循环(Gemini CLI 正是因此把触发线从 0.7 移到 0.5)。v4 在 contextWindow * triggerRatio 处触发压缩,默认 0.7。preserveRatio 现在作用于这个有效预算,而不是原始窗口。
// v3 behavior: compress only when contextWindow itself is exceeded
janitor: { contextWindow: 200_000 }
// v4: compression now fires at 140k tokens. To restore v3 behavior:
janitor: { contextWindow: 200_000, triggerRatio: 1 }如果你在 v3 中按原始窗口调过 preserveRatio,请重新检查 —— 它现在是 contextWindow * triggerRatio 的比例。
3. 压缩失败让历史保持不变
原因:v3 在压缩模型失败时用占位符截断历史,静默摧毁上下文。v4 中模型失败、缩减闸门触发(见下一条)、或 validateCompression 拒绝,都会原样返回历史并使熔断计数 +1。失败也不再设置压缩后抑制标志(E10)—— 该标志现在只在成功压缩后抑制重新检查。
// v3: compressionModel throws → history replaced with a truncation placeholder
// v4: compressionModel throws → history returned as-is, breaker++ (3 strikes → compress() no-ops)如果你依赖有损兜底把 payload 压在窗口内,请在 onBeforeCompress / compact() 里加自己的第一道兜底。
4. 缩减闸门:minShrinkRatio 默认 0.5
原因:会复读输入的摘要器过去会"成功"并永远循环。v4 中,对 ≥ 2000 字符的被压缩片段,摘要必须让它缩小至少 50%,否则压缩按失败处理(历史不变,熔断 +1)。
// A summarizer that returns near-verbatim output now trips the breaker.
// To disable the guard:
janitor: { contextWindow: 200_000, minShrinkRatio: 0 }5. withGuardrails 延迟应用、替换语义、独立消息
原因:v3 中护栏被立即应用并合并进动态状态消息 —— 在 withGuardrails() 之后调用 setDynamicState() 会静默丢弃护栏。v4 中 options 被存储、在 compile() 期间应用:
- 顺序无关 ——
withGuardrails在setDynamicState之前还是之后调用,输出相同。 - 替换语义 —— 每次调用替换上一次的 options(不累积)。
withGuardrails(null)清除存储的 options。- 独立尾部消息 —— 护栏作为独立消息落在三明治末端,不再合并进动态状态消息。payload 会出现字节级差异,属于预期。
- 持久化 ——
ChefSnapshot增加guardrailOptions;快照/恢复完整往返。
// v3 — order mattered, this silently dropped the guardrail:
chef.withGuardrails({ enforceXML: { outputTag: 'answer' } });
chef.setDynamicState(state); // guardrail gone
// v4 — same code works in any order; to remove a guardrail, be explicit:
chef.withGuardrails(null);6. 事件 handler 错误隔离
原因:观察者永远不该打断编译。v3 中抛异常的事件 handler 会从 compile() 传播出去。v4 中错误被记录,compile() 继续。如果你(滥)用抛异常的 handler 做控制流闸门,把逻辑移到真正的钩子里(onBeforeCompile、validateCompression、transformContext)。
7. compile() 调用串行化
同一实例上的并发 compile() 现在排队(snapshot + serialize),不再交错执行、破坏共享状态(轮次计数、熔断器、signal 暂存)。输入在调用时快照 —— 两次排队的 compile 之间调 setHistory(),第一次看到旧历史、第二次看到新历史。被 reject 的 compile 不会污染队列。推荐模式仍然是每个并发调用方一个 chef;队列的目的是让意外共享变得安全,不是变快。
8. @context-chef/ai-sdk-middleware 3.0.0
服务端上下文管理会跳过 middleware 压缩。 当某次调用带
providerOptions.anthropic.contextManagement且 middleware 也配置了压缩,该次调用的 middleware 压缩会被跳过(一次性警告)—— 否则你和 Anthropic 会压缩同一段历史。逃生舱:tscreateContextMiddleware({ ..., allowDoubleCompression: true })继承 core v4 压缩行为,包括
triggerRatio0.7 默认值 —— middleware 压缩现在在contextWindow的 70% 处触发。传triggerRatio: 1恢复 v2 时机。
9. @context-chef/tanstack-ai 1.0.0(面向 @tanstack/ai ^0.44 重写)
旧的 ^0.10 上游 API 已不存在;包围绕 0.44 的 ChatMiddleware 重写,采用基于返回值的 onConfig(初始化 + 每次迭代触发;注入幂等,钩子永不向 host 抛异常)。
Peer dependency:
@tanstack/ai ^0.44.0。contextWindow变为可选 —— 但一旦配置compress、onCompress或onBeforeCompress就必填(否则抛出)。按
ctx.threadId做会话 key(conversationId是仍可用的废弃别名)。没有显式的sessionId选项;不传threadId的调用只有 run 内隔离。compact窗口语义对齐 AI SDKpruneMessages;reasoning支持'all' | 'before-last-message' | 'none',toolCalls接受带按工具粒度的数组形式。emptyMessages默认'remove'(v0.6 保留空消息)。clear: ['thinking']现在生效 —— v0.6 中它是带警告的 no-op。transformContext增加第三个ctx参数,systemPrompts拓宽为SystemPrompt[]:ts// v0.6 transformContext?: (messages, systemPrompts: string[]) => {...} // 1.0 transformContext?: (messages, systemPrompts: SystemPrompt[], ctx: ChatMiddlewareContext) => {...}压缩 adapter 不再限制摘要器的输出 token —— 0.44 移除了顶层
maxTokens。需要上限的话在自己的模型调用里限制。
v4 新特性
| 特性 | 配置 / API |
|---|---|
| 约束固定(固定消息原文穿过压缩,按轮次保护) | Message.pinned: true |
| 可逆压缩归档 + 召回工具 | janitor.archive: 'vfs' | CompressionArchiveConfig、getRecallToolDefinition()、chef.resolveRecall(uri) |
| 服务端上下文管理(Anthropic compaction,betas 自动推导) | contextManagement: { strategy: 'server' } |
| 压缩 prompt 里的领域指南 | janitor.compressionGuidelines: string[] |
| 增量 anchored 压缩(持久 anchor 文档) | janitor.compressionMode: 'incremental-anchored'、janitor.getAnchorDoc() |
| 后台(非阻塞)压缩 | janitor.compressionScheduling: 'background' |
| 摘要后校验闸门 | janitor.validateCompression(summary, { compressed, kept }) |
| 压缩前统一 tool-result 重写(卸载、PII 脱敏) | transformToolResult(content, { toolName, toolCallId }) |
| 细粒度事件 | compress:start、compress:end、offload:created、pruner:tool-blocked |
| OpenAI Responses API target(reasoning 条目逐字节保留) | compile({ target: 'openai-responses' })、fromOpenAIResponses() |
Gemini 3.x thought signatures(往返保留,穿过 compact(['thinking'])) | ToolCall.thoughtSignature(自动) |
| 跨 provider 以文本重放 thinking | new OpenAIAdapter({ preserveThinkingAsText: true }),GeminiAdapter 同 |
| Anthropic Tool Search 注解 | ToolDefinition.deferLoading: true |
新的 compact() target | clear: ['reasoning-tags'];{ target: 'tool-result', toolFilter, exemptTools } |
| 每次 compile 只扫一次 memory store(原来 4 次) | Memory.compileArtifacts()(公开) |
迁移清单
- 升级包版本:core
^4.0.0、ai-sdk-middleware^3.0.0、tanstack-ai^1.0.0(如适用再加@tanstack/ai ^0.44)。 - 替换被移除的 import:
TokenUtils→estimate/estimateObject,XmlGenerator→objectToXml,AdapterFactory→getAdapter/adapterRegistry。 - 重命名
janitor.onBudgetExceeded→onBeforeCompress(签名不变)。 - 决定压缩时机:保留新的 0.7 腐烂前触发,或设
triggerRatio: 1恢复 v3 时机。重新检查调过的preserveRatio。 - 如果你依赖压缩模型失败时的占位符截断,在
onBeforeCompress里加自己的兜底 —— v4 失败时历史保持不变。 - 如果你的摘要器在大片段上确实产出低缩减输出,调低或关闭
minShrinkRatio。 - 审计
withGuardrails用法:移除顺序 workaround,在依赖"被遗忘"行为的地方显式加withGuardrails(null)。payload diff 中护栏会以独立尾部消息出现。 - 把控制流逻辑移出事件 handler(它们不再让
compile()失败)。 - ai-sdk-middleware:如果你有意组合客户端压缩与 Anthropic 服务端上下文管理,设
allowDoubleCompression: true;否则删掉你自己的去重防护。 - tanstack-ai:升级
@tanstack/ai到 ^0.44,为跨 run 会话连续性传threadId,凡配置压缩处补上contextWindow,更新transformContext签名,并重新检查依赖emptyMessages: 'keep'或clear: ['thinking']no-op 的流程。 - 然后在划算的地方采用新特性 —— 固定你的策略消息(
pinned: true),考虑archive: 'vfs'+ 召回工具,并在 Anthropic 上评估contextManagement: { strategy: 'server' }。