Skip to content

Commit c907d2a

Browse files
dtmeadowscameron-mcateer
authored andcommitted
feat(bedrock): add AnthropicBedrockMantle client (#704)
* feat(bedrock): add AnthropicBedrockMantle client Type-restricted client exposing only Messages and Beta.Messages. Does not call DefaultClientOptions() — fully isolated from ANTHROPIC_API_KEY. - Service name: bedrock-mantle - Base URL: https://bedrock-mantle.{region}.api.aws/anthropic - Env vars: ANTHROPIC_BEDROCK_MANTLE_* with ANTHROPIC_AWS_* fallbacks * chore: remove bedrock-mantle example * fix: remove workspace id from mantle client * feat: support request options in mantle client * chore: rename bedrock mantle token env var --------- Co-authored-by: Cameron McAteer <[email protected]>
1 parent 80df704 commit c907d2a

4 files changed

Lines changed: 722 additions & 2 deletions

File tree

bedrock/bedrockmantle.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package bedrock
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/anthropics/anthropic-sdk-go"
8+
"github.com/anthropics/anthropic-sdk-go/internal/awsauth"
9+
"github.com/anthropics/anthropic-sdk-go/option"
10+
)
11+
12+
const mantleServiceName = "bedrock-mantle"
13+
14+
// MantleClientConfig holds the configuration for creating an Anthropic Bedrock Mantle client.
15+
type MantleClientConfig struct {
16+
// APIKey is the Anthropic API key for x-api-key authentication.
17+
// Takes precedence over AWS credentials. When no AWS auth args are set, falls back
18+
// to the AWS_BEARER_TOKEN_BEDROCK environment variable (then ANTHROPIC_AWS_API_KEY)
19+
// before trying SigV4.
20+
APIKey string
21+
22+
// AWSAccessKey is the AWS access key ID for SigV4 authentication.
23+
// Must be paired with AWSSecretAccessKey. When unset, credentials are resolved
24+
// via the default AWS credential chain (env vars, shared credentials file, IAM roles, etc.).
25+
AWSAccessKey string
26+
27+
// AWSSecretAccessKey is the AWS secret access key for SigV4 authentication.
28+
// When unset, credentials are resolved via the default AWS credential chain
29+
// (env vars, shared credentials file, IAM roles, etc.).
30+
AWSSecretAccessKey string
31+
32+
// AWSSessionToken is the optional AWS session token for temporary credentials.
33+
// When unset, resolved via the default AWS credential chain if applicable.
34+
AWSSessionToken string
35+
36+
// AWSProfile is the AWS named profile for credential resolution via the provider chain.
37+
AWSProfile string
38+
39+
// AWSRegion is the AWS region for the base URL and SigV4 signing.
40+
// Resolved by precedence: MantleClientConfig.AWSRegion > AWS_REGION env var.
41+
AWSRegion string
42+
43+
// BaseURL overrides the default base URL.
44+
// Resolved by precedence: MantleClientConfig.BaseURL > ANTHROPIC_BEDROCK_MANTLE_BASE_URL env >
45+
// https://bedrock-mantle.{region}.api.aws/anthropic
46+
BaseURL string
47+
48+
// SkipAuth skips Mantle-specific authentication (API key and SigV4).
49+
// This is useful when a gateway or proxy handles authentication on your behalf.
50+
// Note: when using [NewMantleClient], the base SDK may still send an X-Api-Key header
51+
// if the ANTHROPIC_API_KEY environment variable is set.
52+
SkipAuth bool
53+
}
54+
55+
// MantleClient provides access to the Anthropic Bedrock Mantle API.
56+
// Only the Messages API (/v1/messages) and its subpaths are supported.
57+
type MantleClient struct {
58+
Options []option.RequestOption
59+
Messages anthropic.MessageService
60+
Beta MantleBetaService
61+
}
62+
63+
// MantleBetaService exposes only the beta resources supported by Bedrock Mantle.
64+
type MantleBetaService struct {
65+
Options []option.RequestOption
66+
Messages anthropic.BetaMessageService
67+
}
68+
69+
// NewMantleClient creates a new Bedrock Mantle client with the given configuration.
70+
// Only the Messages API (/v1/messages) and its subpaths are supported on Bedrock Mantle.
71+
//
72+
// Any additional [option.RequestOption] values are applied after the client's
73+
// internal options (base URL, auth, etc.), so they can be used to set custom
74+
// headers, timeouts, middleware, and other request-level settings.
75+
//
76+
// Auth is resolved by precedence:
77+
// 1. APIKey arg (x-api-key header)
78+
// 2. AWSAccessKey + AWSSecretAccessKey args (SigV4)
79+
// 3. AWSProfile arg (SigV4 via provider chain)
80+
// 4. AWS_BEARER_TOKEN_BEDROCK env var, then ANTHROPIC_AWS_API_KEY (x-api-key header)
81+
// 5. Default AWS credential chain (SigV4)
82+
func NewMantleClient(ctx context.Context, cfg MantleClientConfig, opts ...option.RequestOption) (*MantleClient, error) {
83+
baseOpts, err := awsauth.CreateClientOptions(ctx, mantleToInternalConfig(cfg), mantleResolveParams())
84+
if err != nil {
85+
return nil, err
86+
}
87+
88+
// We intentionally do not call anthropic.DefaultClientOptions() here.
89+
// The Mantle client resolves its own base URL, auth, and workspace ID — the
90+
// base SDK defaults (ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL) do not apply.
91+
//
92+
// User-provided opts are appended last so they take highest precedence.
93+
opts = append(baseOpts, opts...)
94+
95+
return &MantleClient{
96+
Options: opts,
97+
Messages: anthropic.NewMessageService(opts...),
98+
Beta: MantleBetaService{
99+
Options: opts,
100+
Messages: anthropic.NewBetaMessageService(opts...),
101+
},
102+
}, nil
103+
}
104+
105+
func mantleResolveParams() awsauth.ResolveParams {
106+
return awsauth.ResolveParams{
107+
EnvAPIKey: "AWS_BEARER_TOKEN_BEDROCK",
108+
EnvAPIKeyFallback: "ANTHROPIC_AWS_API_KEY",
109+
EnvBaseURL: "ANTHROPIC_BEDROCK_MANTLE_BASE_URL",
110+
DeriveBaseURL: func(region string) string { return fmt.Sprintf("https://bedrock-mantle.%s.api.aws/anthropic", region) },
111+
ServiceName: mantleServiceName,
112+
}
113+
}
114+
115+
func mantleToInternalConfig(cfg MantleClientConfig) awsauth.ClientConfig {
116+
return awsauth.ClientConfig{
117+
APIKey: cfg.APIKey,
118+
AWSAccessKey: cfg.AWSAccessKey,
119+
AWSSecretAccessKey: cfg.AWSSecretAccessKey,
120+
AWSSessionToken: cfg.AWSSessionToken,
121+
AWSProfile: cfg.AWSProfile,
122+
AWSRegion: cfg.AWSRegion,
123+
BaseURL: cfg.BaseURL,
124+
SkipAuth: cfg.SkipAuth,
125+
}
126+
}

