feat(bedrock): add support for basic auth with api key on bedrock (#28)

Andrey Nering created

Change summary

go.mod                                                            |  4 
go.sum                                                            |  4 
providers/anthropic/anthropic.go                                  |  6 
providers/anthropic/bedrock.go                                    | 14 
providers/bedrock/bedrock.go                                      |  7 
providertests/.env.sample                                         |  1 
providertests/bedrock_test.go                                     | 14 
providertests/testdata/TestBedrockBasicAuth/simple.yaml           | 32 
providertests/testdata/TestBedrockBasicAuth/simple_streaming.yaml | 76 +
9 files changed, 145 insertions(+), 13 deletions(-)

Detailed changes

go.mod ๐Ÿ”—

@@ -6,6 +6,7 @@ require (
 	cloud.google.com/go/auth v0.17.0
 	github.com/anthropics/anthropic-sdk-go v1.10.0
 	github.com/aws/aws-sdk-go-v2 v1.39.3
+	github.com/aws/smithy-go v1.23.1
 	github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5
 	github.com/charmbracelet/x/json v0.2.0
 	github.com/go-viper/mapstructure/v2 v2.4.0
@@ -37,7 +38,6 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
 	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
 	github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
-	github.com/aws/smithy-go v1.23.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/go-logr/logr v1.4.3 // indirect
@@ -73,7 +73,7 @@ require (
 
 // NOTE(@andreynering): Temporarily pinning @fantasy branch with fixes:
 // https://github.com/charmbracelet/anthropic-sdk-go/commits/fantasy/
-replace github.com/anthropics/anthropic-sdk-go => github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251010172108-7b952cdeeb9d
+replace github.com/anthropics/anthropic-sdk-go => github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251020200929-fdb68318b7af
 
 // NOTE(@andreynering): Temporarily pinning @fantasy branch with fixes:
 // https://github.com/charmbracelet/go-genai/commits/fantasy/

go.sum ๐Ÿ”—

@@ -42,8 +42,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudr
 github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
 github.com/aws/smithy-go v1.23.1 h1:sLvcH6dfAFwGkHLZ7dGiYF7aK6mg4CgKA/iDKjLDt9M=
 github.com/aws/smithy-go v1.23.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
-github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251010172108-7b952cdeeb9d h1:qP7F7r7aVY7AReYHHgkQ79weuUEZK7zXtDtSEydYb0w=
-github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251010172108-7b952cdeeb9d/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
+github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251020200929-fdb68318b7af h1:iPwFVe5v46OfhqxKXSJ4J0YWf8XzthTnWyrim2yGFnU=
+github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251020200929-fdb68318b7af/go.mod h1:WTz31rIUHUHqai2UslPpw5CwXrQP3geYBioRV4WOLvE=
 github.com/charmbracelet/go-genai v0.0.0-20251009191514-c6fa9e37d847 h1:Oyo6YZ59iygXWNUlRozIOFHO4WUG9cNFhiUYCTq4AnU=
 github.com/charmbracelet/go-genai v0.0.0-20251009191514-c6fa9e37d847/go.mod h1:7pAilaICJlQBonjKKJNhftDFv3SREhZcTe9F6nRcjbg=
 github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5 h1:DTSZxdV9qQagD4iGcAt9RgaRBZtJl01bfKgdLzUzUPI=

providers/anthropic/anthropic.go ๐Ÿ”—

@@ -122,7 +122,7 @@ func WithHTTPClient(client option.HTTPClient) Option {
 
 func (a *provider) LanguageModel(modelID string) (fantasy.LanguageModel, error) {
 	clientOptions := make([]option.RequestOption, 0, 5+len(a.options.headers))
-	if a.options.apiKey != "" {
+	if a.options.apiKey != "" && !a.options.useBedrock {
 		clientOptions = append(clientOptions, option.WithAPIKey(a.options.apiKey))
 	}
 	if a.options.baseURL != "" {
@@ -157,10 +157,10 @@ func (a *provider) LanguageModel(modelID string) (fantasy.LanguageModel, error)
 		)
 	}
 	if a.options.useBedrock {
-		if a.options.skipAuth {
+		if a.options.skipAuth || a.options.apiKey != "" {
 			clientOptions = append(
 				clientOptions,
-				bedrock.WithConfig(dummyBedrockConfig),
+				bedrock.WithConfig(bedrockBasicAuthConfig(a.options.apiKey)),
 			)
 		} else {
 			clientOptions = append(

providers/anthropic/bedrock.go ๐Ÿ”—

@@ -1,14 +1,16 @@
 package anthropic
 
 import (
-	"context"
+	"cmp"
+	"os"
 
 	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/smithy-go/auth/bearer"
 )
 
-var dummyBedrockConfig = aws.Config{
-	Region: "us-east-1",
-	Credentials: aws.CredentialsProviderFunc(func(context.Context) (aws.Credentials, error) {
-		return aws.Credentials{}, nil
-	}),
+func bedrockBasicAuthConfig(apiKey string) aws.Config {
+	return aws.Config{
+		Region:                  cmp.Or(os.Getenv("AWS_REGION"), "us-east-1"),
+		BearerAuthTokenProvider: bearer.StaticTokenProvider{Token: bearer.Token{Value: apiKey}},
+	}
 }

providers/bedrock/bedrock.go ๐Ÿ”—

@@ -36,6 +36,13 @@ func New(opts ...Option) fantasy.Provider {
 	)
 }
 
+// WithAPIKey sets the access token for the Bedrock provider.
+func WithAPIKey(apiKey string) Option {
+	return func(o *options) {
+		o.anthropicOptions = append(o.anthropicOptions, anthropic.WithAPIKey(apiKey))
+	}
+}
+
 // WithHeaders sets the headers for the Bedrock provider.
 func WithHeaders(headers map[string]string) Option {
 	return func(o *options) {

providertests/.env.sample ๐Ÿ”—

@@ -1,6 +1,7 @@
 FANTASY_ANTHROPIC_API_KEY=
 FANTASY_AZURE_API_KEY=
 FANTASY_AZURE_BASE_URL=
+FANTASY_BEDROCK_API_KEY=
 FANTASY_GEMINI_API_KEY=
 FANTASY_GROQ_API_KEY=
 FANTASY_OPENAI_API_KEY=

providertests/bedrock_test.go ๐Ÿ”—

@@ -2,6 +2,7 @@ package providertests
 
 import (
 	"net/http"
+	"os"
 	"testing"
 
 	"charm.land/fantasy"
@@ -17,6 +18,10 @@ func TestBedrockCommon(t *testing.T) {
 	})
 }
 
+func TestBedrockBasicAuth(t *testing.T) {
+	testSimple(t, builderPair{"bedrock-anthropic-claude-3-sonnet", buildersBedrockBasicAuth, nil})
+}
+
 func builderBedrockClaude3Sonnet(r *recorder.Recorder) (fantasy.LanguageModel, error) {
 	provider := bedrock.New(
 		bedrock.WithHTTPClient(&http.Client{Transport: r}),
@@ -40,3 +45,12 @@ func builderBedrockClaude3Haiku(r *recorder.Recorder) (fantasy.LanguageModel, er
 	)
 	return provider.LanguageModel("us.anthropic.claude-3-haiku-20240307-v1:0")
 }
+
+func buildersBedrockBasicAuth(r *recorder.Recorder) (fantasy.LanguageModel, error) {
+	provider := bedrock.New(
+		bedrock.WithHTTPClient(&http.Client{Transport: r}),
+		bedrock.WithAPIKey(os.Getenv("FANTASY_BEDROCK_API_KEY")),
+		bedrock.WithSkipAuth(true),
+	)
+	return provider.LanguageModel("us.anthropic.claude-3-sonnet-20240229-v1:0")
+}

providertests/testdata/TestBedrockBasicAuth/simple.yaml ๐Ÿ”—

@@ -0,0 +1,32 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 211
+    host: ""
+    body: '{"max_tokens":4000,"messages":[{"content":[{"text":"Say hi in Portuguese","type":"text"}],"role":"user"}],"system":[{"text":"You are a helpful assistant","type":"text"}],"anthropic_version":"bedrock-2023-05-31"}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - Anthropic/Go 1.14.0
+    url: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-3-sonnet-20240229-v1%3A0/invoke
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: 248
+    body: '{"id":"msg_bdrk_01SLPR8DXPtQG4ryAsmuQrA9","type":"message","role":"assistant","model":"claude-3-sonnet-20240229","content":[{"type":"text","text":"Olรก!"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":16,"output_tokens":7}}'
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 3.764905916s

providertests/testdata/TestBedrockBasicAuth/simple_streaming.yaml ๐Ÿ”—

@@ -0,0 +1,76 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 211
+    host: ""
+    body: '{"max_tokens":4000,"messages":[{"content":[{"text":"Say hi in Portuguese","type":"text"}],"role":"user"}],"system":[{"text":"You are a helpful assistant","type":"text"}],"anthropic_version":"bedrock-2023-05-31"}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - Anthropic/Go 1.14.0
+    url: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-3-sonnet-20240229-v1%3A0/invoke-with-response-stream
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: !!binary |
+      AAABvQAAAEunJ1hxCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcAEGFwcG
+      xpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5SjBlWEJs
+      SWpvaWJXVnpjMkZuWlY5emRHRnlkQ0lzSW0xbGMzTmhaMlVpT25zaWFXUWlPaUp0YzJkZl
+      ltUnlhMTh3TVVjM1ZUWXlaMU4zYWxwU1ptMVVTMkpDWjNGQmExSWlMQ0owZVhCbElqb2li
+      V1Z6YzJGblpTSXNJbkp2YkdVaU9pSmhjM05wYzNSaGJuUWlMQ0p0YjJSbGJDSTZJbU5zWV
+      hWa1pTMHpMWE52Ym01bGRDMHlNREkwTURJeU9TSXNJbU52Ym5SbGJuUWlPbHRkTENKemRH
+      OXdYM0psWVhOdmJpSTZiblZzYkN3aWMzUnZjRjl6WlhGMVpXNWpaU0k2Ym5Wc2JDd2lkWE
+      5oWjJVaU9uc2lhVzV3ZFhSZmRHOXJaVzV6SWpveE5pd2liM1YwY0hWMFgzUnZhMlZ1Y3lJ
+      Nk1uMTlmUT09IiwicCI6ImFiYyJ9vnwv4QAAAPgAAABL/Ghc7Qs6ZXZlbnQtdHlwZQcABW
+      NodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUH
+      AAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRGOWliRzlqYTE5emRHRn
+      lkQ0lzSW1sdVpHVjRJam93TENKamIyNTBaVzUwWDJKc2IyTnJJanA3SW5SNWNHVWlPaUow
+      WlhoMElpd2lkR1Y0ZENJNklpSjlmUT09IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dX
+      Z3eHl6In2rrhIPAAAA+gAAAEuGqA+NCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQt
+      dHlwZQcAEGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcy
+      I6ImV5SjBlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElq
+      b3dMQ0prWld4MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaU
+      pQYkNKOWZRPT0iLCJwIjoiYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXpBQiJ9FHa6UgAA
+      AQ0AAABLt+BQZQs6ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaW
+      NhdGlvbi9qc29uDTptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElq
+      b2lZMjl1ZEdWdWRGOWliRzlqYTE5a1pXeDBZU0lzSW1sdVpHVjRJam93TENKa1pXeDBZU0
+      k2ZXlKMGVYQmxJam9pZEdWNGRGOWtaV3gwWVNJc0luUmxlSFFpT2lMRG9TSjlmUT09Iiwi
+      cCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVIn
+      3UdODmAAAA9AAAAEs5mLHsCzpldmVudC10eXBlBwAFY2h1bmsNOmNvbnRlbnQtdHlwZQcA
+      EGFwcGxpY2F0aW9uL2pzb24NOm1lc3NhZ2UtdHlwZQcABWV2ZW50eyJieXRlcyI6ImV5Sj
+      BlWEJsSWpvaVkyOXVkR1Z1ZEY5aWJHOWphMTlrWld4MFlTSXNJbWx1WkdWNElqb3dMQ0pr
+      Wld4MFlTSTZleUowZVhCbElqb2lkR1Y0ZEY5a1pXeDBZU0lzSW5SbGVIUWlPaUloSW4xOS
+      IsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5eiJ9l8ofGQAAANMAAABLSnlC+As6
+      ZXZlbnQtdHlwZQcABWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDT
+      ptZXNzYWdlLXR5cGUHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2lZMjl1ZEdWdWRG
+      OWliRzlqYTE5emRHOXdJaXdpYVc1a1pYZ2lPakI5IiwicCI6ImFiY2RlZmdoaWprbG1ub3
+      BxcnN0dXZ3eHl6QUJDREVGR0hJSktMTU5PUFFSU1RVVlcifUu+MSwAAAEuAAAASzGBBbEL
+      OmV2ZW50LXR5cGUHAAVjaHVuaw06Y29udGVudC10eXBlBwAQYXBwbGljYXRpb24vanNvbg
+      06bWVzc2FnZS10eXBlBwAFZXZlbnR7ImJ5dGVzIjoiZXlKMGVYQmxJam9pYldWemMyRm5a
+      VjlrWld4MFlTSXNJbVJsYkhSaElqcDdJbk4wYjNCZmNtVmhjMjl1SWpvaVpXNWtYM1IxY2
+      00aUxDSnpkRzl3WDNObGNYVmxibU5sSWpwdWRXeHNmU3dpZFhOaFoyVWlPbnNpYjNWMGNI
+      VjBYM1J2YTJWdWN5STZOMzE5IiwicCI6ImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6QU
+      JDREVGR0hJSktMTU5PUFFSU1RVViJ9tMfCKgAAAVMAAABLMMMhzws6ZXZlbnQtdHlwZQcA
+      BWNodW5rDTpjb250ZW50LXR5cGUHABBhcHBsaWNhdGlvbi9qc29uDTptZXNzYWdlLXR5cG
+      UHAAVldmVudHsiYnl0ZXMiOiJleUowZVhCbElqb2liV1Z6YzJGblpWOXpkRzl3SWl3aVlX
+      MWhlbTl1TFdKbFpISnZZMnN0YVc1MmIyTmhkR2x2YmsxbGRISnBZM01pT25zaWFXNXdkWF
+      JVYjJ0bGJrTnZkVzUwSWpveE5pd2liM1YwY0hWMFZHOXJaVzVEYjNWdWRDSTZOeXdpYVc1
+      MmIyTmhkR2x2Ymt4aGRHVnVZM2tpT2pVMk5Dd2labWx5YzNSQ2VYUmxUR0YwWlc1amVTST
+      ZOVFEyZlgwPSIsInAiOiJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ekFCQyJ9KKxn3Q==
+    headers:
+      Content-Type:
+      - application/vnd.amazon.eventstream
+    status: 200 OK
+    code: 200
+    duration: 693.876083ms