Skip to content

Commit 95f1f99

Browse files
dtmeadowsstainless-app[bot]
authored andcommitted
chore(internal): client updates
1 parent db18294 commit 95f1f99

4 files changed

Lines changed: 1087 additions & 0 deletions

File tree

aws/aws.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Package aws provides client configuration for the Anthropic AWS gateway.
2+
//
3+
// This package name may shadow the AWS SDK's "aws" package. If you use both in the
4+
// same file, alias one of them:
5+
//
6+
// import (
7+
// anthropicaws "github.com/anthropics/anthropic-sdk-go/aws"
8+
// awssdk "github.com/aws/aws-sdk-go-v2/aws"
9+
// )
10+
package aws
11+
12+
import (
13+
"context"
14+
"fmt"
15+
16+
"github.com/anthropics/anthropic-sdk-go"
17+
"github.com/anthropics/anthropic-sdk-go/internal/awsauth"
18+
"github.com/anthropics/anthropic-sdk-go/option"
19+
)
20+
21+
const defaultServiceName = "aws-external-anthropic"
22+
23+
// ClientConfig holds the configuration for creating an Anthropic AWS gateway client.
24+
type ClientConfig struct {
25+
// APIKey is the Anthropic API key for x-api-key authentication.
26+
// Takes precedence over AWS credentials. When no AWS auth args are set, falls back
27+
// to the ANTHROPIC_AWS_API_KEY environment variable before trying SigV4.
28+
APIKey string
29+
30+
// AWSAccessKey is the AWS access key ID for SigV4 authentication.
31+
// Must be paired with AWSSecretAccessKey. When unset, credentials are resolved
32+
// via the default AWS credential chain (env vars, shared credentials file, IAM roles, etc.).
33+
AWSAccessKey string
34+
35+
// AWSSecretAccessKey is the AWS secret access key for SigV4 authentication.
36+
// When unset, credentials are resolved via the default AWS credential chain
37+
// (env vars, shared credentials file, IAM roles, etc.).
38+
AWSSecretAccessKey string
39+
40+
// AWSSessionToken is the optional AWS session token for temporary credentials.
41+
// When unset, resolved via the default AWS credential chain if applicable.
42+
AWSSessionToken string
43+
44+
// AWSProfile is the AWS named profile for credential resolution via the provider chain.
45+
AWSProfile string
46+
47+
// AWSRegion is the AWS region for the gateway URL and SigV4 signing.
48+
// Resolved by precedence: ClientConfig.AWSRegion > AWS_REGION env var.
49+
AWSRegion string
50+
51+
// WorkspaceID is sent as the anthropic-workspace-id header on every request.
52+
// Resolved by precedence: ClientConfig.WorkspaceID > ANTHROPIC_AWS_WORKSPACE_ID env var. Required.
53+
WorkspaceID string
54+
55+
// BaseURL overrides the default gateway base URL.
56+
// Resolved by precedence: ClientConfig.BaseURL > ANTHROPIC_AWS_BASE_URL env > https://aws-external-anthropic.{region}.api.aws
57+
BaseURL string
58+
59+
// SkipAuth skips all authentication (API key and SigV4) and the workspace ID requirement.
60+
// This is useful when a gateway or proxy handles authentication on your behalf.
61+
SkipAuth bool
62+
}
63+
64+
// Client provides access to the Anthropic API via the AWS gateway.
65+
type Client struct {
66+
Options []option.RequestOption
67+
Completions anthropic.CompletionService
68+
Messages anthropic.MessageService
69+
Models anthropic.ModelService
70+
Beta anthropic.BetaService
71+
}
72+
73+
// NewClient creates a new AWS gateway client with the given configuration.
74+
//
75+
// Auth is resolved by precedence:
76+
// 1. APIKey arg (x-api-key header)
77+
// 2. AWSAccessKey + AWSSecretAccessKey args (SigV4)
78+
// 3. AWSProfile arg (SigV4 via provider chain)
79+
// 4. ANTHROPIC_AWS_API_KEY env var (x-api-key header)
80+
// 5. Default AWS credential chain (SigV4)
81+
func NewClient(ctx context.Context, cfg ClientConfig) (*Client, error) {
82+
opts, err := awsauth.CreateClientOptions(ctx, toInternalConfig(cfg), awsResolveParams())
83+
if err != nil {
84+
return nil, err
85+
}
86+
87+
// We intentionally do not call anthropic.DefaultClientOptions() here.
88+
// The AWS client resolves its own base URL, auth, and workspace ID — the
89+
// base SDK defaults (ANTHROPIC_API_KEY, ANTHROPIC_BASE_URL) do not apply.
90+
91+
return &Client{
92+
Options: opts,
93+
Completions: anthropic.NewCompletionService(opts...),
94+
Messages: anthropic.NewMessageService(opts...),
95+
Models: anthropic.NewModelService(opts...),
96+
Beta: anthropic.NewBetaService(opts...),
97+
}, nil
98+
}
99+
100+
func awsResolveParams() awsauth.ResolveParams {
101+
return awsauth.ResolveParams{
102+
EnvAPIKey: "ANTHROPIC_AWS_API_KEY",
103+
EnvWorkspaceID: "ANTHROPIC_AWS_WORKSPACE_ID",
104+
EnvBaseURL: "ANTHROPIC_AWS_BASE_URL",
105+
DeriveBaseURL: func(region string) string { return fmt.Sprintf("https://aws-external-anthropic.%s.api.aws", region) },
106+
ServiceName: defaultServiceName,
107+
}
108+
}
109+
110+
func toInternalConfig(cfg ClientConfig) awsauth.ClientConfig {
111+
return awsauth.ClientConfig{
112+
APIKey: cfg.APIKey,
113+
AWSAccessKey: cfg.AWSAccessKey,
114+
AWSSecretAccessKey: cfg.AWSSecretAccessKey,
115+
AWSSessionToken: cfg.AWSSessionToken,
116+
AWSProfile: cfg.AWSProfile,
117+
AWSRegion: cfg.AWSRegion,
118+
WorkspaceID: cfg.WorkspaceID,
119+
BaseURL: cfg.BaseURL,
120+
SkipAuth: cfg.SkipAuth,
121+
}
122+
}

