From 7ae0312091520ce46aea4fe0016f57e14c17d730 Mon Sep 17 00:00:00 2001 From: Lynricsy Date: Fri, 4 Sep 2026 19:05:14 +0800 Subject: [PATCH] =?UTF-8?q?fix(claude):=20=F0=9F=90=9B=20=E8=A1=A5?= =?UTF-8?q?=E9=BD=90=20thinking=20block=20binding=20beta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Wine Fox --- backend/internal/pkg/claude/constants.go | 12 +++++++----- .../gateway_context_management_test.go | 19 +++++++++++++++++++ backend/internal/service/gateway_request.go | 13 +++++++++++++ 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/backend/internal/pkg/claude/constants.go b/backend/internal/pkg/claude/constants.go index df9b7613b2..b9b5e51b2b 100644 --- a/backend/internal/pkg/claude/constants.go +++ b/backend/internal/pkg/claude/constants.go @@ -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, } } diff --git a/backend/internal/service/gateway_context_management_test.go b/backend/internal/service/gateway_context_management_test.go index 57ae438309..0e48f2cb91 100644 --- a/backend/internal/service/gateway_context_management_test.go +++ b/backend/internal/service/gateway_context_management_test.go @@ -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) { diff --git a/backend/internal/service/gateway_request.go b/backend/internal/service/gateway_request.go index 9a3ca9fb88..ab12f4ad22 100644 --- a/backend/internal/service/gateway_request.go +++ b/backend/internal/service/gateway_request.go @@ -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,