Skip to content

Commit 9b30efe

Browse files
authored
Merge pull request RooCodeInc#1874 from Quentin-M/fix/aws_bedrock_credentials
fix: make AWS Bedrock authentication predictable
2 parents 00506c0 + 82f1f79 commit 9b30efe

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)