diff --git a/coderd/x/chatd/chatprovider/chatprovider_test.go b/coderd/x/chatd/chatprovider/chatprovider_test.go index bb94d3b0c3..0644550cd6 100644 --- a/coderd/x/chatd/chatprovider/chatprovider_test.go +++ b/coderd/x/chatd/chatprovider/chatprovider_test.go @@ -1049,6 +1049,87 @@ func TestModelFromConfig_BedrockStripsAnthropicHeaders(t *testing.T) { require.Contains(t, got.Body, `"anthropic_version":"bedrock-2023-05-31"`) } +func TestModelFromConfig_BedrockStreamingHeaders(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 { + Path string + Accept string + BedrockAccept string + Authorization 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{ + Path: r.URL.Path, + Accept: r.Header.Get("Accept"), + BedrockAccept: r.Header.Get("X-Amzn-Bedrock-Accept"), + Authorization: r.Header.Get("Authorization"), + Body: string(body), + ReadError: err, + } + + w.Header().Set("Content-Type", "application/vnd.amazon.eventstream") + w.WriteHeader(http.StatusOK) + })) + 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) + + stream, err := model.Stream(ctx, fantasy.Call{ + Prompt: []fantasy.Message{ + { + Role: fantasy.MessageRoleUser, + Content: []fantasy.MessagePart{ + fantasy.TextPart{Text: "hello"}, + }, + }, + }, + }) + require.NoError(t, err) + + for part := range stream { + require.NotEqual(t, fantasy.StreamPartTypeError, part.Type) + break + } + + got := testutil.TryReceive(ctx, t, requests) + require.NoError(t, got.ReadError) + require.Equal(t, "/model/us.anthropic.claude-opus-4-6-v1/invoke-with-response-stream", got.Path) + require.Equal(t, "application/vnd.amazon.eventstream", got.Accept) + require.Equal(t, "application/json", got.BedrockAccept) + require.Contains(t, got.Authorization, "AWS4-HMAC-SHA256") + require.Contains(t, got.Authorization, "x-amzn-bedrock-accept") + 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 76b18d902e..3d4240ccfb 100644 --- a/go.mod +++ b/go.mod @@ -91,8 +91,8 @@ replace charm.land/fantasy => github.com/coder/fantasy v0.0.0-20260426185602-951 // 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 +// See: https://github.com/coder/anthropic-sdk-go/commits/67514346e0fb +replace github.com/charmbracelet/anthropic-sdk-go => github.com/coder/anthropic-sdk-go v0.0.0-20260428111347-67514346e0fb // 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 959cbc5ed7..8d36160150 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-20260424230212-3be8e193ec89 h1:IVJutHfU944mb4D66K7XdPwKMAJrNC9FOq6JB4bveuI= -github.com/coder/anthropic-sdk-go v0.0.0-20260424230212-3be8e193ec89/go.mod h1:hqlYqR7uPKOKfnNeicUbZp0Ps0GeYFlKYtwh5HGDCx8= +github.com/coder/anthropic-sdk-go v0.0.0-20260428111347-67514346e0fb h1:/qJzrmYz+ii329dgoQ1BxtlrhdJvtTkk7UwhrroSohU= +github.com/coder/anthropic-sdk-go v0.0.0-20260428111347-67514346e0fb/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=