diff --git a/backend/internal/service/bedrock_request.go b/backend/internal/service/bedrock_request.go index 8a1fb317..f4416ce3 100644 --- a/backend/internal/service/bedrock_request.go +++ b/backend/internal/service/bedrock_request.go @@ -185,6 +185,7 @@ func BuildBedrockURL(region, modelID string, stream bool) string { // 5. 清理 cache_control 中 Bedrock 不支持的字段(scope, ttl) // 6. 修复 thinking 字段兼容性(Opus 4.7 仅支持 adaptive,enabled 需要 budget_tokens) // 7. 清理 tool_use.id / tool_use_id 中 Bedrock 不接受的字符 +// 8. 根据最终 Bedrock beta tokens 剥离不再支持的 beta 字段 func PrepareBedrockRequestBody(body []byte, modelID string, betaHeader string) ([]byte, error) { betaTokens := ResolveBedrockBetaTokens(betaHeader, body, modelID) return PrepareBedrockRequestBodyWithTokens(body, modelID, betaTokens, false) @@ -195,6 +196,9 @@ func PrepareBedrockRequestBody(body []byte, modelID string, betaHeader string) ( func PrepareBedrockRequestBodyWithTokens(body []byte, modelID string, betaTokens []string, ccCompat bool) ([]byte, error) { var err error + betaTokens = filterBedrockBetaTokens(betaTokens) + body = sanitizeBedrockFieldsForBetaTokens(body, betaTokens) + // 注入 anthropic_version(Bedrock 要求) body, err = sjson.SetBytes(body, "anthropic_version", "bedrock-2023-05-31") if err != nil { @@ -471,6 +475,8 @@ var bedrockSupportedBetaTokens = map[string]bool{ "tool-examples-2025-10-29": true, } +const bedrockContextManagementBetaToken = "context-management-2025-06-27" + // bedrockBetaTokenTransforms 定义 Bedrock Invoke 特有的 beta 头转换规则 // Anthropic 直接 API 使用通用头,Bedrock Invoke 需要特定的替代头 var bedrockBetaTokenTransforms = map[string]string{ @@ -617,6 +623,22 @@ func filterBedrockBetaTokens(tokens []string) []string { return result } +func sanitizeBedrockFieldsForBetaTokens(body []byte, betaTokens []string) []byte { + if !containsBedrockBetaToken(betaTokens, bedrockContextManagementBetaToken) && gjson.GetBytes(body, "context_management").Exists() { + body, _ = sjson.DeleteBytes(body, "context_management") + } + return body +} + +func containsBedrockBetaToken(tokens []string, target string) bool { + for _, token := range tokens { + if token == target { + return true + } + } + return false +} + // bedrockToolUseIDRe 匹配 Bedrock 允许的 tool_use ID 字符(字母、数字、下划线、连字符) var bedrockToolUseIDRe = regexp.MustCompile(`[^a-zA-Z0-9_-]`) diff --git a/backend/internal/service/bedrock_request_test.go b/backend/internal/service/bedrock_request_test.go index 98942ba4..94f1a118 100644 --- a/backend/internal/service/bedrock_request_test.go +++ b/backend/internal/service/bedrock_request_test.go @@ -378,6 +378,67 @@ func TestPrepareBedrockRequestBody_BetaFiltering(t *testing.T) { }) } +func TestPrepareBedrockRequestBodyWithTokens_ContextManagementRequiresSupportedBeta(t *testing.T) { + modelID := "us.anthropic.claude-opus-4-6-v1" + + t.Run("strips context_management when final tokens omit context-management beta", func(t *testing.T) { + input := `{ + "messages":[{"role":"user","content":"hi"}], + "max_tokens":100, + "context_management":{"edits":[{"type":"clear_thinking_20251015","keep":"all"}]} + }` + betaTokens := []string{"context-1m-2025-08-07"} + originalTokens := append([]string(nil), betaTokens...) + + result, err := PrepareBedrockRequestBodyWithTokens([]byte(input), modelID, betaTokens, false) + require.NoError(t, err) + + assert.False(t, gjson.GetBytes(result, "context_management").Exists()) + assert.Equal(t, originalTokens, betaTokens) + assert.Equal(t, originalTokens, bedrockAnthropicBetaNames(result)) + }) + + t.Run("leaves body without context_management otherwise intact", func(t *testing.T) { + input := `{"messages":[{"role":"user","content":"hi"}],"max_tokens":100}` + + result, err := PrepareBedrockRequestBodyWithTokens([]byte(input), modelID, nil, false) + require.NoError(t, err) + + assert.False(t, gjson.GetBytes(result, "context_management").Exists()) + assert.False(t, gjson.GetBytes(result, "anthropic_beta").Exists()) + assert.Equal(t, "hi", gjson.GetBytes(result, "messages.0.content").String()) + assert.Equal(t, int64(100), gjson.GetBytes(result, "max_tokens").Int()) + }) + + t.Run("filters explicit unsupported context-management beta and strips field", func(t *testing.T) { + input := `{ + "messages":[{"role":"user","content":"hi"}], + "max_tokens":100, + "context_management":{"edits":[{"type":"clear_thinking_20251015","keep":"all"}]} + }` + + result, err := PrepareBedrockRequestBodyWithTokens( + []byte(input), + modelID, + []string{bedrockContextManagementBetaToken, "context-1m-2025-08-07"}, + false, + ) + require.NoError(t, err) + + assert.False(t, gjson.GetBytes(result, "context_management").Exists()) + assert.Equal(t, []string{"context-1m-2025-08-07"}, bedrockAnthropicBetaNames(result)) + }) +} + +func bedrockAnthropicBetaNames(body []byte) []string { + arr := gjson.GetBytes(body, "anthropic_beta").Array() + names := make([]string, len(arr)) + for i, token := range arr { + names[i] = token.String() + } + return names +} + func TestBedrockCrossRegionPrefix(t *testing.T) { tests := []struct { region string