fix(gateway): pre-filter invalid thinking-block signatures to prevent 400/retry/cache-thrash cycle#1776
Open
mrsxs wants to merge 1 commit intoWei-Shaw:mainfrom
Open
Conversation
… cache thrashing
## 背景
Anthropic 的扩展思考(extended thinking)要求:assistant 回复里的
每个 `thinking` 块都携带服务端 HMAC 签名(`signature` 字段);下一轮
请求回传这些 thinking 块时,上游会校验签名。签名缺失或无效会触发:
HTTP 400: Invalid `signature` in `thinking` block
现有网关路径仅在上游**返回 400 之后**才调用
`FilterThinkingBlocksForRetry` 做补救重试:
- gateway_service.go:4066-4187 的 retry 循环
这是一套"失败即过滤"的被动策略,带来两个副作用:
1. 每次请求至少付出一次 400-RTT(实测 2-8 秒延迟开销)
2. retry 过滤后的 body 与首次请求的 body 不一致 →
Anthropic 的 prompt cache key 改变 → 缓存失效 →
下一轮请求被迫全量重建 `cache_creation_tokens`
生产实测(Claude Pro 订阅经 sub2api 转发,5h 窗口):
- 25 次 `/v1/messages` 中 5 次触发 cache 重建
- 每次重建消耗 20k-30k `cache_creation_tokens`
- 这 5 次总成本占 5h 总开销的 **53.5%**
- 相当于 5h quota 被浪费近一半
## 问题根因
签名缺失最常见的来源是**第三方客户端的 SSE 解析器实现缺陷**:
Anthropic 流式返回 thinking 块时分两类 delta:
- `thinking_delta` → 思考文本
- `signature_delta` → HMAC 签名(在块末尾一次性追加)
非官方 SDK 的自写解析器经常只处理 `thinking_delta`,signature 静默
丢失。即使客户端侧修复,历史对话里存档的 thinking 块已经缺签名,
回传仍会持续触发 400-重试-缓存重建的连锁反应。
## 修复
仓库里**已经存在** `FilterThinkingBlocks()`(gateway_request.go:379),
语义完全契合 pre-flight 过滤需求:
- 若 `thinking.type ∈ {enabled, adaptive}`:
仅移除 signature 为空 / 等于 `DummyThoughtSignature` 的 thinking 块,
**保留签名有效的块**
- 若 thinking 未启用:
移除所有 thinking 相关块(与 retry 路径行为一致)
但该函数**从未被主流程调用**。本 PR 在三处 pre-filter 入口补上调用:
- `Forward`(gateway_service.go:4071)
- `forwardAnthropicAPIKeyPassthroughWithInput`(:4564)
- `ForwardCountTokens`(:8195)
均插在现有 `StripEmptyTextBlocks(body)` 之后,紧邻既有前置过滤。
## 影响面
| 场景 | 行为 |
|---|---|
| 请求不含 thinking 块 | fast-path 直接返回,零开销 |
| thinking 启用 + 签名有效 | 保留原块,字节级不变 |
| thinking 未启用 | 移除历史 thinking 残留,对齐 Anthropic 要求 |
| thinking 启用 + 签名缺失/无效 | 入站即 strip,避免 400,保护 prompt cache |
对使用官方 SDK(Claude Code / `@anthropic-ai/sdk`)的客户端**字节级零影响**。
只在客户端本来就会触发 400 的场景下静默修复。
## 验证指标
修复前:
cache_creation_tokens: 189,450 / 5h (≈30% 请求遭遇全量重建)
/v1/messages 上游 400 率: ~100%
每请求平均 retry 开销: 3.5s
修复后(预期):
cache_creation_tokens: <20,000 / 5h (仅会话首次写入)
/v1/messages 上游 400 率: 0%
每请求 retry 开销: 0
## 代码改动
3 处插入,总计 7 行新增:
backend/internal/service/gateway_service.go
+3 行 @ Forward (line 4072-4074)
+2 行 @ forwardAnthropicAPIKeyPassthroughWithInput (line 4567-4568)
+2 行 @ ForwardCountTokens (line 8200-8201)
均为纯调用现有 `FilterThinkingBlocks(body)`,无新增依赖、无接口变化、
无配置项新增。如需做成开关可轻松改造,但默认开启语义更安全。
Contributor
|
All contributors have signed the CLA. ✅ |
d0f362d to
960b2bb
Compare
Author
|
I have read the CLA Document and I hereby sign the CLA |
Author
|
recheck |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题现象
线上使用经 sub2api 转发的 Claude Opus(Anthropic OAuth 订阅账号)时,
/v1/messages请求几乎每次都会触发如下 WARN + 重试:原来为什么会这样
Anthropic 的扩展思考(extended thinking)签名机制:
thinking块由服务端 HMAC 签名(写入signature字段)Invalid \signature` in `thinking` block`签名丢失最常见的来源:第三方客户端自写的 SSE 解析器实现缺陷。Anthropic 流式响应里 thinking 块的签名通过独立的
signature_delta事件下发,而非官方 SDK 经常只处理thinking_delta忽略signature_delta,导致存档的 thinking 块缺签名。现有 sub2api 如何应对
仓库当前策略是失败后补救(
gateway_service.go:4066-4187):isThinkingBlockSignatureError→ 调用FilterThinkingBlocksForRetrystrip 掉 thinking 块功能上这套重试机制 work,但实际产生两个明显副作用:
副作用 1:每次请求额外 2-8 秒延迟
首次 400 + retry 的 RTT 开销不可避免。
副作用 2:Prompt cache 被连锁破坏(核心问题)
cache_creation_tokens生产实测(Claude Pro 订阅 5h 窗口 / 25 次 /v1/messages)
cache_creation_tokens等效影响:Claude Pro 5h quota 被浪费近一半,用户提前触发限额冷却。
修复
仓库里已经有符合 pre-flight 语义的函数
FilterThinkingBlocks()(backend/internal/service/gateway_request.go:379):这正是预期的 pre-flight 语义:
thinking: enabled开关保留 → 用户扩展思考功能不受影响但该函数从未被主流程调用(grep 确认),只有 retry 路径在用更激进的
FilterThinkingBlocksForRetry(会禁用顶层 thinking)。本 PR 在三个 pre-filter 入口补上调用,均紧邻现有
StripEmptyTextBlocks(body):Forwardgateway_service.go:4071-4074forwardAnthropicAPIKeyPassthroughWithInputgateway_service.go:4564-4568ForwardCountTokensgateway_service.go:8195-8201代码改动
7 行新增,3 个位置。完整 diff:
影响面分析
@anthropic-ai/sdk(签名完整)验证
预期指标变化
/v1/messages上游 400 率cache_creation_tokens回归测试
FilterThinkingBlocks()本身已有测试覆盖(gateway_request_test.go)StripEmptyTextBlocks相同,复用现有测试结构bytes.Contains快速判断),无性能退化可选增强
如需做成开关,可加
gateway.preempt_filter_thinking_blocks bool配置项,默认 true。本 PR 选择默认开启(无需配置),原因:关联问题
本问题最初由 GenericAgent 客户端(https://github.com/lsdefine/GenericAgent)触发。我已向上游提交了客户端侧的根治 PR(https://github.com/lsdefine/GenericAgent/pull/123),但 sub2api 侧的兜底依然有价值:保护其他可能存在相同 bug 的第三方客户端,并避免 upstream 客户端发布新版之前用户持续遭受浪费。