Skip to content

Commit 82f1f79

Browse files
committed
fix: make AWS Bedrock authentication predictable
Anthropic's Bedrock SDK creates an AWS credential provider chain [1] for each request it needs to sign. By doing so right before actually having to sign the request, it can utilize any session created after VSCode launched (i.e. outside of the process), and it can renew the sessions every time necessary transparently for the user. Cline, on the other hand, by transforming the provided AWS_PROFILE into a key / secret / session, as part of client initialization, completely short-circuits this, making it very difficult for users in companies where sessions are short-lived. Furthermore, Cline would silently ignore the provided AWS_PROFILE if there isn't a current/non-expired session at the time of initialization, pass null keys to the Bedrock SDK, which would then make use the default profile, which may not be configured or authorized to use AWS Bedrock (as most AWS SSO hub accounts would). From the perspective of the user, this would manifest itself as an "supported country" error or unhelpful errors that are basically impossible to debug without attaching a debugger to Cline.. and such developers may end up reaching out to their DevOps/IT teams for help, which also could turn into a waste of time. This PR addresses the aforementioned issues by resolving the credentials on every invocation. 1: https://github.com/anthropics/anthropic-sdk-typescript/blob/61b55599d50d9c93840e4736cb756cb3a62b0696/packages/bedrock-sdk/src/auth.ts#L19
1 parent 00506c0 commit 82f1f79

File tree

2 files changed

+76
-61
lines changed

2 files changed

+76
-61
lines changed

.changeset/cold-shirts-deny.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": patch
3+
---
4+
5+
fix: make AWS Bedrock authentication predictable

src/api/providers/bedrock.ts

Lines changed: 71 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,79 +3,25 @@ import { Anthropic } from "@anthropic-ai/sdk"
33
import { ApiHandler } from "../"
44
import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "../../shared/api"
55
import { ApiStream } from "../transform/stream"
6-
import { fromIni } from "@aws-sdk/credential-providers"
6+
import { fromNodeProviderChain } from "@aws-sdk/credential-providers"
77

