diff --git a/coderd/x/chatd/chatprovider/chatprovider_test.go b/coderd/x/chatd/chatprovider/chatprovider_test.go index acb0dc9ea1..bb94d3b0c3 100644 --- a/coderd/x/chatd/chatprovider/chatprovider_test.go +++ b/coderd/x/chatd/chatprovider/chatprovider_test.go @@ -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", diff --git a/go.mod b/go.mod index c34586470a..c7c4e3552c 100644 --- a/go.mod +++ b/go.mod @@ -89,10 +89,10 @@ replace github.com/spf13/afero => github.com/aslilac/afero v0.0.0-20250403163713 // See: https://github.com/coder/fantasy/commits/f83367a4a205 replace charm.land/fantasy => github.com/coder/fantasy v0.0.0-20260426185602-951a49c681df -// coder/coder uses a fork of charmbracelet's fork of the Anthropic Go SDK with some -// additional performance improvements. -// See: https://github.com/coder/anthropic-sdk-go/commits/a31d7d0e7067 -replace github.com/charmbracelet/anthropic-sdk-go => github.com/coder/anthropic-sdk-go v0.0.0-20260415160422-a31d7d0e7067 +// coder/coder uses a fork of charmbracelet's fork of the Anthropic Go SDK +// with performance improvements and Bedrock header cleanup. +// See: https://github.com/coder/anthropic-sdk-go/commits/3be8e193ec89 +replace github.com/charmbracelet/anthropic-sdk-go => github.com/coder/anthropic-sdk-go v0.0.0-20260424230212-3be8e193ec89 // Replace sdks with our own optimized forks until relevant upstream PRs are merged. // https://github.com/anthropics/anthropic-sdk-go/pull/262 diff --git a/go.sum b/go.sum index 26c747b5fc..35bb75efaf 100644 --- a/go.sum +++ b/go.sum @@ -314,8 +314,8 @@ github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225 h1:tRIViZ5JR github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225/go.mod h1:rNLVpYgEVeu1Zk29K64z6Od8RBP9DwqCu9OfCzh8MR4= github.com/coder/aisdk-go v0.0.9 h1:Vzo/k2qwVGLTR10ESDeP2Ecek1SdPfZlEjtTfMveiVo= github.com/coder/aisdk-go v0.0.9/go.mod h1:KF6/Vkono0FJJOtWtveh5j7yfNrSctVTpwgweYWSp5M= -github.com/coder/anthropic-sdk-go v0.0.0-20260415160422-a31d7d0e7067 h1:v1RAkUO21u0QH6UlUueSHMbgFf++BZZW41Rj6LM2eWo= -github.com/coder/anthropic-sdk-go v0.0.0-20260415160422-a31d7d0e7067/go.mod h1:hqlYqR7uPKOKfnNeicUbZp0Ps0GeYFlKYtwh5HGDCx8= +github.com/coder/anthropic-sdk-go v0.0.0-20260424230212-3be8e193ec89 h1:IVJutHfU944mb4D66K7XdPwKMAJrNC9FOq6JB4bveuI= +github.com/coder/anthropic-sdk-go v0.0.0-20260424230212-3be8e193ec89/go.mod h1:hqlYqR7uPKOKfnNeicUbZp0Ps0GeYFlKYtwh5HGDCx8= github.com/coder/boundary v0.8.4-0.20260304164748-566aeea939ab h1:HrlxyTmMQpOHfSKzRU1vf5TxrmV6vL5OiWq+Dvn5qh0= github.com/coder/boundary v0.8.4-0.20260304164748-566aeea939ab/go.mod h1:BhJhyKW/+zZQzaGZ3vn27if2k0Vx5xLXzq7ZCQx5gPk= github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41 h1:SBN/DA63+ZHwuWwPHPYoCZ/KLAjHv5g4h2MS4f2/MTI=