fix: clean Bedrock headers (#24718)

Bedrock chat provider requests can inherit Anthropic public API headers
from the process environment, which causes mixed Anthropic and Bedrock
auth headers on signed requests.

Update the Anthropic SDK fork so its Bedrock middleware strips
Anthropic-only headers before signing requests, and keep a chatprovider
regression test for the production request shape.

> Mux is acting on Mike's behalf.
This commit is contained in:
Michael Suchacz
2026-04-26 21:50:29 +02:00
committed by GitHub
parent e32581dc68
commit 99a83a2702
3 changed files with 88 additions and 6 deletions
@@ -2,6 +2,7 @@ package chatprovider_test
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
@@ -967,6 +968,87 @@ func TestModelFromConfig_Bedrock(t *testing.T) {
})
}
// TestModelFromConfig_BedrockStripsAnthropicHeaders is a regression test
// for a bug where the Anthropic SDK reads ANTHROPIC_API_KEY from the
// process environment and adds X-Api-Key and Anthropic-Version headers to
// every request. On Bedrock, these headers conflict with SigV4 signing and
// cause auth failures. The SDK's Bedrock middleware strips them before
// signing. This test verifies the outgoing request shape with both
// Anthropic and AWS credentials present.
func TestModelFromConfig_BedrockStripsAnthropicHeaders(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitShort)
t.Setenv("ANTHROPIC_API_KEY", "anthropic-env-key")
t.Setenv("AWS_REGION", "us-east-2")
t.Setenv("AWS_ACCESS_KEY_ID", "test-access-key")
t.Setenv("AWS_SECRET_ACCESS_KEY", "test-secret-key")
t.Setenv("AWS_SESSION_TOKEN", "test-session-token")
type requestCapture struct {
Authorization string
AnthropicVersion string
XAPIKey string
Body string
ReadError error
}
requests := make(chan requestCapture, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
requests <- requestCapture{
Authorization: r.Header.Get("Authorization"),
AnthropicVersion: r.Header.Get("Anthropic-Version"),
XAPIKey: r.Header.Get("X-Api-Key"),
Body: string(body),
ReadError: err,
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(bedrockNonStreamingResponse())
}))
defer server.Close()
model, err := chatprovider.ModelFromConfig(
fantasybedrock.Name,
"anthropic.claude-opus-4-6-v1",
chatprovider.ProviderAPIKeys{
ByProvider: map[string]string{
fantasybedrock.Name: "",
},
BaseURLByProvider: map[string]string{
fantasybedrock.Name: server.URL,
},
},
chatprovider.UserAgent(),
nil,
nil,
)
require.NoError(t, err)
require.NotNil(t, model)
_, err = model.Generate(ctx, fantasy.Call{
Prompt: []fantasy.Message{
{
Role: fantasy.MessageRoleUser,
Content: []fantasy.MessagePart{
fantasy.TextPart{Text: "hello"},
},
},
},
})
require.NoError(t, err)
got := testutil.TryReceive(ctx, t, requests)
require.NoError(t, got.ReadError)
require.Empty(t, got.AnthropicVersion)
require.Empty(t, got.XAPIKey)
require.Contains(t, got.Authorization, "AWS4-HMAC-SHA256")
require.NotContains(t, got.Authorization, "anthropic-version")
require.NotContains(t, got.Authorization, "x-api-key")
require.Contains(t, got.Body, `"anthropic_version":"bedrock-2023-05-31"`)
}
func bedrockNonStreamingResponse() map[string]any {
return map[string]any{
"id": "msg_01Test",