From 82f1f79b3e5695cd127267286a0080b2e3770462 Mon Sep 17 00:00:00 2001 From: Quentin Machu Date: Thu, 20 Feb 2025 11:42:59 +0800 Subject: [PATCH] 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 --- .changeset/cold-shirts-deny.md | 5 ++ src/api/providers/bedrock.ts | 132 ++++++++++++++++++--------------- 2 files changed, 76 insertions(+), 61 deletions(-) create mode 100644 .changeset/cold-shirts-deny.md diff --git a/.changeset/cold-shirts-deny.md b/.changeset/cold-shirts-deny.md new file mode 100644 index 0000000000..13459ef8b6 --- /dev/null +++ b/.changeset/cold-shirts-deny.md @@ -0,0 +1,5 @@ +--- +"claude-dev": patch +--- + +fix: make AWS Bedrock authentication predictable diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index 04de4af2b2..75a0c980b1 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -3,79 +3,25 @@ import { Anthropic } from "@anthropic-ai/sdk" import { ApiHandler } from "../" import { ApiHandlerOptions, bedrockDefaultModelId, BedrockModelId, bedrockModels, ModelInfo } from "../../shared/api" import { ApiStream } from "../transform/stream" -import { fromIni } from "@aws-sdk/credential-providers" +import { fromNodeProviderChain } from "@aws-sdk/credential-providers" // https://docs.anthropic.com/en/api/claude-on-amazon-bedrock export class AwsBedrockHandler implements ApiHandler { private options: ApiHandlerOptions - private client: AnthropicBedrock | any - private initializationPromise: Promise constructor(options: ApiHandlerOptions) { this.options = options - this.initializationPromise = this.initializeClient() - } - - private async initializeClient() { - let clientConfig: any = { - awsRegion: this.options.awsRegion || "us-east-1", - } - try { - if (this.options.awsUseProfile) { - // Use profile-based credentials if enabled - // Use named profile, defaulting to 'default' if not specified - var credentials: any - if (this.options.awsProfile) { - credentials = await fromIni({ - profile: this.options.awsProfile, - ignoreCache: true, - })() - } else { - credentials = await fromIni({ - ignoreCache: true, - })() - } - clientConfig.awsAccessKey = credentials.accessKeyId - clientConfig.awsSecretKey = credentials.secretAccessKey - clientConfig.awsSessionToken = credentials.sessionToken - } else if (this.options.awsAccessKey && this.options.awsSecretKey) { - // Use direct credentials if provided - clientConfig.awsAccessKey = this.options.awsAccessKey - clientConfig.awsSecretKey = this.options.awsSecretKey - if (this.options.awsSessionToken) { - clientConfig.awsSessionToken = this.options.awsSessionToken - } - } - } catch (error) { - console.error("Failed to initialize Bedrock client:", error) - throw error - } finally { - this.client = new AnthropicBedrock(clientConfig) - } } async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream { // cross region inference requires prefixing the model id with the region - let modelId: string - if (this.options.awsUseCrossRegionInference) { - let regionPrefix = (this.options.awsRegion || "").slice(0, 3) - switch (regionPrefix) { - case "us-": - modelId = `us.${this.getModel().id}` - break - case "eu-": - modelId = `eu.${this.getModel().id}` - break - default: - // cross region inference is not supported in this region, falling back to default model - modelId = this.getModel().id - break - } - } else { - modelId = this.getModel().id - } + let modelId = await this.getModelId() - const stream = await this.client.messages.create({ + // create anthropic client, using sessions created or renewed after this handler's + // initialization, and allowing for session renewal if necessary as well + let client = await this.getClient() + + const stream = await client.messages.create({ model: modelId, max_tokens: this.getModel().info.maxTokens || 8192, temperature: 0, @@ -142,4 +88,68 @@ export class AwsBedrockHandler implements ApiHandler { info: bedrockModels[bedrockDefaultModelId], } } + + private async getClient(): Promise { + // Create AWS credentials by executing a an AWS provider chain exactly as the + // Anthropic SDK does it, by wrapping the default chain into a temporary process + // environment. + const providerChain = fromNodeProviderChain() + const credentials = await AwsBedrockHandler.withTempEnv( + () => { + AwsBedrockHandler.setEnv("AWS_REGION", this.options.awsRegion) + AwsBedrockHandler.setEnv("AWS_ACCESS_KEY_ID", this.options.awsAccessKey) + AwsBedrockHandler.setEnv("AWS_SECRET_ACCESS_KEY", this.options.awsSecretKey) + AwsBedrockHandler.setEnv("AWS_SESSION_TOKEN", this.options.awsSessionToken) + AwsBedrockHandler.setEnv("AWS_PROFILE", this.options.awsProfile) + }, + () => providerChain(), + ) + + // Return an AnthropicBedrock client with the resolved/assumed credentials. + // + // When AnthropicBedrock creates its AWS client, the chain will execute very + // fast as the access/secret keys will already be already provided, and have + // a higher precedence than the profiles. + return new AnthropicBedrock({ + awsAccessKey: credentials.accessKeyId, + awsSecretKey: credentials.secretAccessKey, + awsSessionToken: credentials.sessionToken, + awsRegion: this.options.awsRegion || "us-east-1", + }) + } + + private async getModelId(): Promise { + if (this.options.awsUseCrossRegionInference) { + let regionPrefix = (this.options.awsRegion || "").slice(0, 3) + switch (regionPrefix) { + case "us-": + return `us.${this.getModel().id}` + case "eu-": + return `eu.${this.getModel().id}` + break + default: + // cross region inference is not supported in this region, falling back to default model + return this.getModel().id + break + } + } + return this.getModel().id + } + + private static async withTempEnv(updateEnv: () => void, fn: () => Promise): Promise { + const previousEnv = { ...process.env } + + try { + updateEnv() + return await fn() + } finally { + process.env = previousEnv + } + } + + private static async setEnv(key: string, value: string | undefined) { + if (key !== "" && value !== undefined) { + process.env[key] = value + } + } }