aws/aws_live_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package aws_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/aws"
10+
)
11+
12+
// Live integration tests for the AWS gateway client. Skipped unless ANTHROPIC_LIVE=1.
13+
//
14+
// Required env vars vary by auth mode:
15+
//
16+
// API key mode: ANTHROPIC_AWS_API_KEY, ANTHROPIC_AWS_WORKSPACE_ID, AWS_REGION
17+
//
18+
// SigV4 mode: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION,
19+
// ANTHROPIC_AWS_WORKSPACE_ID
20+
//
21+
// Run: ANTHROPIC_LIVE=1 go test ./aws/... -run TestLiveAWS -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 sendAWSMessage(t *testing.T, client *aws.Client) {
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 TestLiveAWSAPIKey(t *testing.T) {
66+
skipUnlessLive(t)
67+
requireEnv(t, "ANTHROPIC_AWS_API_KEY", "ANTHROPIC_AWS_WORKSPACE_ID", "AWS_REGION")
68+
69+
client, err := aws.NewClient(context.Background(), aws.ClientConfig{})
70+
if err != nil {
71+
t.Fatalf("NewClient failed: %v", err)
72+
}
73+
74+
sendAWSMessage(t, client)
75+
}
76+
77+
func TestLiveAWSSigV4ExplicitCreds(t *testing.T) {
78+
skipUnlessLive(t)
79+
requireEnv(t, "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_REGION", "ANTHROPIC_AWS_WORKSPACE_ID")
80+
81+
// Clear API key so SigV4 is used
82+
t.Setenv("ANTHROPIC_AWS_API_KEY", "")
83+
84+
client, err := aws.NewClient(context.Background(), aws.ClientConfig{
85+
AWSAccessKey: os.Getenv("AWS_ACCESS_KEY_ID"),
86+
AWSSecretAccessKey: os.Getenv("AWS_SECRET_ACCESS_KEY"),
87+
AWSSessionToken: os.Getenv("AWS_SESSION_TOKEN"),
88+
})
89+
if err != nil {
90+
t.Fatalf("NewClient failed: %v", err)
91+
}
92+
93+
sendAWSMessage(t, client)
94+
}
95+
96+
func TestLiveAWSSigV4DefaultChain(t *testing.T) {
97+
skipUnlessLive(t)
98+
requireEnv(t, "AWS_REGION", "ANTHROPIC_AWS_WORKSPACE_ID")
99+
100+
// Clear all API key env vars so the default AWS credential chain is used
101+
t.Setenv("ANTHROPIC_AWS_API_KEY", "")
102+
103+
client, err := aws.NewClient(context.Background(), aws.ClientConfig{})
104+
if err != nil {
105+
t.Fatalf("NewClient failed (default AWS credential chain): %v", err)
106+
}
107+
108+
sendAWSMessage(t, client)
109+
}
110+
111+
func TestLiveAWSSigV4ProfileFromCredentialsFile(t *testing.T) {
112+
skipUnlessLive(t)
113+
requireEnv(t, "AWS_REGION", "ANTHROPIC_AWS_WORKSPACE_ID", "AWS_PROFILE")
114+
115+
// Clear explicit creds and API keys so the SDK must resolve from ~/.aws/credentials
116+
t.Setenv("ANTHROPIC_AWS_API_KEY", "")
117+
t.Setenv("AWS_ACCESS_KEY_ID", "")
118+
t.Setenv("AWS_SECRET_ACCESS_KEY", "")
119+
t.Setenv("AWS_SESSION_TOKEN", "")
120+
121+
client, err := aws.NewClient(context.Background(), aws.ClientConfig{
122+
AWSProfile: os.Getenv("AWS_PROFILE"),
123+
})
124+
if err != nil {
125+
t.Fatalf("NewClient failed (profile from credentials file): %v", err)
126+
}
127+
128+
sendAWSMessage(t, client)
129+
}

0 commit comments

Comments
 (0)