Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export const SECRET_STATE_KEYS = [
"glamaApiKey",
"openRouterApiKey",
"awsAccessKey",
"awsApiKey",
"awsSecretKey",
"awsSessionToken",
"openAiApiKey",
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ const bedrockSchema = apiModelIdProviderModelSchema.extend({
awsUsePromptCache: z.boolean().optional(),
awsProfile: z.string().optional(),
awsUseProfile: z.boolean().optional(),
awsApiKey: z.string().optional(),
awsUseApiKey: z.boolean().optional(),
awsCustomArn: z.string().optional(),
awsModelContextWindow: z.number().optional(),
awsBedrockEndpointEnabled: z.boolean().optional(),
Expand Down
1,155 changes: 602 additions & 553 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

46 changes: 46 additions & 0 deletions src/api/providers/__tests__/bedrock-reasoning.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,5 +278,51 @@ describe("AwsBedrockHandler - Extended Thinking", () => {
expect(reasoningChunks[0].text).toBe("Let me think...")
expect(reasoningChunks[1].text).toBe(" about this problem.")
})

it("should support API key authentication", async () => {
handler = new AwsBedrockHandler({
apiProvider: "bedrock",
apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
awsRegion: "us-east-1",
awsUseApiKey: true,
awsApiKey: "test-api-key-token",
})

mockSend.mockResolvedValue({
stream: (async function* () {
yield { messageStart: { role: "assistant" } }
yield {
contentBlockStart: {
start: { text: "Hello from API key auth" },
contentBlockIndex: 0,
},
}
yield { metadata: { usage: { inputTokens: 100, outputTokens: 50 } } }
})(),
})

const messages = [{ role: "user" as const, content: "Test message" }]
const stream = handler.createMessage("System prompt", messages)

const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}

// Verify the client was created with API key token
expect(BedrockRuntimeClient).toHaveBeenCalledWith(
expect.objectContaining({
region: "us-east-1",
token: { token: "test-api-key-token" },
authSchemePreference: ["httpBearerAuth"],
}),
)

// Verify the stream worked correctly
expect(mockSend).toHaveBeenCalledTimes(1)
const textChunks = chunks.filter((c) => c.type === "text")
expect(textChunks).toHaveLength(1)
expect(textChunks[0].text).toBe("Hello from API key auth")
})
})
})
6 changes: 5 additions & 1 deletion src/api/providers/bedrock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,11 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
this.options.awsBedrockEndpointEnabled && { endpoint: this.options.awsBedrockEndpoint }),
}

