Skip to content

Commit 9008f16

Browse files
committed
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
1 parent 24d2adb commit 9008f16

File tree

5 files changed

+380
-13
lines changed

5 files changed

+380
-13
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest"
2+
import { AwsBedrockHandler } from "../bedrock"
3+
import { BedrockRuntimeClient } from "@aws-sdk/client-bedrock-runtime"
4+
5+
// Mock the AWS SDK
6+
vi.mock("@aws-sdk/client-bedrock-runtime", () => ({
7+
BedrockRuntimeClient: vi.fn().mockImplementation((config) => ({
8+
config,
9+
send: vi.fn(),
10+
})),
11+
ConverseCommand: vi.fn(),
12+
ConverseStreamCommand: vi.fn(),
13+
}))
14+
15+
describe("AwsBedrockHandler - Custom Region Support", () => {
16+
beforeEach(() => {
17+
vi.clearAllMocks()
18+
})
19+
20+
it("should use custom region when awsRegion is 'custom' and awsCustomRegion is provided", () => {
21+
const handler = new AwsBedrockHandler({
22+
apiProvider: "bedrock",
23+
apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0",
24+
awsAccessKey: "test-access-key",
25+
awsSecretKey: "test-secret-key",
26+
awsRegion: "custom",
27+
awsCustomRegion: "us-west-3",
28+
})
29+
30+
// Get the mock instance to check the config
31+
const mockClientInstance = vi.mocked(BedrockRuntimeClient).mock.results[0]?.value
32+
expect(mockClientInstance.config.region).toBe("us-west-3")
33+
})
34+
35+
it("should use standard region when awsRegion is not 'custom'", () => {
36+
const handler = new AwsBedrockHandler({
37+
apiProvider: "bedrock",
38+
apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0",
39+
awsAccessKey: "test-access-key",
40+
awsSecretKey: "test-secret-key",
41+
awsRegion: "us-east-1",
42+
awsCustomRegion: "us-west-3", // This should be ignored
43+
})
44+
45+
// Get the mock instance to check the config
46+
const mockClientInstance = vi.mocked(BedrockRuntimeClient).mock.results[0]?.value
47+
expect(mockClientInstance.config.region).toBe("us-east-1")
48+
})
49+
50+
it("should use awsRegion when awsCustomRegion is not provided", () => {
51+
const handler = new AwsBedrockHandler({
52+
apiProvider: "bedrock",
53+
apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0",
54+
awsAccessKey: "test-access-key",
55+
awsSecretKey: "test-secret-key",
56+
awsRegion: "custom",
57+
// awsCustomRegion is not provided
58+
})
59+
60+
// Get the mock instance to check the config
61+
const mockClientInstance = vi.mocked(BedrockRuntimeClient).mock.results[0]?.value
62+
expect(mockClientInstance.config.region).toBe("custom")
63+
})
64+
65+
it("should use custom region for cross-region inference prefix calculation", () => {
66+
const handler = new AwsBedrockHandler({
67+
apiProvider: "bedrock",
68+
apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0",
69+
awsAccessKey: "test-access-key",
70+
awsSecretKey: "test-secret-key",
71+
awsRegion: "custom",
72+
awsCustomRegion: "us-west-3",
73+
awsUseCrossRegionInference: true,
74+
})
75+
76+
const model = handler.getModel()
77+
// Should have the us. prefix for us-west-3
78+
expect(model.id).toContain("us.")
79+
})
80+
81+
it("should handle custom regions with different prefixes for cross-region inference", () => {
82+
const testCases = [
83+
{ customRegion: "eu-central-3", expectedPrefix: "eu." },
84+
{ customRegion: "ap-southeast-4", expectedPrefix: "apac." },
85+
{ customRegion: "ca-west-1", expectedPrefix: "ca." },
86+
{ customRegion: "sa-east-2", expectedPrefix: "sa." },
87+
{ customRegion: "us-gov-west-2", expectedPrefix: "ug." },
88+
]
89+
90+
for (const { customRegion, expectedPrefix } of testCases) {
91+
vi.clearAllMocks()
92+
93+
const handler = new AwsBedrockHandler({
94+
apiProvider: "bedrock",
95+
apiModelId: "anthropic.claude-3-sonnet-20240229-v1:0",
96+
awsAccessKey: "test-access-key",
97+
awsSecretKey: "test-secret-key",
98+
awsRegion: "custom",
99+
awsCustomRegion: customRegion,
100+
awsUseCrossRegionInference: true,
101+
})
102+
103+
const model = handler.getModel()
104+
expect(model.id).toContain(expectedPrefix)
105+
}
106+
})
107+
})