bedrock/bedrockmantle_live_test.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package bedrock_test
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
8+
"github.com/anthropics/anthropic-sdk-go"
9+
"github.com/anthropics/anthropic-sdk-go/bedrock"
10+
)
11+
12+
// Live integration tests for Bedrock Mantle. Skipped unless ANTHROPIC_LIVE=1.
13+
//
14+
// Required env vars vary by auth mode:
15+
//
16+
// API key mode: AWS_BEARER_TOKEN_BEDROCK (or ANTHROPIC_AWS_API_KEY),
17+
// AWS_REGION
18+
//
19+
// SigV4 mode: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION
20+
//
21+
// Run: ANTHROPIC_LIVE=1 go test ./bedrock/... -run TestLiveMantle -v
22+
23+
func skipUnlessLive(t *testing.T) {
24+
t.Helper()
25+
if os.Getenv("ANTHROPIC_LIVE") != "1" {
26+
t.Skip("set ANTHROPIC_LIVE=1 to run live integration tests")
27+
}
28+
}
29+
30+
func requireEnv(t *testing.T, names ...string) {
31+
t.Helper()
32+
for _, name := range names {
33+
if os.Getenv(name) == "" {
34+
t.Fatalf("required env var %s is not set", name)
35+
}
36+
}
37+
}
38+
39+
func liveModel() string {
40+
if m := os.Getenv("ANTHROPIC_LIVE_MODEL"); m != "" {
41+
return m
42+
}
43+
return "claude-sonnet-4-6"
44+
}
45+
46+
func sendMantleMessage(t *testing.T, client *bedrock.MantleClient) {
47+
t.Helper()
48+
49+
message, err := client.Messages.New(context.Background(), anthropic.MessageNewParams{
50+
Model: liveModel(),
51+
MaxTokens: 32,
52+
Messages: []anthropic.MessageParam{
53+
anthropic.NewUserMessage(anthropic.NewTextBlock("Say exactly: hello")),
54+
},
55+
})
56+
if err != nil {
57+
t.Fatalf("Messages.New failed: %v", err)
58+
}
59+
if len(message.Content) == 0 {
60+
t.Fatal("expected non-empty content in response")
61+
}
62+
t.Logf("response: %s", message.Content[0].Text)
63+
}
64+
65+
func TestLiveMantleAPIKey(t *testing.T) {
66+
skipUnlessLive(t)
67+
requireEnv(t, "AWS_REGION")
68+
69+
// Need at least one of these for the API key
70+
apiKey := os.Getenv("AWS_BEARER_TOKEN_BEDROCK")
71+
if apiKey == "" {
72+
apiKey = os.Getenv("ANTHROPIC_AWS_API_KEY")
73+
}
74+
if apiKey == "" {
75+
t.Fatal("required env var AWS_BEARER_TOKEN_BEDROCK or ANTHROPIC_AWS_API_KEY is not set")
76+
}
77+
78+
client, err := bedrock.NewMantleClient(context.Background(), bedrock.MantleClientConfig{
79+
APIKey: apiKey,
80+
})
81+
if err != nil {
82+
t.Fatalf("NewMantleClient failed: %v", err)
83+
}
84+
85+
sendMantleMessage(t, client)
86+
}
87+
88+
func TestLiveMantleSigV4ExplicitCreds(t *testing.T) {
89+
skipUnlessLive(t)
90+
requireEnv(t, "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION")
91+
92+
client, err := bedrock.NewMantleClient(context.Background(), bedrock.MantleClientConfig{
93+
AWSAccessKey: os.Getenv("AWS_ACCESS_KEY_ID"),
94+
AWSSecretAccessKey: os.Getenv("AWS_SECRET_ACCESS_KEY"),
95+
AWSSessionToken: os.Getenv("AWS_SESSION_TOKEN"),
96+
})
97+
if err != nil {
98+
t.Fatalf("NewMantleClient failed: %v", err)
99+
}
100+
101+
sendMantleMessage(t, client)
102+
}
103+
104+
func TestLiveMantleSigV4DefaultChain(t *testing.T) {
105+
skipUnlessLive(t)
106+
requireEnv(t, "AWS_REGION")
107+
108+
// Clear all API key env vars so the default AWS credential chain is used
109+
t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "")
110+
t.Setenv("ANTHROPIC_AWS_API_KEY", "")
111+
t.Setenv("ANTHROPIC_API_KEY", "")
112+
113+
client, err := bedrock.NewMantleClient(context.Background(), bedrock.MantleClientConfig{})
114+
if err != nil {
115+
t.Fatalf("NewMantleClient failed (default AWS credential chain): %v", err)
116+
}
117+
118+
sendMantleMessage(t, client)
119+
}
120+
121+
func TestLiveMantleSigV4ProfileFromCredentialsFile(t *testing.T) {
122+
skipUnlessLive(t)
123+
requireEnv(t, "AWS_REGION", "AWS_PROFILE")
124+
125+
// Clear explicit creds and API keys so the SDK must resolve from ~/.aws/credentials
126+
t.Setenv("AWS_BEARER_TOKEN_BEDROCK", "")
127+
t.Setenv("ANTHROPIC_AWS_API_KEY", "")
128+
t.Setenv("ANTHROPIC_API_KEY", "")
129+
t.Setenv("AWS_ACCESS_KEY_ID", "")
130+
t.Setenv("AWS_SECRET_ACCESS_KEY", "")
131+
t.Setenv("AWS_SESSION_TOKEN", "")
132+
133+
client, err := bedrock.NewMantleClient(context.Background(), bedrock.MantleClientConfig{
134+
AWSProfile: os.Getenv("AWS_PROFILE"),
135+
})
136+
if err != nil {
137+
t.Fatalf("NewMantleClient failed (profile from credentials file): %v", err)
138+
}
139+
140+
sendMantleMessage(t, client)
141+
}

0 commit comments

Comments
 (0)