Skip to content

Commit 2f1f4a1

Browse files
feat(client): Create Bedrock Mantle client (#810)
1 parent 4105fd6 commit 2f1f4a1

File tree

6 files changed

+495
-2
lines changed

6 files changed

+495
-2
lines changed

bin/replace-internal-symlinks

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ for package in "${PACKAGES[@]}"; do
1515
INTERNAL_SYMLINK="$PACKAGE_DIR/src/internal"
1616

1717
if [ -L "$INTERNAL_SYMLINK" ]; then
18-
echo "Processing $package..."
18+
echo "Processing $package internal symlink..."
1919

2020
# Remove the symlink
2121
rm "$INTERNAL_SYMLINK"
@@ -31,3 +31,13 @@ for package in "${PACKAGES[@]}"; do
3131
echo "No 'internal' symlink found in $package or it's not a symlink"
3232
fi
3333
done
34+
35+
# Replace individual file symlinks within packages
36+
BEDROCK_AWS_AUTH="$ROOT_DIR/packages/bedrock-sdk/src/core/aws-auth.ts"
37+
if [ -L "$BEDROCK_AWS_AUTH" ]; then
38+
echo "Processing bedrock-sdk aws-auth symlink..."
39+
RESOLVED="$(realpath "$BEDROCK_AWS_AUTH")"
40+
rm "$BEDROCK_AWS_AUTH"
41+
cp "$RESOLVED" "$BEDROCK_AWS_AUTH"
42+
echo "Replaced aws-auth.ts symlink with file copy"
43+
fi
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../aws-sdk/src/core/auth.ts

packages/bedrock-sdk/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './client';
22
export { AnthropicBedrock as default } from './client';
3+
export { AnthropicBedrockMantle, BedrockMantleClientOptions } from './mantle-client';
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import type { NullableHeaders } from './internal/headers';
2+
import { buildHeaders } from './internal/headers';
3+
import * as Errors from './core/error';
4+
import { readEnv } from './internal/utils/env';
5+
import { BaseAnthropic, ClientOptions } from '@anthropic-ai/sdk/client';
6+
import * as Resources from '@anthropic-ai/sdk/resources/index';
7+
import { AwsCredentialIdentityProvider } from '@smithy/types';
8+
import { getAuthHeaders } from './core/aws-auth';
9+
import { FinalRequestOptions } from './internal/request-options';
10+
import { FinalizedRequestInit } from './internal/types';
11+
12+
const DEFAULT_SERVICE_NAME = 'bedrock-mantle';
13+
14+
export interface BedrockMantleClientOptions extends ClientOptions {
15+
/**
16+
* AWS region for the Bedrock Mantle API.
17+
*
18+
* Resolved by precedence: `awsRegion` arg > `AWS_REGION` env var > `AWS_DEFAULT_REGION` env var.
19+
*/
20+
awsRegion?: string | undefined;
21+
22+
/**
23+
* API key for x-api-key authentication.
24+
*
25+
* Takes precedence over AWS credential options. If neither `apiKey` nor
26+
* AWS credentials are provided, falls back to the `AWS_BEARER_TOKEN_BEDROCK`
27+
* environment variable, then to the default AWS credential chain.
28+
*/
29+
apiKey?: string | undefined;
30+
31+
/**
32+
* AWS access key ID for SigV4 authentication.
33+
*
34+
* Must be provided together with `awsSecretAccessKey`.
35+
*/
36+
awsAccessKey?: string | null | undefined;
37+
38+
/**
39+
* AWS secret access key for SigV4 authentication.
40+
*
41+
* Must be provided together with `awsAccessKey`.
42+
*/
43+
awsSecretAccessKey?: string | null | undefined;
44+
45+
/**
46+
* AWS session token for temporary credentials.
47+
*/
48+
awsSessionToken?: string | null | undefined;
49+
50+
/**
51+
* AWS named profile for credential resolution.
52+
*
53+
* When set, credentials are loaded from the AWS credential chain
54+
* using this profile.
55+
*/
56+
awsProfile?: string | undefined;
57+
58+
/**
59+
* Custom provider chain resolver for AWS credentials.
60+
* Useful for non-Node environments, like edge workers, where the default
61+
* credential provider chain may not work.
62+
*/
63+
providerChainResolver?: (() => Promise<AwsCredentialIdentityProvider>) | null;
64+
65+
/**
66+
* Skip authentication for requests. This is useful when you have a gateway
67+
* or proxy that handles authentication on your behalf.
68+
*
69+
* @default false
70+
*/
71+
skipAuth?: boolean;
72+
}
73+
74+
/**
75+
* API Client for interfacing with the Anthropic Bedrock Mantle API.
76+
*
77+
* This client uses SigV4 authentication with the `bedrock-mantle` service name
78+
* and targets `https://bedrock-mantle.{region}.api.aws/anthropic`.
79+
*
80+
* Only the `messages` and `beta.messages` resources are supported.
81+
*/
82+
export class AnthropicBedrockMantle extends BaseAnthropic {
83+
messages: Resources.Messages = new Resources.Messages(this);
84+
beta: MantleBetaResource = makeMantleBetaResource(this);
85+
86+
awsRegion: string | undefined;
87+
awsAccessKey: string | null;
88+
awsSecretAccessKey: string | null;
89+
awsSessionToken: string | null;
90+
awsProfile: string | null;
91+
providerChainResolver: (() => Promise<AwsCredentialIdentityProvider>) | null;
92+
skipAuth: boolean = false;
93+
94+
private _useSigV4: boolean;
95+
96+
/**
97+
* API Client for interfacing with the Anthropic Bedrock Mantle API.
98+
*
99+
* Auth is resolved by precedence: `apiKey` constructor arg > explicit AWS
100+
* credentials > `awsProfile` > `AWS_BEARER_TOKEN_BEDROCK` env var > default
101+
* AWS credential chain.
102+
*
103+
* @param {string | undefined} [opts.apiKey] - API key for x-api-key authentication.
104+
* @param {string | null | undefined} [opts.awsAccessKey] - AWS access key ID for SigV4 authentication.
105+
* @param {string | null | undefined} [opts.awsSecretAccessKey] - AWS secret access key for SigV4 authentication.
106+
* @param {string | null | undefined} [opts.awsSessionToken] - AWS session token for temporary credentials.
107+
* @param {string | undefined} [opts.awsProfile] - AWS named profile for credential resolution.
108+
* @param {string | undefined} [opts.awsRegion] - AWS region. Resolved by precedence: arg > `AWS_REGION` env > `AWS_DEFAULT_REGION` env.
109+
* @param {(() => Promise<AwsCredentialIdentityProvider>) | null} [opts.providerChainResolver] - Custom provider chain resolver for AWS credentials.
110+
* @param {string} [opts.baseURL=process.env['ANTHROPIC_BEDROCK_MANTLE_BASE_URL'] ?? https://bedrock-mantle.{awsRegion}.api.aws/anthropic] - Override the default base URL for the API.
111+
* @param {number} [opts.timeout=10 minutes] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out.
112+
* @param {MergedRequestInit} [opts.fetchOptions] - Additional `RequestInit` options to be passed to `fetch` calls.
113+
* @param {Fetch} [opts.fetch] - Specify a custom `fetch` function implementation.
114+
* @param {number} [opts.maxRetries=2] - The maximum number of times the client will retry a request.
115+
* @param {HeadersLike} opts.defaultHeaders - Default headers to include with every request to the API.
116+
* @param {Record<string, string | undefined>} opts.defaultQuery - Default query parameters to include with every request to the API.
117+
* @param {boolean} [opts.dangerouslyAllowBrowser=false] - By default, client-side use of this library is not allowed, as it risks exposing your secret API credentials to attackers.
118+
* @param {boolean} [opts.skipAuth=false] - Skip authentication for requests. This is useful when you have a gateway or proxy that handles authentication on your behalf.
119+
*/
120+
constructor({
121+
awsRegion,
122+
baseURL,
123+
apiKey,
124+
awsAccessKey = null,
125+
awsSecretAccessKey = null,
126+
awsSessionToken = null,
127+
awsProfile,
128+
providerChainResolver = null,
129+
skipAuth = false,
130+
...opts
131+
}: BedrockMantleClientOptions = {}) {
132+
// Region resolution: arg > AWS_REGION env > AWS_DEFAULT_REGION env
133+
const resolvedRegion = awsRegion ?? readEnv('AWS_REGION') ?? readEnv('AWS_DEFAULT_REGION');
134+
135+
const resolvedBaseURL =
136+
baseURL ??
137+
readEnv('ANTHROPIC_BEDROCK_MANTLE_BASE_URL') ??
138+
(resolvedRegion ? `https://bedrock-mantle.${resolvedRegion}.api.aws/anthropic` : undefined);
139+
140+
if (!resolvedBaseURL) {
141+
throw new Errors.AnthropicError(
142+
'No AWS region or base URL found. Set `awsRegion` in the constructor, the `AWS_REGION` / `AWS_DEFAULT_REGION` environment variable, or provide a `baseURL` / `ANTHROPIC_BEDROCK_MANTLE_BASE_URL` environment variable.',
143+
);
144+
}
145+
146+
// Precedence-based auth resolution:
147+
// 1. apiKey constructor arg
148+
// 2. awsAccessKey/awsSecretAccessKey constructor args (SigV4)
149+
// 3. awsProfile constructor arg (SigV4)
150+
// 4. AWS_BEARER_TOKEN_BEDROCK env var
151+
// 5. Default AWS credential chain (SigV4)
152+
const hasExplicitApiKey = apiKey != null;
153+
const hasPartialAwsCreds = (awsAccessKey != null) !== (awsSecretAccessKey != null);
154+
if (hasPartialAwsCreds) {
155+
throw new Errors.AnthropicError(
156+
'`awsAccessKey` and `awsSecretAccessKey` must be provided together. You provided only one.',
157+
);
158+
}
159+
const hasExplicitAwsCreds = awsAccessKey != null && awsSecretAccessKey != null;
160+
const hasAwsProfile = awsProfile != null;
161+
162+
let resolvedApiKey: string | undefined;
163+
if (hasExplicitApiKey) {
164+
resolvedApiKey = apiKey;
165+
} else if (!hasExplicitAwsCreds && !hasAwsProfile) {
166+
resolvedApiKey = readEnv('AWS_BEARER_TOKEN_BEDROCK') ?? undefined;
167+
}
168+
169+
super({
170+
apiKey: resolvedApiKey,
171+
baseURL: resolvedBaseURL,
172+
...opts,
173+
});
174+
175+
this.awsRegion = resolvedRegion;
176+
this.awsAccessKey = awsAccessKey;
177+
this.awsSecretAccessKey = awsSecretAccessKey;
178+
this.awsSessionToken = awsSessionToken;
179+
this.awsProfile = awsProfile ?? null;
180+
this.providerChainResolver = providerChainResolver;
181+
this.skipAuth = skipAuth;
182+
this._useSigV4 = resolvedApiKey == null;
183+
}
184+
185+
protected override async authHeaders(opts: FinalRequestOptions): Promise<NullableHeaders | undefined> {
186+
if (this.skipAuth) {
187+
return undefined;
188+
}
189+
190+
if (!this._useSigV4) {
191+
// API key mode — use inherited x-api-key auth
192+
return super.authHeaders(opts);
193+
}
194+
195+
// SigV4 mode — auth is handled in prepareRequest since it needs the full request
196+
return undefined;
197+
}
198+
199+
protected override validateHeaders(): void {
200+
// Auth validation is handled in the constructor and prepareRequest
201+
}
202+
203+
protected override async prepareRequest(
204+
request: FinalizedRequestInit,
205+
{ url, options }: { url: string; options: FinalRequestOptions },
206+
): Promise<void> {
207+
if (this.skipAuth || !this._useSigV4) {
208+
return;
209+
}
210+
211+
const regionName = this.awsRegion;
212+
if (!regionName) {
213+
throw new Errors.AnthropicError(
214+
'No AWS region found. Set `awsRegion` in the constructor or the `AWS_REGION` / `AWS_DEFAULT_REGION` environment variable.',
215+
);
216+
}
217+
218+
const headers = await getAuthHeaders(request, {
219+
url,
220+
regionName,
221+
serviceName: DEFAULT_SERVICE_NAME,
222+
awsAccessKey: this.awsAccessKey,
223+
awsSecretAccessKey: this.awsSecretAccessKey,
224+
awsSessionToken: this.awsSessionToken,
225+
awsProfile: this.awsProfile,
226+
providerChainResolver: this.providerChainResolver,
227+
});
228+
request.headers = buildHeaders([headers, request.headers]).values;
229+
}
230+
}
231+
232+
/**
233+
* Bedrock Mantle does not support completions, models, or non-messages beta resources.
234+
*/
235+
type MantleBetaResource = Pick<Resources.Beta, 'messages'>;
236+
237+
function makeMantleBetaResource(client: AnthropicBedrockMantle): MantleBetaResource {
238+
const { messages } = new Resources.Beta(client);
239+
return { messages };
240+
}

0 commit comments

Comments
 (0)