fix(claude): 🐛 补齐 thinking block binding beta

Co-authored-by: Wine Fox <fox@ling.plus>
This commit is contained in:
Lynricsy
2026-09-06 00:34:05 +08:00
co-authored by Wine Fox
parent ab99d56e96
commit 7ae0312091
3 changed files with 39 additions and 5 deletions
+7 -5
View File
@@ -20,11 +20,12 @@ const (
BetaFastMode = "fast-mode-2026-02-01"
// 新增(对齐官方 CLI 2.1.9x 以来的流量)
BetaPromptCachingScope = "prompt-caching-scope-2026-01-05"
BetaEffort = "effort-2025-11-24"
BetaRedactThinking = "redact-thinking-2026-02-12"
BetaContextManagement = "context-management-2025-06-27"
BetaExtendedCacheTTL = "extended-cache-ttl-2025-04-11"
BetaPromptCachingScope = "prompt-caching-scope-2026-01-05"
BetaEffort = "effort-2025-11-24"
BetaRedactThinking = "redact-thinking-2026-02-12"
BetaContextManagement = "context-management-2025-06-27"
BetaThinkingBindingControls = "thinking-binding-controls-2026-08-01"
BetaExtendedCacheTTL = "extended-cache-ttl-2025-04-11"
// server-side refusal fallback beta 字段族(beta Messages API 专有)。
// 客户端(Claude Code / SDK / OpenCode 等)会默认透传 body.fallbacks /
@@ -99,6 +100,7 @@ func FullClaudeCodeMimicryBetas() []string {
BetaPromptCachingScope,
BetaEffort,
BetaContextManagement,
BetaThinkingBindingControls,
BetaExtendedCacheTTL,
}
}
@@ -114,6 +114,23 @@ func TestSanitizeAnthropicBodyForBetaTokens_EmptyBody(t *testing.T) {
require.Empty(t, out)
}
func TestSanitizeAnthropicBodyForBetaTokens_ThinkingBlockBindingKeptWhenBetaPresent(t *testing.T) {
body := []byte(`{"model":"claude-fable-5-1","thinking":{"type":"adaptive","display":"summarized","block_binding":{"prefix_mismatch_behavior":"drop_block"}},"messages":[]}`)
out, changed := sanitizeAnthropicBodyForBetaTokens(body, claude.BetaThinkingBindingControls)
require.False(t, changed)
require.Equal(t, "drop_block",
gjson.GetBytes(out, "thinking.block_binding.prefix_mismatch_behavior").String())
}
func TestSanitizeAnthropicBodyForBetaTokens_ThinkingBlockBindingStrippedWhenBetaMissing(t *testing.T) {
body := []byte(`{"model":"claude-fable-5-1","thinking":{"type":"adaptive","display":"summarized","block_binding":{"prefix_mismatch_behavior":"drop_block"}},"messages":[]}`)
out, changed := sanitizeAnthropicBodyForBetaTokens(body, claude.BetaContextManagement)
require.True(t, changed)
require.False(t, gjson.GetBytes(out, "thinking.block_binding").Exists())
require.Equal(t, "adaptive", gjson.GetBytes(out, "thinking.type").String())
require.Equal(t, "summarized", gjson.GetBytes(out, "thinking.display").String())
}
// ★ 关键回归断言:能力维度 sanitize 解决了 "真 CC + haiku" 路径的过度删除问题。
// 真实 Claude Code CLI 2.1.87+ 客户端 header 含 context-management beta;
// 即使 model 是 haiku,sanitize 也不应剥离功能字段。
@@ -145,6 +162,8 @@ func TestComputeFinalAnthropicBeta_OAuthMimic_NonHaiku_IncludesContextManagement
"OAuth mimic non-haiku 必须注入完整 CC mimicry beta,含 context-management-2025-06-27")
require.True(t, anthropicBetaTokensContains(final, claude.BetaOAuth))
require.True(t, anthropicBetaTokensContains(final, claude.BetaClaudeCode))
require.True(t, anthropicBetaTokensContains(final, claude.BetaThinkingBindingControls),
"OAuth mimic 必须注入 thinking block binding 所需的 beta")
}
func TestComputeFinalAnthropicBeta_OAuthMimic_Haiku_IncludesFullClaudeCodeBetas(t *testing.T) {
@@ -912,6 +912,12 @@ const anthropicBetaContextManagementToken = "context-management-2025-06-27"
// FullClaudeCodeMimicryBetas 覆盖客户端 beta(该列表不含 fallback beta),
// 若不 strip,body 字段与 header 不对称 → 所有模型 400
//
// thinking.block_binding 场景:
// - Claude Fable 5.1 的会话前缀绑定控制受
// `thinking-binding-controls-2026-08-01` beta 保护
// - 缺 token 时上游拒收:
// "thinking.adaptive.block_binding: Extra inputs are not permitted"
//
// 本函数按最终发送的 anthropic-beta header 决定是否保留 body 中的上述字段:
// 缺对应 beta token → strip;客户端 header 已带对应 beta → 保留(不过度删除)。
// 这将限制完全建立在 "能力维度" 上,与 model 名 / token type / mimicry 子路径无关。
@@ -935,6 +941,13 @@ func sanitizeAnthropicBodyForBetaTokens(body []byte, anthropicBetaHeader string)
body, changed = b, true
}
// thinking.block_binding:需要 thinking-binding-controls beta。
if b, deleted := stripAnthropicBodyFieldUnlessBeta(
body, "thinking.block_binding", anthropicBetaHeader, claude.BetaThinkingBindingControls,
); deleted {
body, changed = b, true
}
// fallbacks:server-side refusal fallback,仅接受 server-side-fallback beta。
if b, deleted := stripAnthropicBodyFieldUnlessBeta(
body, "fallbacks", anthropicBetaHeader, claude.BetaServerSideFallback,