88
// https://docs.anthropic.com/en/api/claude-on-amazon-bedrock
99
export class AwsBedrockHandler implements ApiHandler {
1010
private options: ApiHandlerOptions
11-
private client: AnthropicBedrock | any
12-
private initializationPromise: Promise<void>
1311

1412
constructor(options: ApiHandlerOptions) {
1513
this.options = options
16-
this.initializationPromise = this.initializeClient()
17-
}
18-
19-
private async initializeClient() {
20-
let clientConfig: any = {
21-
awsRegion: this.options.awsRegion || "us-east-1",
22-
}
23-
try {
24-
if (this.options.awsUseProfile) {
25-
// Use profile-based credentials if enabled
26-
// Use named profile, defaulting to 'default' if not specified
27-
var credentials: any
28-
if (this.options.awsProfile) {
29-
credentials = await fromIni({
30-
profile: this.options.awsProfile,
31-
ignoreCache: true,
32-
})()
33-
} else {
34-
credentials = await fromIni({
35-
ignoreCache: true,
36-
})()
37-
}
38-
clientConfig.awsAccessKey = credentials.accessKeyId
39-
clientConfig.awsSecretKey = credentials.secretAccessKey
40-
clientConfig.awsSessionToken = credentials.sessionToken
41-
} else if (this.options.awsAccessKey && this.options.awsSecretKey) {
42-
// Use direct credentials if provided
43-
clientConfig.awsAccessKey = this.options.awsAccessKey
44-
clientConfig.awsSecretKey = this.options.awsSecretKey
45-
if (this.options.awsSessionToken) {
46-
clientConfig.awsSessionToken = this.options.awsSessionToken
47-
}
48-
}
49-
} catch (error) {
50-
console.error("Failed to initialize Bedrock client:", error)
51-
throw error
52-
} finally {
53-
this.client = new AnthropicBedrock(clientConfig)
54-
}
5514
}
5615

5716
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
5817
// cross region inference requires prefixing the model id with the region
59-
let modelId: string
60-
if (this.options.awsUseCrossRegionInference) {
61-
let regionPrefix = (this.options.awsRegion || "").slice(0, 3)
62-
switch (regionPrefix) {
63-
case "us-":
64-
modelId = `us.${this.getModel().id}`
65-
break
66-
case "eu-":
67-
modelId = `eu.${this.getModel().id}`
68-
break
69-
default:
70-
// cross region inference is not supported in this region, falling back to default model
71-
modelId = this.getModel().id
72-
break
73-
}
74-
} else {
75-
modelId = this.getModel().id
76-
}
18+
let modelId = await this.getModelId()
19+
20+
// create anthropic client, using sessions created or renewed after this handler's
21+
// initialization, and allowing for session renewal if necessary as well
22+
let client = await this.getClient()
7723

78-
const stream = await this.client.messages.create({
24+
const stream = await client.messages.create({
7925
model: modelId,
8026
max_tokens: this.getModel().info.maxTokens || 8192,
8127
temperature: 0,
@@ -142,4 +88,68 @@ export class AwsBedrockHandler implements ApiHandler {
14288
info: bedrockModels[bedrockDefaultModelId],
14389
}
14490
}
91+
92+
private async getClient(): Promise<AnthropicBedrock> {
93+
// Create AWS credentials by executing a an AWS provider chain exactly as the
94+
// Anthropic SDK does it, by wrapping the default chain into a temporary process
95+
// environment.
96+
const providerChain = fromNodeProviderChain()
97+
const credentials = await AwsBedrockHandler.withTempEnv(
98+
() => {
99+
AwsBedrockHandler.setEnv("AWS_REGION", this.options.awsRegion)
100+
AwsBedrockHandler.setEnv("AWS_ACCESS_KEY_ID", this.options.awsAccessKey)
101+
AwsBedrockHandler.setEnv("AWS_SECRET_ACCESS_KEY", this.options.awsSecretKey)
102+
AwsBedrockHandler.setEnv("AWS_SESSION_TOKEN", this.options.awsSessionToken)
103+
AwsBedrockHandler.setEnv("AWS_PROFILE", this.options.awsProfile)
104+
},
105+
() => providerChain(),
106+
)
107+
108+
// Return an AnthropicBedrock client with the resolved/assumed credentials.
109+
//
110+
// When AnthropicBedrock creates its AWS client, the chain will execute very
111+
// fast as the access/secret keys will already be already provided, and have
112+
// a higher precedence than the profiles.
113+
return new AnthropicBedrock({
114+
awsAccessKey: credentials.accessKeyId,
115+
awsSecretKey: credentials.secretAccessKey,
116+
awsSessionToken: credentials.sessionToken,
117+
awsRegion: this.options.awsRegion || "us-east-1",
118+
})
119+
}
120+
121+
private async getModelId(): Promise<string> {
122+
if (this.options.awsUseCrossRegionInference) {
123+
let regionPrefix = (this.options.awsRegion || "").slice(0, 3)
124+
switch (regionPrefix) {
125+
case "us-":
126+
return `us.${this.getModel().id}`
127+
case "eu-":
128+
return `eu.${this.getModel().id}`
129+
break
130+
default:
131+
// cross region inference is not supported in this region, falling back to default model
132+
return this.getModel().id
133+
break
134+
}
135+
}
136+
return this.getModel().id
137+
}
138+
139+
private static async withTempEnv<R>(updateEnv: () => void, fn: () => Promise<R>): Promise<R> {
140+
const previousEnv = { ...process.env }
141+
142+
try {
143+
updateEnv()
144+
return await fn()
145+
} finally {
146+
process.env = previousEnv
147+
}
148+
}
149+
150+
private static async setEnv(key: string, value: string | undefined) {
151+
if (key !== "" && value !== undefined) {
152+
process.env[key] = value
153+
}
154+
}
145155
}

0 commit comments

Comments
 (0)