Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -835,7 +835,28 @@ func main() {

If you already have an `aws.Config`, you can also use it directly with `bedrock.WithConfig(cfg)`.

Read more about Anthropic and Amazon Bedrock [here](https://docs.anthropic.com/en/api/claude-on-amazon-bedrock).
### Bearer Token Authentication

You can also authenticate with Bedrock using bearer tokens instead of AWS credentials. This is particularly useful in corporate environments where teams need access to Bedrock without managing AWS credentials, IAM roles, or account-level permissions.

```go
package main

import (
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/bedrock"
)

func main() {
client := anthropic.NewClient(
bedrock.WithBearerToken("your-bearer-token", "us-east-1"),
)
}
```

The bearer token can be provided directly or via the `AWS_BEARER_TOKEN_BEDROCK` environment variable. If the token parameter is empty, it will automatically read from the environment variable.

Read more about Anthropic and Amazon Bedrock [here](https://docs.anthropic.com/en/api/claude-on-amazon-bedrock) and about Bedrock API keys [here](https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys-use.html).

## Google Vertex AI

Expand Down
53 changes: 42 additions & 11 deletions bedrock/bedrock.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"io"
"net/http"
"net/url"
"os"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
Expand Down Expand Up @@ -179,7 +180,7 @@ func WithLoadDefaultConfig(ctx context.Context, optFns ...func(*config.LoadOptio
// intercepts request to the Messages API so that this SDK can be used with Amazon Bedrock.
func WithConfig(cfg aws.Config) option.RequestOption {
signer := v4.NewSigner()
middleware := bedrockMiddleware(signer, cfg)
middleware := bedrockMiddleware(signer, cfg, "")

return requestconfig.RequestOptionFunc(func(rc *requestconfig.RequestConfig) error {
return rc.Apply(
Expand All @@ -189,7 +190,33 @@ func WithConfig(cfg aws.Config) option.RequestOption {
})
}

func bedrockMiddleware(signer *v4.Signer, cfg aws.Config) option.Middleware {
// WithBearerToken returns a request option which uses the provided bearer token for authentication
// instead of AWS SigV4 signing. This is useful in corporate environments where teams need access
// to Bedrock without managing AWS credentials, IAM roles, or account-level permissions.
//
// The bearer token can be obtained from AWS Bedrock API keys.
// See: https://docs.aws.amazon.com/bedrock/latest/userguide/api-keys-use.html
//
// If token is empty, it will attempt to read from the AWS_BEARER_TOKEN_BEDROCK environment variable.
func WithBearerToken(token string, region string) option.RequestOption {
if token == "" {
token = os.Getenv("AWS_BEARER_TOKEN_BEDROCK")
}
if region == "" {
region = "us-east-1"
}

middleware := bedrockMiddleware(nil, aws.Config{Region: region}, token)

return requestconfig.RequestOptionFunc(func(rc *requestconfig.RequestConfig) error {
return rc.Apply(
option.WithBaseURL(fmt.Sprintf("https://bedrock-runtime.%s.amazonaws.com", region)),
option.WithMiddleware(middleware),
)
})
}

func bedrockMiddleware(signer *v4.Signer, cfg aws.Config, bearerToken string) option.Middleware {
return func(r *http.Request, next option.MiddlewareNext) (res *http.Response, err error) {
var body []byte
if r.Body != nil {
Expand Down Expand Up @@ -230,16 +257,20 @@ func bedrockMiddleware(signer *v4.Signer, cfg aws.Config) option.Middleware {
r.ContentLength = int64(len(body))
}

ctx := r.Context()
credentials, err := cfg.Credentials.Retrieve(ctx)
if err != nil {
return nil, err
}
if bearerToken != "" {
r.Header.Set("Authorization", "Bearer "+bearerToken)
} else {
ctx := r.Context()
credentials, err := cfg.Credentials.Retrieve(ctx)
if err != nil {
return nil, err
}

hash := sha256.Sum256(body)
err = signer.SignHTTP(ctx, credentials, r, hex.EncodeToString(hash[:]), "bedrock", cfg.Region, time.Now())
if err != nil {
return nil, err
hash := sha256.Sum256(body)
err = signer.SignHTTP(ctx, credentials, r, hex.EncodeToString(hash[:]), "bedrock", cfg.Region, time.Now())
if err != nil {
return nil, err
}
}

return next(r)
Expand Down
50 changes: 48 additions & 2 deletions bedrock/bedrock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ func TestBedrockURLEncoding(t *testing.T) {
}

signer := v4.NewSigner()
middleware := bedrockMiddleware(signer, cfg)
middleware := bedrockMiddleware(signer, cfg, "")

// Create request body
requestBody := map[string]interface{}{
requestBody := map[string]any{
"model": tc.model,
"stream": tc.stream,
"messages": []map[string]string{
Expand Down Expand Up @@ -125,3 +125,49 @@ func TestBedrockURLEncoding(t *testing.T) {
})
}
}

func TestBedrockBearerToken(t *testing.T) {
token := "test-bearer-token"
region := "us-west-2"

middleware := bedrockMiddleware(nil, aws.Config{Region: region}, token)

requestBody := map[string]any{
"model": "claude-3-sonnet",
"messages": []map[string]string{
{"role": "user", "content": "Hello"},
},
}

bodyBytes, err := json.Marshal(requestBody)
if err != nil {
t.Fatalf("Failed to marshal request body: %v", err)
}

req, err := http.NewRequest("POST", "https://bedrock-runtime.us-west-2.amazonaws.com/v1/messages", bytes.NewReader(bodyBytes))
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")

_, err = middleware(req, func(r *http.Request) (*http.Response, error) {
authHeader := r.Header.Get("Authorization")
expectedAuth := "Bearer " + token
if authHeader != expectedAuth {
t.Errorf("Expected Authorization header %q, got %q", expectedAuth, authHeader)
}

if r.Header.Get("X-Amz-Date") != "" {
t.Error("Expected no AWS SigV4 headers when using bearer token")
}

return &http.Response{
StatusCode: 200,
Body: http.NoBody,
}, nil
})

if err != nil {
t.Fatalf("Middleware failed: %v", err)
}
}
55 changes: 55 additions & 0 deletions examples/bedrock-bearer-token-streaming/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package main

import (
"context"
"os"

"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/bedrock"
)

func main() {
token := os.Getenv("AWS_BEARER_TOKEN_BEDROCK")
region := os.Getenv("AWS_REGION")
if region == "" {
region = "us-east-1"
}

client := anthropic.NewClient(
bedrock.WithBearerToken(token, region),
)

content := "Write a haiku about Go programming."

println("[user]: " + content)

stream := client.Messages.NewStreaming(context.TODO(), anthropic.MessageNewParams{
MaxTokens: 1024,
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock(content)),
},
Model: "us.anthropic.claude-sonnet-4-20250514-v1:0",
})

print("[assistant]: ")

for stream.Next() {
event := stream.Current()

switch eventVariant := event.AsAny().(type) {
case anthropic.MessageDeltaEvent:
print(eventVariant.Delta.StopSequence)
case anthropic.ContentBlockDeltaEvent:
switch deltaVariant := eventVariant.Delta.AsAny().(type) {
case anthropic.TextDelta:
print(deltaVariant.Text)
}
}
}

println()

if stream.Err() != nil {
println("Stream error:", stream.Err().Error())
}
}
39 changes: 39 additions & 0 deletions examples/bedrock-bearer-token/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package main

import (
"context"
"os"

"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/bedrock"
)

func main() {
token := os.Getenv("AWS_BEARER_TOKEN_BEDROCK")
region := os.Getenv("AWS_REGION")
if region == "" {
region = "us-east-1"
}

client := anthropic.NewClient(
bedrock.WithBearerToken(token, region),
)

content := "Write me a function to call the Anthropic message API in Node.js using the Anthropic Typescript SDK."

println("[user]: " + content)

message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
MaxTokens: 1024,
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock(content)),
},
Model: "us.anthropic.claude-sonnet-4-20250514-v1:0",
StopSequences: []string{"```\n"},
})
if err != nil {
panic(err)
}

println("[assistant]: " + message.Content[0].Text + message.StopSequence)
}