if (this.options.awsUseProfile && this.options.awsProfile) {
if (this.options.awsUseApiKey && this.options.awsApiKey) {
// Use API key/token-based authentication if enabled and API key is set
clientConfig.token = { token: this.options.awsApiKey }
clientConfig.authSchemePreference = ["httpBearerAuth"] // Otherwise there's no end of credential problems.
} else if (this.options.awsUseProfile && this.options.awsProfile) {
// Use profile-based credentials if enabled and profile is set
clientConfig.credentials = fromIni({
profile: this.options.awsProfile,
Expand Down
4 changes: 2 additions & 2 deletions src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -408,8 +408,8 @@
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.37.0",
"@anthropic-ai/vertex-sdk": "^0.7.0",
"@aws-sdk/client-bedrock-runtime": "^3.779.0",
"@aws-sdk/credential-providers": "^3.806.0",
"@aws-sdk/client-bedrock-runtime": "^3.848.0",
"@aws-sdk/credential-providers": "^3.848.0",
"@google/genai": "^1.0.0",
"@lmstudio/sdk": "^1.1.1",
"@mistralai/mistralai": "^1.3.6",
Expand Down
54 changes: 43 additions & 11 deletions webview-ui/src/components/settings/providers/Bedrock.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useCallback, useState, useEffect } from "react"
import { Checkbox } from "vscrui"
import { VSCodeTextField, VSCodeRadio, VSCodeRadioGroup } from "@vscode/webview-ui-toolkit/react"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"

import { type ProviderSettings, type ModelInfo, BEDROCK_REGIONS } from "@roo-code/types"

Expand Down Expand Up @@ -37,19 +37,51 @@ export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedMo

return (
<>
<VSCodeRadioGroup
value={apiConfiguration?.awsUseProfile ? "profile" : "credentials"}
onChange={handleInputChange(
"awsUseProfile",
(e) => (e.target as HTMLInputElement).value === "profile",
)}>
<VSCodeRadio value="credentials">{t("settings:providers.awsCredentials")}</VSCodeRadio>
<VSCodeRadio value="profile">{t("settings:providers.awsProfile")}</VSCodeRadio>
</VSCodeRadioGroup>
<div>
<label className="block font-medium mb-1">Authentication Method</label>
<Select
value={
apiConfiguration?.awsUseApiKey
? "apikey"
: apiConfiguration?.awsUseProfile
? "profile"
: "credentials"
}
onValueChange={(value) => {
if (value === "apikey") {
setApiConfigurationField("awsUseApiKey", true)
setApiConfigurationField("awsUseProfile", false)
} else if (value === "profile") {
setApiConfigurationField("awsUseApiKey", false)
setApiConfigurationField("awsUseProfile", true)
} else {
setApiConfigurationField("awsUseApiKey", false)
setApiConfigurationField("awsUseProfile", false)
}
}}>
<SelectTrigger className="w-full">
<SelectValue placeholder={t("settings:common.select")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="credentials">{t("settings:providers.awsCredentials")}</SelectItem>
<SelectItem value="profile">{t("settings:providers.awsProfile")}</SelectItem>
<SelectItem value="apikey">{t("settings:providers.awsApiKey")}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="text-sm text-vscode-descriptionForeground -mt-3">
{t("settings:providers.apiKeyStorageNotice")}
</div>
{apiConfiguration?.awsUseProfile ? (
{apiConfiguration?.awsUseApiKey ? (
<VSCodeTextField
value={apiConfiguration?.awsApiKey || ""}
type="password"
onInput={handleInputChange("awsApiKey")}
placeholder={t("settings:placeholders.apiKey")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.awsApiKey")}</label>
</VSCodeTextField>
) : apiConfiguration?.awsUseProfile ? (
<VSCodeTextField
value={apiConfiguration?.awsProfile || ""}
onInput={handleInputChange("awsProfile")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,16 @@ vi.mock("@src/i18n/TranslationContext", () => ({

// Mock the UI components
vi.mock("@src/components/ui", () => ({
Select: ({ children }: any) => <div>{children}</div>,
SelectContent: ({ children }: any) => <div>{children}</div>,
SelectItem: () => <div>Item</div>,
SelectTrigger: ({ children }: any) => <div>{children}</div>,
SelectValue: () => <div>Value</div>,
Select: ({ children, value, onValueChange }: any) => (
<select value={value} onChange={(e) => onValueChange && onValueChange(e.target.value)}>
{children}
</select>
),
SelectContent: ({ children }: any) => <>{children}</>,
SelectItem: ({ children, value }: any) => <option value={value}>{children}</option>,
SelectTrigger: ({ children }: any) => <>{children}</>,
SelectValue: () => null,
StandardTooltip: ({ children }: any) => <div>{children}</div>,
}))

// Mock the constants
Expand Down Expand Up @@ -424,5 +429,126 @@ describe("Bedrock Component", () => {
expect(screen.getByTestId("vpc-endpoint-input")).toBeInTheDocument()
expect(screen.getByTestId("vpc-endpoint-input")).toHaveValue("https://updated-endpoint.aws.com")
})

// Test Scenario 6: Authentication Method Selection Tests
describe("Authentication Method Selection", () => {
it("should display credentials option as selected when neither awsUseProfile nor awsUseApiKey is true", () => {
const apiConfiguration: Partial<ProviderSettings> = {
awsUseProfile: false,
awsUseApiKey: false,
}

render(
<Bedrock
apiConfiguration={apiConfiguration as ProviderSettings}
setApiConfigurationField={mockSetApiConfigurationField}
/>,
)

// Find the first select element (authentication method)
const selectInputs = screen.getAllByRole("combobox")
const authSelect = selectInputs[0] as HTMLSelectElement
expect(authSelect).toHaveValue("credentials")
})

it("should display profile option as selected when awsUseProfile is true", () => {
const apiConfiguration: Partial<ProviderSettings> = {
awsUseProfile: true,
awsUseApiKey: false,
}

render(
<Bedrock
apiConfiguration={apiConfiguration as ProviderSettings}
setApiConfigurationField={mockSetApiConfigurationField}
/>,
)

const selectInputs = screen.getAllByRole("combobox")
const authSelect = selectInputs[0] as HTMLSelectElement
expect(authSelect).toHaveValue("profile")
})

it("should display apikey option as selected when awsUseApiKey is true", () => {
const apiConfiguration: Partial<ProviderSettings> = {
awsUseProfile: false,
awsUseApiKey: true,
}

render(
<Bedrock
apiConfiguration={apiConfiguration as ProviderSettings}
setApiConfigurationField={mockSetApiConfigurationField}
/>,
)

const selectInputs = screen.getAllByRole("combobox")
const authSelect = selectInputs[0] as HTMLSelectElement
expect(authSelect).toHaveValue("apikey")
})

it("should call setApiConfigurationField correctly when switching to profile", () => {
const apiConfiguration: Partial<ProviderSettings> = {
awsUseProfile: false,
awsUseApiKey: false,
}

render(
<Bedrock
apiConfiguration={apiConfiguration as ProviderSettings}
setApiConfigurationField={mockSetApiConfigurationField}
/>,
)

const selectInputs = screen.getAllByRole("combobox")
const authSelect = selectInputs[0] as HTMLSelectElement
fireEvent.change(authSelect, { target: { value: "profile" } })

expect(mockSetApiConfigurationField).toHaveBeenCalledWith("awsUseApiKey", false)
expect(mockSetApiConfigurationField).toHaveBeenCalledWith("awsUseProfile", true)
})

it("should call setApiConfigurationField correctly when switching to apikey", () => {
const apiConfiguration: Partial<ProviderSettings> = {
awsUseProfile: false,
awsUseApiKey: false,
}

render(
<Bedrock
apiConfiguration={apiConfiguration as ProviderSettings}
setApiConfigurationField={mockSetApiConfigurationField}
/>,
)

const selectInputs = screen.getAllByRole("combobox")
const authSelect = selectInputs[0] as HTMLSelectElement
fireEvent.change(authSelect, { target: { value: "apikey" } })

expect(mockSetApiConfigurationField).toHaveBeenCalledWith("awsUseApiKey", true)
expect(mockSetApiConfigurationField).toHaveBeenCalledWith("awsUseProfile", false)
})

it("should call setApiConfigurationField correctly when switching to credentials", () => {
const apiConfiguration: Partial<ProviderSettings> = {
awsUseProfile: true,
awsUseApiKey: false,
}

render(
<Bedrock
apiConfiguration={apiConfiguration as ProviderSettings}
setApiConfigurationField={mockSetApiConfigurationField}
/>,
)

const selectInputs = screen.getAllByRole("combobox")
const authSelect = selectInputs[0] as HTMLSelectElement
fireEvent.change(authSelect, { target: { value: "credentials" } })

expect(mockSetApiConfigurationField).toHaveBeenCalledWith("awsUseApiKey", false)
expect(mockSetApiConfigurationField).toHaveBeenCalledWith("awsUseProfile", false)
})
})
})
})
1 change: 1 addition & 0 deletions webview-ui/src/i18n/locales/ca/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions webview-ui/src/i18n/locales/de/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions webview-ui/src/i18n/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@
"litellmBaseUrl": "LiteLLM Base URL",
"awsCredentials": "AWS Credentials",
"awsProfile": "AWS Profile",
"awsApiKey": "AWS Bedrock API Key",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe Amazon would like us to refer to it as "Amazon Bedrock"

"awsProfileName": "AWS Profile Name",
"awsAccessKey": "AWS Access Key",
"awsSecretKey": "AWS Secret Key",
Expand Down
1 change: 1 addition & 0 deletions webview-ui/src/i18n/locales/es/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions webview-ui/src/i18n/locales/fr/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions webview-ui/src/i18n/locales/hi/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions webview-ui/src/i18n/locales/id/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions webview-ui/src/i18n/locales/it/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions webview-ui/src/i18n/locales/ja/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions webview-ui/src/i18n/locales/ko/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions webview-ui/src/i18n/locales/nl/settings.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading