diff --git a/go.mod b/go.mod index 97a477b6b..4ef592399 100644 --- a/go.mod +++ b/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/ diff --git a/go.sum b/go.sum index 83e0abdbc..b7562cbda 100644 --- a/go.sum +++ b/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= diff --git a/providers/anthropic/anthropic.go b/providers/anthropic/anthropic.go index a7c74bb66..360cd076c 100644 --- a/providers/anthropic/anthropic.go +++ b/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( diff --git a/providers/anthropic/bedrock.go b/providers/anthropic/bedrock.go index 87039a230..ae9b5a309 100644 --- a/providers/anthropic/bedrock.go +++ b/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}}, + } } diff --git a/providers/bedrock/bedrock.go b/providers/bedrock/bedrock.go index f956a0c1c..67a05db08 100644 --- a/providers/bedrock/bedrock.go +++ b/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) { diff --git a/providertests/.env.sample b/providertests/.env.sample index e94d94d90..9666bdbb2 100644 --- a/providertests/.env.sample +++ b/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= diff --git a/providertests/bedrock_test.go b/providertests/bedrock_test.go index c405ee184..ef5b09953 100644 --- a/providertests/bedrock_test.go +++ b/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") +} diff --git a/providertests/testdata/TestBedrockBasicAuth/simple.yaml b/providertests/testdata/TestBedrockBasicAuth/simple.yaml new file mode 100644 index 000000000..78bdadacf --- /dev/null +++ b/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 diff --git a/providertests/testdata/TestBedrockBasicAuth/simple_streaming.yaml b/providertests/testdata/TestBedrockBasicAuth/simple_streaming.yaml new file mode 100644 index 000000000..467575035 --- /dev/null +++ b/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