src/api/providers/bedrock.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,11 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
169169
constructor(options: ProviderSettings) {
170170
super()
171171
this.options = options
172-
let region = this.options.awsRegion
172+
// Use custom region if awsRegion is "custom"
173+
let region =
174+
this.options.awsRegion === "custom" && this.options.awsCustomRegion
175+
? this.options.awsCustomRegion
176+
: this.options.awsRegion
173177

174178
// process the various user input options, be opinionated about the intent of the options
175179
// and determine the model to use during inference and for cost calculations
@@ -216,7 +220,7 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
216220
this.costModelConfig = this.getModel()
217221

218222
const clientConfig: BedrockRuntimeClientConfig = {
219-
region: this.options.awsRegion,
223+
region: region, // Use the resolved region (either standard or custom)
220224
// Add the endpoint configuration when specified and enabled
221225
...(this.options.awsBedrockEndpoint &&
222226
this.options.awsBedrockEndpointEnabled && { endpoint: this.options.awsBedrockEndpoint }),
@@ -943,10 +947,18 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
943947
modelConfig = this.getModelById(this.options.apiModelId as string)
944948

945949
// Add cross-region inference prefix if enabled
946-
if (this.options.awsUseCrossRegionInference && this.options.awsRegion) {
947-
const prefix = AwsBedrockHandler.getPrefixForRegion(this.options.awsRegion)
948-
if (prefix) {
949-
modelConfig.id = `${prefix}${modelConfig.id}`
950+
if (this.options.awsUseCrossRegionInference) {
951+
// Use custom region if awsRegion is "custom"
952+
const regionToUse =
953+
this.options.awsRegion === "custom" && this.options.awsCustomRegion
954+
? this.options.awsCustomRegion
955+
: this.options.awsRegion
956+
957+
if (regionToUse) {
958+
const prefix = AwsBedrockHandler.getPrefixForRegion(regionToUse)
959+
if (prefix) {
960+
modelConfig.id = `${prefix}${modelConfig.id}`
961+
}
950962
}
951963
}
952964
}

webview-ui/src/components/settings/providers/Bedrock.tsx

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Standard
99

1010
import { inputEventTransform, noTransform } from "../transforms"
1111

12+
// AWS region format validation regex
13+
const AWS_REGION_REGEX = /^[a-z]{2,}-[a-z]+-\d+$/
14+
1215
type BedrockProps = {
1316
apiConfiguration: ProviderSettings
1417
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
@@ -19,6 +22,7 @@ export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedMo
1922
const { t } = useAppTranslation()
2023
const [awsEndpointSelected, setAwsEndpointSelected] = useState(!!apiConfiguration?.awsBedrockEndpointEnabled)
2124
const [customRegionSelected, setCustomRegionSelected] = useState(apiConfiguration?.awsRegion === "custom")
25+
const [customRegionError, setCustomRegionError] = useState<string | null>(null)
2226

2327
// Update the endpoint enabled state when the configuration changes
2428
useEffect(() => {
@@ -41,6 +45,34 @@ export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedMo
4145
[setApiConfigurationField],
4246
)
4347

48+
// Validate custom region format
49+
const validateCustomRegion = useCallback(
50+
(value: string) => {
51+
if (!value && customRegionSelected) {
52+
setCustomRegionError(t("settings:providers.awsCustomRegion.validation.required"))
53+
return false
54+
}
55+
if (value && !AWS_REGION_REGEX.test(value)) {
56+
setCustomRegionError(t("settings:providers.awsCustomRegion.validation.format"))
57+
return false
58+
}
59+
setCustomRegionError(null)
60+
return true
61+
},
62+
[customRegionSelected, t],
63+
)
64+
65+
// Handle custom region input change with validation
66+
const handleCustomRegionChange = useCallback(
67+
(event: Event | React.FormEvent<HTMLElement>) => {
68+
const target = event.target as HTMLInputElement
69+
const value = target.value
70+
validateCustomRegion(value)
71+
setApiConfigurationField("awsCustomRegion", value)
72+
},
73+
[setApiConfigurationField, validateCustomRegion],
74+
)
75+
4476
return (
4577
<>
4678
<VSCodeRadioGroup
@@ -98,9 +130,13 @@ export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedMo
98130
onValueChange={(value) => {
99131
setApiConfigurationField("awsRegion", value)
100132
setCustomRegionSelected(value === "custom")
101-
// Clear custom region when switching to a standard region
102-
if (value !== "custom") {
103-
setApiConfigurationField("awsCustomRegion", "")
133+
// Don't clear custom region when switching away - preserve the value
134+
if (value === "custom" && apiConfiguration?.awsCustomRegion) {
135+
// Validate the existing custom region value
136+
validateCustomRegion(apiConfiguration.awsCustomRegion)
137+
} else {
138+
// Clear validation error when not using custom region
139+
setCustomRegionError(null)
104140
}
105141
}}>
106142
<SelectTrigger className="w-full">
@@ -120,10 +156,14 @@ export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedMo
120156
<VSCodeTextField
121157
value={apiConfiguration?.awsCustomRegion || ""}
122158
style={{ width: "100%", marginTop: 3, marginBottom: 5 }}
123-
onInput={handleInputChange("awsCustomRegion")}
124-
placeholder={t("settings:placeholders.customRegion")}
159+
onInput={handleCustomRegionChange}
160+
placeholder={t("settings:providers.awsCustomRegion.placeholder")}
125161
data-testid="custom-region-input"
162+
className={customRegionError ? "error" : ""}
126163
/>
164+
{customRegionError && (
165+
<div className="text-sm text-vscode-errorForeground ml-6 mt-1 mb-2">{customRegionError}</div>
166+
)}
127167
<div className="text-sm text-vscode-descriptionForeground ml-6 mt-1 mb-3">
128168
{t("settings:providers.awsCustomRegion.examples")}
129169
<div className="ml-2">• us-west-3</div>

0 commit comments

Comments
 (0)