mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
fix: implement backend support for custom AWS regions
- Backend now properly uses awsCustomRegion when awsRegion is 'custom' - Added i18n translations for custom region UI elements - Added validation for AWS region format (e.g., us-west-3) - Improved state management to preserve custom region value when switching - Added comprehensive tests for custom region functionality
This commit is contained in:
parent
24d2adb298
commit
9008f16cca
5 changed files with 380 additions and 13 deletions
107
src/api/providers/__tests__/bedrock-custom-region.spec.ts
Normal file
107
src/api/providers/__tests__/bedrock-custom-region.spec.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { AwsBedrockHandler } from "../bedrock"
|
||||
import { BedrockRuntimeClient } from "@aws-sdk/client-bedrock-runtime"
|
||||
|
||||
// Mock the AWS SDK
|
||||
vi.mock("@aws-sdk/client-bedrock-runtime", () => ({
|
||||
BedrockRuntimeClient: vi.fn().mockImplementation((config) => ({
|
||||
config,
|
||||
send: vi.fn(),
|
||||
})),
|
||||
ConverseCommand: vi.fn(),
|
||||
ConverseStreamCommand: vi.fn(),
|
||||
}))
|
||||
|
||||
describe("AwsBedrockHandler - Custom Region Support", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should use custom region when awsRegion is 'custom' and awsCustomRegion is provided", () => {
|
||||
const handler = new AwsBedrockHandler({
|
||||
apiProvider: "bedrock",
|
||||
apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0",
|
||||
awsAccessKey: "test-access-key",
|
||||
awsSecretKey: "test-secret-key",
|
||||
awsRegion: "custom",
|
||||
awsCustomRegion: "us-west-3",
|
||||
})
|
||||
|
||||
// Get the mock instance to check the config
|
||||
const mockClientInstance = vi.mocked(BedrockRuntimeClient).mock.results[0]?.value
|
||||
expect(mockClientInstance.config.region).toBe("us-west-3")
|
||||
})
|
||||
|
||||
it("should use standard region when awsRegion is not 'custom'", () => {
|
||||
const handler = new AwsBedrockHandler({
|
||||
apiProvider: "bedrock",
|
||||
apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0",
|
||||
awsAccessKey: "test-access-key",
|
||||
awsSecretKey: "test-secret-key",
|
||||
awsRegion: "us-east-1",
|
||||
awsCustomRegion: "us-west-3", // This should be ignored
|
||||
})
|
||||
|
||||
// Get the mock instance to check the config
|
||||
const mockClientInstance = vi.mocked(BedrockRuntimeClient).mock.results[0]?.value
|
||||
expect(mockClientInstance.config.region).toBe("us-east-1")
|
||||
})
|
||||
|
||||
it("should use awsRegion when awsCustomRegion is not provided", () => {
|
||||
const handler = new AwsBedrockHandler({
|
||||
apiProvider: "bedrock",
|
||||
apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0",
|
||||
awsAccessKey: "test-access-key",
|
||||
awsSecretKey: "test-secret-key",
|
||||
awsRegion: "custom",
|
||||
// awsCustomRegion is not provided
|
||||
})
|
||||
|
||||
// Get the mock instance to check the config
|
||||
const mockClientInstance = vi.mocked(BedrockRuntimeClient).mock.results[0]?.value
|
||||
expect(mockClientInstance.config.region).toBe("custom")
|
||||
})
|
||||
|
||||
it("should use custom region for cross-region inference prefix calculation", () => {
|
||||
const handler = new AwsBedrockHandler({
|
||||
apiProvider: "bedrock",
|
||||
apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0",
|
||||
awsAccessKey: "test-access-key",
|
||||
awsSecretKey: "test-secret-key",
|
||||
awsRegion: "custom",
|
||||
awsCustomRegion: "us-west-3",
|
||||
awsUseCrossRegionInference: true,
|
||||
})
|
||||
|
||||
const model = handler.getModel()
|
||||
// Should have the us. prefix for us-west-3
|
||||
expect(model.id).toContain("us.")
|
||||
})
|
||||
|
||||
it("should handle custom regions with different prefixes for cross-region inference", () => {
|
||||
const testCases = [
|
||||
{ customRegion: "eu-central-3", expectedPrefix: "eu." },
|
||||
{ customRegion: "ap-southeast-4", expectedPrefix: "apac." },
|
||||
{ customRegion: "ca-west-1", expectedPrefix: "ca." },
|
||||
{ customRegion: "sa-east-2", expectedPrefix: "sa." },
|
||||
{ customRegion: "us-gov-west-2", expectedPrefix: "ug." },
|
||||
]
|
||||
|
||||
for (const { customRegion, expectedPrefix } of testCases) {
|
||||
vi.clearAllMocks()
|
||||
|
||||
const handler = new AwsBedrockHandler({
|
||||
apiProvider: "bedrock",
|
||||
apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0",
|
||||
awsAccessKey: "test-access-key",
|
||||
awsSecretKey: "test-secret-key",
|
||||
awsRegion: "custom",
|
||||
awsCustomRegion: customRegion,
|
||||
awsUseCrossRegionInference: true,
|
||||
})
|
||||
|
||||
const model = handler.getModel()
|
||||
expect(model.id).toContain(expectedPrefix)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -169,7 +169,11 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
|
|||
constructor(options: ProviderSettings) {
|
||||
super()
|
||||
this.options = options
|
||||
let region = this.options.awsRegion
|
||||
// Use custom region if awsRegion is "custom"
|
||||
let region =
|
||||
this.options.awsRegion === "custom" && this.options.awsCustomRegion
|
||||
? this.options.awsCustomRegion
|
||||
: this.options.awsRegion
|
||||
|
||||
// process the various user input options, be opinionated about the intent of the options
|
||||
// and determine the model to use during inference and for cost calculations
|
||||
|
|
@ -216,7 +220,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
|
|||
this.costModelConfig = this.getModel()
|
||||
|
||||
const clientConfig: BedrockRuntimeClientConfig = {
|
||||
region: this.options.awsRegion,
|
||||
region: region, // Use the resolved region (either standard or custom)
|
||||
// Add the endpoint configuration when specified and enabled
|
||||
...(this.options.awsBedrockEndpoint &&
|
||||
this.options.awsBedrockEndpointEnabled && { endpoint: this.options.awsBedrockEndpoint }),
|
||||
|
|
@ -943,10 +947,18 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
|
|||
modelConfig = this.getModelById(this.options.apiModelId as string)
|
||||
|
||||
// Add cross-region inference prefix if enabled
|
||||
if (this.options.awsUseCrossRegionInference && this.options.awsRegion) {
|
||||
const prefix = AwsBedrockHandler.getPrefixForRegion(this.options.awsRegion)
|
||||
if (prefix) {
|
||||
modelConfig.id = `${prefix}${modelConfig.id}`
|
||||
if (this.options.awsUseCrossRegionInference) {
|
||||
// Use custom region if awsRegion is "custom"
|
||||
const regionToUse =
|
||||
this.options.awsRegion === "custom" && this.options.awsCustomRegion
|
||||
? this.options.awsCustomRegion
|
||||
: this.options.awsRegion
|
||||
|
||||
if (regionToUse) {
|
||||
const prefix = AwsBedrockHandler.getPrefixForRegion(regionToUse)
|
||||
if (prefix) {
|
||||
modelConfig.id = `${prefix}${modelConfig.id}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Standard
|
|||
|
||||
import { inputEventTransform, noTransform } from "../transforms"
|
||||
|
||||
// AWS region format validation regex
|
||||
const AWS_REGION_REGEX = /^[a-z]{2,}-[a-z]+-\d+$/
|
||||
|
||||
type BedrockProps = {
|
||||
apiConfiguration: ProviderSettings
|
||||
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
|
||||
|
|
@ -19,6 +22,7 @@ export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedMo
|
|||
const { t } = useAppTranslation()
|
||||
const [awsEndpointSelected, setAwsEndpointSelected] = useState(!!apiConfiguration?.awsBedrockEndpointEnabled)
|
||||
const [customRegionSelected, setCustomRegionSelected] = useState(apiConfiguration?.awsRegion === "custom")
|
||||
const [customRegionError, setCustomRegionError] = useState<string | null>(null)
|
||||
|
||||
// Update the endpoint enabled state when the configuration changes
|
||||
useEffect(() => {
|
||||
|
|
@ -41,6 +45,34 @@ export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedMo
|
|||
[setApiConfigurationField],
|
||||
)
|
||||
|
||||
// Validate custom region format
|
||||
const validateCustomRegion = useCallback(
|
||||
(value: string) => {
|
||||
if (!value && customRegionSelected) {
|
||||
setCustomRegionError(t("settings:providers.awsCustomRegion.validation.required"))
|
||||
return false
|
||||
}
|
||||
if (value && !AWS_REGION_REGEX.test(value)) {
|
||||
setCustomRegionError(t("settings:providers.awsCustomRegion.validation.format"))
|
||||
return false
|
||||
}
|
||||
setCustomRegionError(null)
|
||||
return true
|
||||
},
|
||||
[customRegionSelected, t],
|
||||
)
|
||||
|
||||
// Handle custom region input change with validation
|
||||
const handleCustomRegionChange = useCallback(
|
||||
(event: Event | React.FormEvent<HTMLElement>) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
const value = target.value
|
||||
validateCustomRegion(value)
|
||||
setApiConfigurationField("awsCustomRegion", value)
|
||||
},
|
||||
[setApiConfigurationField, validateCustomRegion],
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<VSCodeRadioGroup
|
||||
|
|
@ -98,9 +130,13 @@ export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedMo
|
|||
onValueChange={(value) => {
|
||||
setApiConfigurationField("awsRegion", value)
|
||||
setCustomRegionSelected(value === "custom")
|
||||
// Clear custom region when switching to a standard region
|
||||
if (value !== "custom") {
|
||||
setApiConfigurationField("awsCustomRegion", "")
|
||||
// Don't clear custom region when switching away - preserve the value
|
||||
if (value === "custom" && apiConfiguration?.awsCustomRegion) {
|
||||
// Validate the existing custom region value
|
||||
validateCustomRegion(apiConfiguration.awsCustomRegion)
|
||||
} else {
|
||||
// Clear validation error when not using custom region
|
||||
setCustomRegionError(null)
|
||||
}
|
||||
}}>
|
||||
<SelectTrigger className="w-full">
|
||||
|
|
@ -120,10 +156,14 @@ export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedMo
|
|||
<VSCodeTextField
|
||||
value={apiConfiguration?.awsCustomRegion || ""}
|
||||
style={{ width: "100%", marginTop: 3, marginBottom: 5 }}
|
||||
onInput={handleInputChange("awsCustomRegion")}
|
||||
placeholder={t("settings:placeholders.customRegion")}
|
||||
onInput={handleCustomRegionChange}
|
||||
placeholder={t("settings:providers.awsCustomRegion.placeholder")}
|
||||
data-testid="custom-region-input"
|
||||
className={customRegionError ? "error" : ""}
|
||||
/>
|
||||
{customRegionError && (
|
||||
<div className="text-sm text-vscode-errorForeground ml-6 mt-1 mb-2">{customRegionError}</div>
|
||||
)}
|
||||
<div className="text-sm text-vscode-descriptionForeground ml-6 mt-1 mb-3">
|
||||
{t("settings:providers.awsCustomRegion.examples")}
|
||||
<div className="ml-2">• us-west-3</div>
|
||||
|
|
|
|||
|
|
@ -61,11 +61,18 @@ vi.mock("@src/i18n/TranslationContext", () => ({
|
|||
|
||||
// Mock the UI components
|
||||
vi.mock("@src/components/ui", () => ({
|
||||
Select: ({ children }: any) => <div>{children}</div>,
|
||||
Select: ({ children, onValueChange }: any) => {
|
||||
// Store the onValueChange callback on the window for testing
|
||||
if (typeof window !== "undefined") {
|
||||
;(window as any).__selectOnValueChange = onValueChange
|
||||
}
|
||||
return <div data-testid="select-component">{children}</div>
|
||||
},
|
||||
SelectContent: ({ children }: any) => <div>{children}</div>,
|
||||
SelectItem: () => <div>Item</div>,
|
||||
SelectTrigger: ({ children }: any) => <div>{children}</div>,
|
||||
SelectTrigger: ({ children }: any) => <div role="combobox">{children}</div>,
|
||||
SelectValue: () => <div>Value</div>,
|
||||
StandardTooltip: ({ children }: any) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
// Mock the constants
|
||||
|
|
@ -636,4 +643,196 @@ describe("Bedrock Component", () => {
|
|||
expect(screen.getByText("settings:providers.awsCustomRegion.examples")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Test Scenario 7: Custom Region Validation Tests
|
||||
describe("Custom Region Validation", () => {
|
||||
it("should show validation error when custom region is empty", () => {
|
||||
const apiConfiguration: Partial<ProviderSettings> = {
|
||||
awsRegion: "custom",
|
||||
awsCustomRegion: "",
|
||||
awsUseProfile: true,
|
||||
}
|
||||
|
||||
render(
|
||||
<Bedrock
|
||||
apiConfiguration={apiConfiguration as ProviderSettings}
|
||||
setApiConfigurationField={mockSetApiConfigurationField}
|
||||
/>,
|
||||
)
|
||||
|
||||
// The custom region input should be visible
|
||||
const customRegionInput = screen.getByTestId("custom-region-input")
|
||||
expect(customRegionInput).toBeInTheDocument()
|
||||
|
||||
// Trigger validation by changing input
|
||||
fireEvent.change(customRegionInput, { target: { value: "" } })
|
||||
|
||||
// Should show required validation error
|
||||
expect(screen.getByText("settings:providers.awsCustomRegion.validation.required")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should show validation error for invalid region format", () => {
|
||||
const apiConfiguration: Partial<ProviderSettings> = {
|
||||
awsRegion: "custom",
|
||||
awsCustomRegion: "",
|
||||
awsUseProfile: true,
|
||||
}
|
||||
|
||||
render(
|
||||
<Bedrock
|
||||
apiConfiguration={apiConfiguration as ProviderSettings}
|
||||
setApiConfigurationField={mockSetApiConfigurationField}
|
||||
/>,
|
||||
)
|
||||
|
||||
const customRegionInput = screen.getByTestId("custom-region-input")
|
||||
|
||||
// Test various invalid formats
|
||||
const invalidRegions = [
|
||||
"invalid-region",
|
||||
"us-west",
|
||||
"us-3",
|
||||
"uswest3",
|
||||
"US-WEST-3",
|
||||
"us-west-three",
|
||||
"123-west-3",
|
||||
]
|
||||
|
||||
for (const invalidRegion of invalidRegions) {
|
||||
fireEvent.change(customRegionInput, { target: { value: invalidRegion } })
|
||||
|
||||
// Should show format validation error
|
||||
expect(screen.getByText("settings:providers.awsCustomRegion.validation.format")).toBeInTheDocument()
|
||||
}
|
||||
})
|
||||
|
||||
it("should accept valid region formats", () => {
|
||||
const apiConfiguration: Partial<ProviderSettings> = {
|
||||
awsRegion: "custom",
|
||||
awsCustomRegion: "",
|
||||
awsUseProfile: true,
|
||||
}
|
||||
|
||||
render(
|
||||
<Bedrock
|
||||
apiConfiguration={apiConfiguration as ProviderSettings}
|
||||
setApiConfigurationField={mockSetApiConfigurationField}
|
||||
/>,
|
||||
)
|
||||
|
||||
const customRegionInput = screen.getByTestId("custom-region-input")
|
||||
|
||||
// Test various valid formats
|
||||
const validRegions = [
|
||||
"us-west-3",
|
||||
"eu-central-2",
|
||||
"ap-southeast-4",
|
||||
"sa-east-2",
|
||||
"ca-west-1",
|
||||
"me-south-2",
|
||||
"af-south-1",
|
||||
]
|
||||
|
||||
for (const validRegion of validRegions) {
|
||||
fireEvent.change(customRegionInput, { target: { value: validRegion } })
|
||||
|
||||
// Should update the field
|
||||
expect(mockSetApiConfigurationField).toHaveBeenCalledWith("awsCustomRegion", validRegion)
|
||||
|
||||
// Should not show any error
|
||||
expect(
|
||||
screen.queryByText("settings:providers.awsCustomRegion.validation.format"),
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByText("settings:providers.awsCustomRegion.validation.required"),
|
||||
).not.toBeInTheDocument()
|
||||
}
|
||||
})
|
||||
|
||||
it("should preserve custom region value when switching between regions", () => {
|
||||
const apiConfiguration: Partial<ProviderSettings> = {
|
||||
awsRegion: "custom",
|
||||
awsCustomRegion: "us-west-3",
|
||||
awsUseProfile: true,
|
||||
}
|
||||
|
||||
const { rerender } = render(
|
||||
<Bedrock
|
||||
apiConfiguration={apiConfiguration as ProviderSettings}
|
||||
setApiConfigurationField={mockSetApiConfigurationField}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Custom region input should be visible with the value
|
||||
let customRegionInput = screen.getByTestId("custom-region-input") as HTMLInputElement
|
||||
expect(customRegionInput.value).toBe("us-west-3")
|
||||
|
||||
// Switch to a standard region by calling the onValueChange directly
|
||||
if ((window as any).__selectOnValueChange) {
|
||||
;(window as any).__selectOnValueChange("us-east-1")
|
||||
}
|
||||
|
||||
// Update the configuration
|
||||
apiConfiguration.awsRegion = "us-east-1"
|
||||
rerender(
|
||||
<Bedrock
|
||||
apiConfiguration={apiConfiguration as ProviderSettings}
|
||||
setApiConfigurationField={mockSetApiConfigurationField}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Custom region input should be hidden
|
||||
expect(screen.queryByTestId("custom-region-input")).not.toBeInTheDocument()
|
||||
|
||||
// Switch back to custom region
|
||||
if ((window as any).__selectOnValueChange) {
|
||||
;(window as any).__selectOnValueChange("custom")
|
||||
}
|
||||
|
||||
// Update the configuration
|
||||
apiConfiguration.awsRegion = "custom"
|
||||
rerender(
|
||||
<Bedrock
|
||||
apiConfiguration={apiConfiguration as ProviderSettings}
|
||||
setApiConfigurationField={mockSetApiConfigurationField}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Custom region input should be visible again with the preserved value
|
||||
customRegionInput = screen.getByTestId("custom-region-input") as HTMLInputElement
|
||||
expect(customRegionInput.value).toBe("us-west-3")
|
||||
})
|
||||
|
||||
it("should validate existing custom region when switching back to custom", () => {
|
||||
const apiConfiguration: Partial<ProviderSettings> = {
|
||||
awsRegion: "us-east-1",
|
||||
awsCustomRegion: "invalid-region", // Invalid format
|
||||
awsUseProfile: true,
|
||||
}
|
||||
|
||||
const { rerender } = render(
|
||||
<Bedrock
|
||||
apiConfiguration={apiConfiguration as ProviderSettings}
|
||||
setApiConfigurationField={mockSetApiConfigurationField}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Switch to custom region
|
||||
if ((window as any).__selectOnValueChange) {
|
||||
;(window as any).__selectOnValueChange("custom")
|
||||
}
|
||||
|
||||
// Update the configuration
|
||||
apiConfiguration.awsRegion = "custom"
|
||||
rerender(
|
||||
<Bedrock
|
||||
apiConfiguration={apiConfiguration as ProviderSettings}
|
||||
setApiConfigurationField={mockSetApiConfigurationField}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Should show validation error for the existing invalid value
|
||||
expect(screen.getByText("settings:providers.awsCustomRegion.validation.format")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -273,6 +273,14 @@
|
|||
"awsSessionToken": "AWS Session Token",
|
||||
"awsRegion": "AWS Region",
|
||||
"awsCrossRegion": "Use cross-region inference",
|
||||
"awsCustomRegion": {
|
||||
"placeholder": "Enter custom region (e.g., us-west-3)",
|
||||
"examples": "Examples of new or custom regions:",
|
||||
"validation": {
|
||||
"required": "Custom region is required when 'Custom region...' is selected",
|
||||
"format": "Region must be in format: region-direction-number (e.g., us-west-3, eu-central-2)"
|
||||
}
|
||||
},
|
||||
"awsBedrockVpc": {
|
||||
"useCustomVpcEndpoint": "Use custom VPC endpoint",
|
||||
"vpcEndpointUrlPlaceholder": "Enter VPC Endpoint URL (optional)",
|
||||
|
|
@ -686,6 +694,7 @@
|
|||
"keyFilePath": "Enter Key File Path...",
|
||||
"projectId": "Enter Project ID...",
|
||||
"customArn": "Enter ARN (e.g. arn:aws:bedrock:us-east-1:123456789012:foundation-model/my-model)",
|
||||
"customRegion": "Enter custom region (e.g., us-west-3)",
|
||||
"baseUrl": "Enter base URL...",
|
||||
"modelId": {
|
||||
"lmStudio": "e.g. meta-llama-3.1-8b-instruct",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue