Skip to content

Commit b8fd389

Browse files
authored
Merge pull request #392 from Lunchb0ne/aws-profile-support
Add support for using AWS Profile
2 parents 0a32e24 + 0cfc095 commit b8fd389

File tree

6 files changed

+137
-27
lines changed

6 files changed

+137
-27
lines changed

.changeset/afraid-pillows-kiss.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": minor
3+
---
4+
5+
Added suport for configuring Bedrock provider with AWS Profiles. Useful for users with SSO or other integrations who don't have access to long term credentials.

src/api/providers/__tests__/bedrock.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
// Mock AWS SDK credential providers
2+
jest.mock("@aws-sdk/credential-providers", () => ({
3+
fromIni: jest.fn().mockReturnValue({
4+
accessKeyId: "profile-access-key",
5+
secretAccessKey: "profile-secret-key",
6+
}),
7+
}))
8+
19
import { AwsBedrockHandler } from "../bedrock"
210
import { MessageContent } from "../../../shared/api"
311
import { BedrockRuntimeClient } from "@aws-sdk/client-bedrock-runtime"
412
import { Anthropic } from "@anthropic-ai/sdk"
13+
import { fromIni } from "@aws-sdk/credential-providers"
514

615
describe("AwsBedrockHandler", () => {
716
let handler: AwsBedrockHandler
@@ -30,6 +39,57 @@ describe("AwsBedrockHandler", () => {
3039
})
3140
expect(handlerWithoutCreds).toBeInstanceOf(AwsBedrockHandler)
3241
})
42+
43+
it("should initialize with AWS profile credentials", () => {
44+
const handlerWithProfile = new AwsBedrockHandler({
45+
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
46+
awsRegion: "us-east-1",
47+
awsUseProfile: true,
48+
awsProfile: "test-profile",
49+
})
50+
expect(handlerWithProfile).toBeInstanceOf(AwsBedrockHandler)
51+
expect(handlerWithProfile["options"].awsUseProfile).toBe(true)
52+
expect(handlerWithProfile["options"].awsProfile).toBe("test-profile")
53+
})
54+
55+
it("should initialize with AWS profile enabled but no profile set", () => {
56+
const handlerWithoutProfile = new AwsBedrockHandler({
57+
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
58+
awsRegion: "us-east-1",
59+
awsUseProfile: true,
60+
})
61+
expect(handlerWithoutProfile).toBeInstanceOf(AwsBedrockHandler)
62+
expect(handlerWithoutProfile["options"].awsUseProfile).toBe(true)
63+
expect(handlerWithoutProfile["options"].awsProfile).toBeUndefined()
64+
})
65+
})
66+
67+
describe("AWS SDK client configuration", () => {
68+
it("should configure client with profile credentials when profile mode is enabled", async () => {
69+
const handlerWithProfile = new AwsBedrockHandler({
70+
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
71+
awsRegion: "us-east-1",
72+
awsUseProfile: true,
73+
awsProfile: "test-profile",
74+
})
75+
76+
// Mock a simple API call to verify credentials are used
77+
const mockResponse = {
78+
output: new TextEncoder().encode(JSON.stringify({ content: "test" })),
79+
}
80+
const mockSend = jest.fn().mockResolvedValue(mockResponse)
81+
handlerWithProfile["client"] = {
82+
send: mockSend,
83+
} as unknown as BedrockRuntimeClient
84+
85+
await handlerWithProfile.completePrompt("test")
86+
87+
// Verify the client was configured with profile credentials
88+
expect(mockSend).toHaveBeenCalled()
89+
expect(fromIni).toHaveBeenCalledWith({
90+
profile: "test-profile",
91+
})
92+
})
3393
})
3494

3595
describe("createMessage", () => {

src/api/providers/bedrock.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ConverseCommand,
55
BedrockRuntimeClientConfig,
66
} from "@aws-sdk/client-bedrock-runtime"
7+
import { fromIni } from "@aws-sdk/credential-providers"
78
import { Anthropic } from "@anthropic-ai/sdk"
89
import { ApiHandler, SingleCompletionHandler } from "../"
910
import { ApiHandlerOptions, BedrockModelId, ModelInfo, bedrockDefaultModelId, bedrockModels } from "../../shared/api"
@@ -50,13 +51,17 @@ export class AwsBedrockHandler implements ApiHandler, SingleCompletionHandler {
5051
constructor(options: ApiHandlerOptions) {
5152
this.options = options
5253

53-
// Only include credentials if they actually exist
5454
const clientConfig: BedrockRuntimeClientConfig = {
5555
region: this.options.awsRegion || "us-east-1",
5656
}
5757

58-
if (this.options.awsAccessKey && this.options.awsSecretKey) {
59-
// Create credentials object with all properties at once
58+
if (this.options.awsUseProfile && this.options.awsProfile) {
59+
// Use profile-based credentials if enabled and profile is set
60+
clientConfig.credentials = fromIni({
61+
profile: this.options.awsProfile,
62+
})
63+
} else if (this.options.awsAccessKey && this.options.awsSecretKey) {
64+
// Use direct credentials if provided
6065
clientConfig.credentials = {
6166
accessKeyId: this.options.awsAccessKey,
6267
secretAccessKey: this.options.awsSecretKey,

src/core/webview/ClineProvider.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ type GlobalStateKey =
6969
| "glamaModelInfo"
7070
| "awsRegion"
7171
| "awsUseCrossRegionInference"
72+
| "awsProfile"
73+
| "awsUseProfile"
7274
| "vertexProjectId"
7375
| "vertexRegion"
7476
| "lastShownAnnouncementId"
@@ -1264,6 +1266,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
12641266
awsSessionToken,
12651267
awsRegion,
12661268
awsUseCrossRegionInference,
1269+
awsProfile,
1270+
awsUseProfile,
12671271
vertexProjectId,
12681272
vertexRegion,
12691273
openAiBaseUrl,
@@ -1299,6 +1303,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
12991303
await this.storeSecret("awsSessionToken", awsSessionToken)
13001304
await this.updateGlobalState("awsRegion", awsRegion)
13011305
await this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference)
1306+
await this.updateGlobalState("awsProfile", awsProfile)
1307+
await this.updateGlobalState("awsUseProfile", awsUseProfile)
13021308
await this.updateGlobalState("vertexProjectId", vertexProjectId)
13031309
await this.updateGlobalState("vertexRegion", vertexRegion)
13041310
await this.updateGlobalState("openAiBaseUrl", openAiBaseUrl)
@@ -1919,6 +1925,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
19191925
awsSessionToken,
19201926
awsRegion,
19211927
awsUseCrossRegionInference,
1928+
awsProfile,
1929+
awsUseProfile,
19221930
vertexProjectId,
19231931
vertexRegion,
19241932
openAiBaseUrl,
@@ -1985,6 +1993,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
19851993
this.getSecret("awsSessionToken") as Promise<string | undefined>,
19861994
this.getGlobalState("awsRegion") as Promise<string | undefined>,
19871995
this.getGlobalState("awsUseCrossRegionInference") as Promise<boolean | undefined>,
1996+
this.getGlobalState("awsProfile") as Promise<string | undefined>,
1997+
this.getGlobalState("awsUseProfile") as Promise<boolean | undefined>,
19881998
this.getGlobalState("vertexProjectId") as Promise<string | undefined>,
19891999
this.getGlobalState("vertexRegion") as Promise<string | undefined>,
19902000
this.getGlobalState("openAiBaseUrl") as Promise<string | undefined>,
@@ -2068,6 +2078,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
20682078
awsSessionToken,
20692079
awsRegion,
20702080
awsUseCrossRegionInference,
2081+
awsProfile,
2082+
awsUseProfile,
20712083
vertexProjectId,
20722084
vertexRegion,
20732085
openAiBaseUrl,

src/shared/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export interface ApiHandlerOptions {
3333
awsUseCrossRegionInference?: boolean
3434
awsUsePromptCache?: boolean
3535
awspromptCacheId?: string
36+
awsProfile?: string
37+
awsUseProfile?: boolean
3638
vertexProjectId?: string
3739
vertexRegion?: string
3840
openAiBaseUrl?: string

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -340,30 +340,56 @@ const ApiOptions = ({ apiErrorMessage, modelIdErrorMessage }: ApiOptionsProps) =
340340

341341
{selectedProvider === "bedrock" && (
342342
<div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
343-
<VSCodeTextField
344-
value={apiConfiguration?.awsAccessKey || ""}
345-
style={{ width: "100%" }}
346-
type="password"
347-
onInput={handleInputChange("awsAccessKey")}
348-
placeholder="Enter Access Key...">
349-
<span style={{ fontWeight: 500 }}>AWS Access Key</span>
350-
</VSCodeTextField>
351-
<VSCodeTextField
352-
value={apiConfiguration?.awsSecretKey || ""}
353-
style={{ width: "100%" }}
354-
type="password"
355-
onInput={handleInputChange("awsSecretKey")}
356-
placeholder="Enter Secret Key...">
357-
<span style={{ fontWeight: 500 }}>AWS Secret Key</span>
358-
</VSCodeTextField>
359-
<VSCodeTextField
360-
value={apiConfiguration?.awsSessionToken || ""}
361-
style={{ width: "100%" }}
362-
type="password"
363-
onInput={handleInputChange("awsSessionToken")}
364-
placeholder="Enter Session Token...">
365-
<span style={{ fontWeight: 500 }}>AWS Session Token</span>
366-
</VSCodeTextField>
343+
<VSCodeRadioGroup
344+
value={apiConfiguration?.awsUseProfile ? "profile" : "credentials"}
345+
onChange={(e) => {
346+
const value = (e.target as HTMLInputElement)?.value
347+
const useProfile = value === "profile"
348+
handleInputChange("awsUseProfile")({
349+
target: { value: useProfile },
350+
})
351+
}}>
352+
<VSCodeRadio value="credentials">AWS Credentials</VSCodeRadio>
353+
<VSCodeRadio value="profile">AWS Profile</VSCodeRadio>
354+
</VSCodeRadioGroup>
355+
{/* AWS Profile Config Block */}
356+
{apiConfiguration?.awsUseProfile ? (
357+
<VSCodeTextField
358+
value={apiConfiguration?.awsProfile || ""}
359+
style={{ width: "100%" }}
360+
onInput={handleInputChange("awsProfile")}
361+
placeholder="Enter profile name">
362+
<span style={{ fontWeight: 500 }}>AWS Profile Name</span>
363+
</VSCodeTextField>
364+
) : (
365+
<>
366+
{/* AWS Credentials Config Block */}
367+
<VSCodeTextField
368+
value={apiConfiguration?.awsAccessKey || ""}
369+
style={{ width: "100%" }}
370+
type="password"
371+
onInput={handleInputChange("awsAccessKey")}
372+
placeholder="Enter Access Key...">
373+
<span style={{ fontWeight: 500 }}>AWS Access Key</span>
374+
</VSCodeTextField>
375+
<VSCodeTextField
376+
value={apiConfiguration?.awsSecretKey || ""}
377+
style={{ width: "100%" }}
378+
type="password"
379+
onInput={handleInputChange("awsSecretKey")}
380+
placeholder="Enter Secret Key...">
381+
<span style={{ fontWeight: 500 }}>AWS Secret Key</span>
382+
</VSCodeTextField>
383+
<VSCodeTextField
384+
value={apiConfiguration?.awsSessionToken || ""}
385+
style={{ width: "100%" }}
386+
type="password"
387+
onInput={handleInputChange("awsSessionToken")}
388+
placeholder="Enter Session Token...">
389+
<span style={{ fontWeight: 500 }}>AWS Session Token</span>
390+
</VSCodeTextField>
391+
</>
392+
)}
367393
<div className="dropdown-container">
368394
<label htmlFor="aws-region-dropdown">
369395
<span style={{ fontWeight: 500 }}>AWS Region</span>

0 commit comments

Comments
 (0)