Răsfoiți Sursa

Adding support for custom VPC endpoints when using AWS Bedrock models (#3947)

* feat: Add custom VPC endpoint support for AWS Bedrock

* fix: Fix TypeScript error in Bedrock.tsx

* fix: Update VPC endpoint UI to match Cline's implementation

* Fix AWS Bedrock VPC endpoint UI implementation

- Changed checkbox label to 'Use custom VPC endpoint' to match Cline

- Fixed conditional rendering to show text field when checkbox is checked

- Ensured placeholder text appears correctly

- Maintained proper styling for consistency

* Fix AWS Bedrock VPC endpoint UI implementation to match Cline exactly

- Added state variable to track checkbox selection

- Fixed conditional rendering to show/hide text field based on checkbox state

- Maintained proper styling and placeholder text

* Fix AWS Bedrock VPC endpoint UI implementation with proper event handling

- Fixed checkbox onChange handler to accept boolean directly instead of event object

- Added unit tests to verify the behavior

- Maintained proper styling and placeholder text

* Update Bedrock VPC endpoint tests with proper test IDs

* Improve AWS Bedrock VPC endpoint text field alignment

- Removed left margin from text field to align with checkbox

- Maintained proper styling and placeholder text

* Preserve AWS Bedrock VPC endpoint URL when toggling checkbox

- Added awsBedrockEndpointEnabled field to schema

- Modified Bedrock provider to check both endpoint URL and enabled flag

- Updated UI to preserve endpoint URL when checkbox is toggled

- Maintained proper alignment with checkbox

* Implement AWS Bedrock Custom VPC Endpoint functionality

* fix: update ApiConfiguration to ProviderSettings in Bedrock tests and regenerate types

* fix: update all instances of ApiConfiguration to ProviderSettings in Bedrock tests

* Fixed broken unit test

* Add changeset for Bedrock VPC endpoint support

* informative placeholder

* Bug fixes

* Fixed failing tests

* Add example URLs to Bedrock VPC endpoint section and update tests

* Fix truncated test assertion in Bedrock.test.tsx that was breaking the UI

* Refactor mock components in Bedrock.test.tsx for improved data-testid handling

* feat(i18n): add VPC endpoint translations for AWS Bedrock settings

* test: update Bedrock component tests for internationalized strings

---------

Co-authored-by: Kevin White <[email protected]>
Co-authored-by: Daniel <[email protected]>
Co-authored-by: Daniel Riccio <[email protected]>
kcwhite 7 luni în urmă
părinte
comite
a4c91c65b3

+ 5 - 0
.changeset/bedrock-vpc-endpoint.md

@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Add AWS Bedrock VPC endpoint support, allowing users to connect to Bedrock through private VPC endpoints. This includes UI configuration options, updated types, and comprehensive test coverage.

+ 4 - 0
packages/types/src/provider-settings.ts

@@ -100,6 +100,8 @@ const bedrockSchema = apiModelIdProviderModelSchema.extend({
 	awsProfile: z.string().optional(),
 	awsUseProfile: z.boolean().optional(),
 	awsCustomArn: z.string().optional(),
+	awsBedrockEndpointEnabled: z.boolean().optional(),
+	awsBedrockEndpoint: z.string().optional(),
 })
 
 const vertexSchema = apiModelIdProviderModelSchema.extend({
@@ -283,6 +285,8 @@ export const PROVIDER_SETTINGS_KEYS = keysOf<ProviderSettings>()([
 	"awsProfile",
 	"awsUseProfile",
 	"awsCustomArn",
+	"awsBedrockEndpointEnabled",
+	"awsBedrockEndpoint",
 	// Google Vertex
 	"vertexKeyFile",
 	"vertexJsonCredentials",

+ 178 - 0
src/api/providers/__tests__/bedrock-vpc-endpoint.test.ts

@@ -0,0 +1,178 @@
+// Mock AWS SDK credential providers
+jest.mock("@aws-sdk/credential-providers", () => {
+	const mockFromIni = jest.fn().mockReturnValue({
+		accessKeyId: "profile-access-key",
+		secretAccessKey: "profile-secret-key",
+	})
+	return { fromIni: mockFromIni }
+})
+
+// Mock BedrockRuntimeClient and ConverseStreamCommand
+const mockBedrockRuntimeClient = jest.fn()
+const mockSend = jest.fn().mockResolvedValue({
+	stream: [],
+})
+
+jest.mock("@aws-sdk/client-bedrock-runtime", () => ({
+	BedrockRuntimeClient: mockBedrockRuntimeClient.mockImplementation(() => ({
+		send: mockSend,
+	})),
+	ConverseStreamCommand: jest.fn(),
+	ConverseCommand: jest.fn(),
+}))
+
+import { AwsBedrockHandler } from "../bedrock"
+
+describe("AWS Bedrock VPC Endpoint Functionality", () => {
+	beforeEach(() => {
+		// Clear all mocks before each test
+		jest.clearAllMocks()
+	})
+
+	// Test Scenario 1: Input Validation Test
+	describe("VPC Endpoint URL Validation", () => {
+		it("should configure client with endpoint URL when both URL and enabled flag are provided", () => {
+			// Create handler with endpoint URL and enabled flag
+			new AwsBedrockHandler({
+				apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
+				awsAccessKey: "test-access-key",
+				awsSecretKey: "test-secret-key",
+				awsRegion: "us-east-1",
+				awsBedrockEndpoint: "https://bedrock-vpc.example.com",
+				awsBedrockEndpointEnabled: true,
+			})
+
+			// Verify the client was created with the correct endpoint
+			expect(mockBedrockRuntimeClient).toHaveBeenCalledWith(
+				expect.objectContaining({
+					region: "us-east-1",
+					endpoint: "https://bedrock-vpc.example.com",
+				}),
+			)
+		})
+
+		it("should not configure client with endpoint URL when URL is provided but enabled flag is false", () => {
+			// Create handler with endpoint URL but disabled flag
+			new AwsBedrockHandler({
+				apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
+				awsAccessKey: "test-access-key",
+				awsSecretKey: "test-secret-key",
+				awsRegion: "us-east-1",
+				awsBedrockEndpoint: "https://bedrock-vpc.example.com",
+				awsBedrockEndpointEnabled: false,
+			})
+
+			// Verify the client was created without the endpoint
+			expect(mockBedrockRuntimeClient).toHaveBeenCalledWith(
+				expect.objectContaining({
+					region: "us-east-1",
+				}),
+			)
+
+			// Verify the endpoint property is not present
+			const clientConfig = mockBedrockRuntimeClient.mock.calls[0][0]
+			expect(clientConfig).not.toHaveProperty("endpoint")
+		})
+	})
+
+	// Test Scenario 2: Edge Case Tests
+	describe("Edge Cases", () => {
+		it("should handle empty endpoint URL gracefully", () => {
+			// Create handler with empty endpoint URL but enabled flag
+			new AwsBedrockHandler({
+				apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
+				awsAccessKey: "test-access-key",
+				awsSecretKey: "test-secret-key",
+				awsRegion: "us-east-1",
+				awsBedrockEndpoint: "",
+				awsBedrockEndpointEnabled: true,
+			})
+
+			// Verify the client was created without the endpoint (since it's empty)
+			expect(mockBedrockRuntimeClient).toHaveBeenCalledWith(
+				expect.objectContaining({
+					region: "us-east-1",
+				}),
+			)
+
+			// Verify the endpoint property is not present
+			const clientConfig = mockBedrockRuntimeClient.mock.calls[0][0]
+			expect(clientConfig).not.toHaveProperty("endpoint")
+		})
+
+		it("should handle undefined endpoint URL gracefully", () => {
+			// Create handler with undefined endpoint URL but enabled flag
+			new AwsBedrockHandler({
+				apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
+				awsAccessKey: "test-access-key",
+				awsSecretKey: "test-secret-key",
+				awsRegion: "us-east-1",
+				awsBedrockEndpoint: undefined,
+				awsBedrockEndpointEnabled: true,
+			})
+
+			// Verify the client was created without the endpoint
+			expect(mockBedrockRuntimeClient).toHaveBeenCalledWith(
+				expect.objectContaining({
+					region: "us-east-1",
+				}),
+			)
+
+			// Verify the endpoint property is not present
+			const clientConfig = mockBedrockRuntimeClient.mock.calls[0][0]
+			expect(clientConfig).not.toHaveProperty("endpoint")
+		})
+	})
+
+	// Test Scenario 4: Error Handling Tests
+	describe("Error Handling", () => {
+		it("should handle invalid endpoint URLs by passing them directly to AWS SDK", () => {
+			// Create handler with an invalid URL format
+			new AwsBedrockHandler({
+				apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
+				awsAccessKey: "test-access-key",
+				awsSecretKey: "test-secret-key",
+				awsRegion: "us-east-1",
+				awsBedrockEndpoint: "invalid-url-format",
+				awsBedrockEndpointEnabled: true,
+			})
+
+			// Verify the client was created with the invalid endpoint
+			// (AWS SDK will handle the validation/errors)
+			expect(mockBedrockRuntimeClient).toHaveBeenCalledWith(
+				expect.objectContaining({
+					region: "us-east-1",
+					endpoint: "invalid-url-format",
+				}),
+			)
+		})
+	})
+
+	// Test Scenario 5: Persistence Tests
+	describe("Persistence", () => {
+		it("should maintain consistent behavior across multiple requests", async () => {
+			// Create handler with endpoint URL and enabled flag
+			const handler = new AwsBedrockHandler({
+				apiModelId: "anthropic.claude-3-5-sonnet-20241022-v2:0",
+				awsAccessKey: "test-access-key",
+				awsSecretKey: "test-secret-key",
+				awsRegion: "us-east-1",
+				awsBedrockEndpoint: "https://bedrock-vpc.example.com",
+				awsBedrockEndpointEnabled: true,
+			})
+
+			// Reset mock to clear the constructor call
+			mockBedrockRuntimeClient.mockClear()
+
+			// Make a request
+			try {
+				await handler.completePrompt("Test prompt")
+			} catch (error) {
+				// Ignore errors, we're just testing the client configuration
+			}
+
+			// Verify the client was configured with the endpoint
+			expect(mockSend).toHaveBeenCalled()
+		})
+	})
+})

+ 3 - 0
src/api/providers/bedrock.ts

@@ -169,6 +169,9 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH
 
 		const clientConfig: BedrockRuntimeClientConfig = {
 			region: this.options.awsRegion,
+			// Add the endpoint configuration when specified and enabled
+			...(this.options.awsBedrockEndpoint &&
+				this.options.awsBedrockEndpointEnabled && { endpoint: this.options.awsBedrockEndpoint }),
 		}
 
 		if (this.options.awsUseProfile && this.options.awsProfile) {

+ 2 - 4
src/core/config/__tests__/importExport.test.ts

@@ -231,10 +231,8 @@ describe("importExport", () => {
 				customModesManager: mockCustomModesManager,
 			})
 
-			expect(result).toEqual({
-				success: false,
-				error: "Expected property name or '}' in JSON at position 2",
-			})
+			expect(result.success).toBe(false)
+			expect(result.error).toMatch(/^Expected property name or '}' in JSON at position 2/)
 			expect(fs.readFile).toHaveBeenCalledWith("/mock/path/settings.json", "utf-8")
 			expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
 			expect(mockContextProxy.setValues).not.toHaveBeenCalled()

+ 32 - 1
webview-ui/src/components/settings/providers/Bedrock.tsx

@@ -1,4 +1,4 @@
-import { useCallback } from "react"
+import { useCallback, useState, useEffect } from "react"
 import { Checkbox } from "vscrui"
 import { VSCodeTextField, VSCodeRadio, VSCodeRadioGroup } from "@vscode/webview-ui-toolkit/react"
 
@@ -17,6 +17,12 @@ type BedrockProps = {
 
 export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedModelInfo }: BedrockProps) => {
 	const { t } = useAppTranslation()
+	const [awsEndpointSelected, setAwsEndpointSelected] = useState(!!apiConfiguration?.awsBedrockEndpointEnabled)
+
+	// Update the endpoint enabled state when the configuration changes
+	useEffect(() => {
+		setAwsEndpointSelected(!!apiConfiguration?.awsBedrockEndpointEnabled)
+	}, [apiConfiguration?.awsBedrockEndpointEnabled])
 
 	const handleInputChange = useCallback(
 		<K extends keyof ProviderSettings, E>(
@@ -120,6 +126,31 @@ export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedMo
 					{t("settings:providers.cacheUsageNote")}
 				</div>
 			</div>
+			<Checkbox
+				checked={awsEndpointSelected}
+				onChange={(isChecked) => {
+					setAwsEndpointSelected(isChecked)
+					setApiConfigurationField("awsBedrockEndpointEnabled", isChecked)
+				}}>
+				{t("settings:providers.awsBedrockVpc.useCustomVpcEndpoint")}
+			</Checkbox>
+			{awsEndpointSelected && (
+				<>
+					<VSCodeTextField
+						value={apiConfiguration?.awsBedrockEndpoint || ""}
+						style={{ width: "100%", marginTop: 3, marginBottom: 5 }}
+						type="url"
+						onInput={handleInputChange("awsBedrockEndpoint")}
+						placeholder={t("settings:providers.awsBedrockVpc.vpcEndpointUrlPlaceholder")}
+						data-testid="vpc-endpoint-input"
+					/>
+					<div className="text-sm text-vscode-descriptionForeground ml-6 mt-1 mb-3">
+						{t("settings:providers.awsBedrockVpc.examples")}
+						<div className="ml-2">• https://vpce-xxx.bedrock.region.vpce.amazonaws.com/</div>
+						<div className="ml-2">• https://gateway.my-company.com/route/app/bedrock</div>
+					</div>
+				</>
+			)}
 		</>
 	)
 }

+ 420 - 0
webview-ui/src/components/settings/providers/__tests__/Bedrock.test.tsx

@@ -0,0 +1,420 @@
+import React from "react"
+import { render, screen, fireEvent } from "@testing-library/react"
+import { Bedrock } from "../Bedrock"
+import { ProviderSettings } from "@roo-code/types"
+
+// Mock the vscrui Checkbox component
+jest.mock("vscrui", () => ({
+	Checkbox: ({ children, checked, onChange }: any) => (
+		<label data-testid={`checkbox-${children?.toString().replace(/\s+/g, "-").toLowerCase()}`}>
+			<input
+				type="checkbox"
+				checked={checked}
+				onChange={() => onChange(!checked)} // Toggle the checked state
+				data-testid={`checkbox-input-${children?.toString().replace(/\s+/g, "-").toLowerCase()}`}
+			/>
+			{children}
+		</label>
+	),
+}))
+
+// Mock the VSCodeTextField component
+jest.mock("@vscode/webview-ui-toolkit/react", () => ({
+	VSCodeTextField: ({
+		children,
+		value,
+		onInput,
+		placeholder,
+		className,
+		style,
+		"data-testid": dataTestId,
+		...rest
+	}: any) => {
+		// For all text fields - apply data-testid directly to input if provided
+		return (
+			<div
+				data-testid={dataTestId ? `${dataTestId}-text-field` : "vscode-text-field"}
+				className={className}
+				style={style}>
+				{children}
+				<input
+					type="text"
+					value={value}
+					onChange={(e) => onInput && onInput(e)}
+					placeholder={placeholder}
+					data-testid={dataTestId}
+					{...rest}
+				/>
+			</div>
+		)
+	},
+	VSCodeRadio: () => <div>Radio</div>,
+	VSCodeRadioGroup: ({ children }: any) => <div>{children}</div>,
+}))
+
+// Mock the translation hook
+jest.mock("@src/i18n/TranslationContext", () => ({
+	useAppTranslation: () => ({
+		t: (key: string) => key,
+	}),
+}))
+
+// Mock the UI components
+jest.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>,
+}))
+
+// Mock the constants
+jest.mock("../../constants", () => ({
+	AWS_REGIONS: [{ value: "us-east-1", label: "US East (N. Virginia)" }],
+}))
+
+describe("Bedrock Component", () => {
+	const mockSetApiConfigurationField = jest.fn()
+
+	beforeEach(() => {
+		jest.clearAllMocks()
+	})
+
+	it("should show text field when VPC endpoint checkbox is checked", () => {
+		// Initial render with checkbox unchecked
+		const apiConfiguration: Partial<ProviderSettings> = {
+			awsBedrockEndpoint: "",
+			awsUseProfile: true, // Use profile to avoid rendering other text fields
+		}
+
+		render(
+			<Bedrock
+				apiConfiguration={apiConfiguration as ProviderSettings}
+				setApiConfigurationField={mockSetApiConfigurationField}
+			/>,
+		)
+
+		// Text field should not be visible initially
+		expect(screen.queryByTestId("vpc-endpoint-input")).not.toBeInTheDocument()
+
+		// Click the checkbox
+		fireEvent.click(screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint"))
+
+		// Text field should now be visible
+		expect(screen.getByTestId("vpc-endpoint-input")).toBeInTheDocument()
+	})
+
+	it("should hide text field when VPC endpoint checkbox is unchecked", () => {
+		// Initial render with checkbox checked
+		const apiConfiguration: Partial<ProviderSettings> = {
+			awsBedrockEndpoint: "https://example.com",
+			awsBedrockEndpointEnabled: true, // Need to explicitly set this to true
+			awsUseProfile: true, // Use profile to avoid rendering other text fields
+		}
+
+		render(
+			<Bedrock
+				apiConfiguration={apiConfiguration as ProviderSettings}
+				setApiConfigurationField={mockSetApiConfigurationField}
+			/>,
+		)
+
+		// Text field should be visible initially
+		expect(screen.getByTestId("vpc-endpoint-input")).toBeInTheDocument()
+
+		// Click the checkbox to uncheck it
+		fireEvent.click(screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint"))
+
+		// Text field should now be hidden
+		expect(screen.queryByTestId("vpc-endpoint-input")).not.toBeInTheDocument()
+
+		// Should call setApiConfigurationField to update the enabled flag
+		expect(mockSetApiConfigurationField).toHaveBeenCalledWith("awsBedrockEndpointEnabled", false)
+	})
+
+	// Test Scenario 1: Input Validation Test
+	describe("Input Validation", () => {
+		it("should accept valid URL formats", () => {
+			const apiConfiguration: Partial<ProviderSettings> = {
+				awsBedrockEndpoint: "",
+				awsBedrockEndpointEnabled: true,
+				awsUseProfile: true,
+			}
+
+			render(
+				<Bedrock
+					apiConfiguration={apiConfiguration as ProviderSettings}
+					setApiConfigurationField={mockSetApiConfigurationField}
+				/>,
+			)
+
+			// Find the input field
+			const inputField = screen.getByTestId("vpc-endpoint-input")
+			expect(inputField).toBeInTheDocument()
+
+			// Test with a valid URL
+			fireEvent.change(inputField, { target: { value: "https://bedrock.us-east-1.amazonaws.com" } })
+
+			// Verify the configuration field was updated with the valid URL
+			expect(mockSetApiConfigurationField).toHaveBeenCalledWith(
+				"awsBedrockEndpoint",
+				"https://bedrock.us-east-1.amazonaws.com",
+			)
+		})
+
+		it("should handle empty URL input", () => {
+			const apiConfiguration: Partial<ProviderSettings> = {
+				awsBedrockEndpoint: "https://example.com",
+				awsBedrockEndpointEnabled: true,
+				awsUseProfile: true,
+			}
+
+			render(
+				<Bedrock
+					apiConfiguration={apiConfiguration as ProviderSettings}
+					setApiConfigurationField={mockSetApiConfigurationField}
+				/>,
+			)
+
+			// Find the input field
+			const inputField = screen.getByTestId("vpc-endpoint-input")
+
+			// Clear the field
+			fireEvent.change(inputField, { target: { value: "" } })
+
+			// Verify the configuration field was updated with empty string
+			expect(mockSetApiConfigurationField).toHaveBeenCalledWith("awsBedrockEndpoint", "")
+		})
+	})
+
+	// Test Scenario 2: Edge Case Tests
+	describe("Edge Cases", () => {
+		it("should preserve endpoint URL when toggling checkbox multiple times", () => {
+			const apiConfiguration: Partial<ProviderSettings> = {
+				awsBedrockEndpoint: "https://bedrock-vpc.example.com",
+				awsBedrockEndpointEnabled: true,
+				awsUseProfile: true,
+			}
+
+			render(
+				<Bedrock
+					apiConfiguration={apiConfiguration as ProviderSettings}
+					setApiConfigurationField={mockSetApiConfigurationField}
+				/>,
+			)
+
+			// Initial state: checkbox checked, URL visible
+			expect(screen.getByTestId("vpc-endpoint-input")).toBeInTheDocument()
+			expect(screen.getByTestId("vpc-endpoint-input")).toHaveValue("https://bedrock-vpc.example.com")
+
+			// Uncheck the checkbox
+			fireEvent.click(screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint"))
+
+			// Verify endpoint enabled was set to false
+			expect(mockSetApiConfigurationField).toHaveBeenCalledWith("awsBedrockEndpointEnabled", false)
+
+			// Check the checkbox again
+			fireEvent.click(screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint"))
+
+			// Verify endpoint enabled was set to true
+			expect(mockSetApiConfigurationField).toHaveBeenCalledWith("awsBedrockEndpointEnabled", true)
+
+			// Verify the URL field is visible again
+			expect(screen.getByTestId("vpc-endpoint-input")).toBeInTheDocument()
+		})
+
+		it("should handle very long endpoint URLs", () => {
+			const veryLongUrl =
+				"https://bedrock-vpc-endpoint-with-a-very-long-name-that-might-cause-issues-in-some-ui-components.region-1.amazonaws.com/api/v1/endpoint"
+
+			const apiConfiguration: Partial<ProviderSettings> = {
+				awsBedrockEndpoint: veryLongUrl,
+				awsBedrockEndpointEnabled: true,
+				awsUseProfile: true,
+			}
+
+			render(
+				<Bedrock
+					apiConfiguration={apiConfiguration as ProviderSettings}
+					setApiConfigurationField={mockSetApiConfigurationField}
+				/>,
+			)
+
+			// Verify the long URL is displayed correctly
+			expect(screen.getByTestId("vpc-endpoint-input")).toHaveValue(veryLongUrl)
+
+			// Change the URL to something else
+			fireEvent.change(screen.getByTestId("vpc-endpoint-input"), {
+				target: { value: "https://shorter-url.com" },
+			})
+
+			// Verify the configuration was updated
+			expect(mockSetApiConfigurationField).toHaveBeenCalledWith("awsBedrockEndpoint", "https://shorter-url.com")
+		})
+	})
+
+	// Test Scenario 3: UI Elements Tests
+	describe("UI Elements", () => {
+		it("should display example URLs when VPC endpoint checkbox is checked", () => {
+			const apiConfiguration: Partial<ProviderSettings> = {
+				awsBedrockEndpoint: "https://example.com",
+				awsBedrockEndpointEnabled: true,
+				awsUseProfile: true,
+			}
+
+			render(
+				<Bedrock
+					apiConfiguration={apiConfiguration as ProviderSettings}
+					setApiConfigurationField={mockSetApiConfigurationField}
+				/>,
+			)
+
+			// Check that the VPC endpoint input is visible
+			expect(screen.getByTestId("vpc-endpoint-input")).toBeInTheDocument()
+
+			// Check for the example URLs section
+			// Since we don't have a specific testid for the examples section,
+			// we'll check for the text content
+			expect(screen.getByText("settings:providers.awsBedrockVpc.examples")).toBeInTheDocument()
+			expect(screen.getByText("• https://vpce-xxx.bedrock.region.vpce.amazonaws.com/")).toBeInTheDocument()
+			expect(screen.getByText("• https://gateway.my-company.com/route/app/bedrock")).toBeInTheDocument()
+		})
+
+		it("should hide example URLs when VPC endpoint checkbox is unchecked", () => {
+			const apiConfiguration: Partial<ProviderSettings> = {
+				awsBedrockEndpoint: "https://example.com",
+				awsBedrockEndpointEnabled: true,
+				awsUseProfile: true,
+			}
+
+			render(
+				<Bedrock
+					apiConfiguration={apiConfiguration as ProviderSettings}
+					setApiConfigurationField={mockSetApiConfigurationField}
+				/>,
+			)
+
+			// Initially the examples should be visible
+			expect(screen.getByText("settings:providers.awsBedrockVpc.examples")).toBeInTheDocument()
+
+			// Uncheck the VPC endpoint checkbox
+			fireEvent.click(screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint"))
+
+			// Now the examples should be hidden
+			expect(screen.queryByText("settings:providers.awsBedrockVpc.examples")).not.toBeInTheDocument()
+			expect(screen.queryByText("• https://vpce-xxx.bedrock.region.vpce.amazonaws.com/")).not.toBeInTheDocument()
+			expect(screen.queryByText("• https://gateway.my-company.com/route/app/bedrock")).not.toBeInTheDocument()
+		})
+	})
+
+	// Test Scenario 4: Error Handling Tests
+	describe("Error Handling", () => {
+		it("should handle invalid endpoint URLs gracefully", () => {
+			const apiConfiguration: Partial<ProviderSettings> = {
+				awsBedrockEndpoint: "",
+				awsBedrockEndpointEnabled: true,
+				awsUseProfile: true,
+			}
+
+			render(
+				<Bedrock
+					apiConfiguration={apiConfiguration as ProviderSettings}
+					setApiConfigurationField={mockSetApiConfigurationField}
+				/>,
+			)
+
+			// Find the input field
+			const inputField = screen.getByTestId("vpc-endpoint-input")
+
+			// Enter an invalid URL (missing protocol)
+			fireEvent.change(inputField, { target: { value: "invalid-url" } })
+
+			// The component should still update the configuration
+			// (URL validation would typically happen at a higher level or when used)
+			expect(mockSetApiConfigurationField).toHaveBeenCalledWith("awsBedrockEndpoint", "invalid-url")
+		})
+	})
+
+	// Test Scenario 5: Persistence Tests
+	describe("Persistence", () => {
+		it("should initialize with the correct state from apiConfiguration", () => {
+			// Test with endpoint enabled
+			const apiConfigurationEnabled: Partial<ProviderSettings> = {
+				awsBedrockEndpoint: "https://custom-endpoint.aws.com",
+				awsBedrockEndpointEnabled: true,
+				awsUseProfile: true,
+			}
+
+			const { unmount } = render(
+				<Bedrock
+					apiConfiguration={apiConfigurationEnabled as ProviderSettings}
+					setApiConfigurationField={mockSetApiConfigurationField}
+				/>,
+			)
+
+			// Verify checkbox is checked and endpoint is visible
+			expect(screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint")).toBeChecked()
+			expect(screen.getByTestId("vpc-endpoint-input")).toBeInTheDocument()
+			expect(screen.getByTestId("vpc-endpoint-input")).toHaveValue("https://custom-endpoint.aws.com")
+
+			unmount()
+
+			// Test with endpoint disabled
+			const apiConfigurationDisabled: Partial<ProviderSettings> = {
+				awsBedrockEndpoint: "https://custom-endpoint.aws.com",
+				awsBedrockEndpointEnabled: false,
+				awsUseProfile: true,
+			}
+
+			render(
+				<Bedrock
+					apiConfiguration={apiConfigurationDisabled as ProviderSettings}
+					setApiConfigurationField={mockSetApiConfigurationField}
+				/>,
+			)
+
+			// Verify checkbox is unchecked and endpoint is not visible
+			expect(screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint")).not.toBeChecked()
+			expect(screen.queryByTestId("vpc-endpoint-input")).not.toBeInTheDocument()
+		})
+
+		it("should update state when apiConfiguration changes", () => {
+			// Initial render with endpoint disabled
+			const apiConfigurationInitial: Partial<ProviderSettings> = {
+				awsBedrockEndpoint: "https://initial-endpoint.aws.com",
+				awsBedrockEndpointEnabled: false,
+				awsUseProfile: true,
+			}
+
+			const { rerender } = render(
+				<Bedrock
+					apiConfiguration={apiConfigurationInitial as ProviderSettings}
+					setApiConfigurationField={mockSetApiConfigurationField}
+				/>,
+			)
+
+			// Verify initial state
+			expect(screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint")).not.toBeChecked()
+			expect(screen.queryByTestId("vpc-endpoint-input")).not.toBeInTheDocument()
+
+			// Update with new configuration
+			const apiConfigurationUpdated: Partial<ProviderSettings> = {
+				awsBedrockEndpoint: "https://updated-endpoint.aws.com",
+				awsBedrockEndpointEnabled: true,
+				awsUseProfile: true,
+			}
+
+			rerender(
+				<Bedrock
+					apiConfiguration={apiConfigurationUpdated as ProviderSettings}
+					setApiConfigurationField={mockSetApiConfigurationField}
+				/>,
+			)
+
+			// Verify updated state
+			expect(screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint")).toBeChecked()
+			expect(screen.getByTestId("vpc-endpoint-input")).toBeInTheDocument()
+			expect(screen.getByTestId("vpc-endpoint-input")).toHaveValue("https://updated-endpoint.aws.com")
+		})
+	})
+})

+ 5 - 0
webview-ui/src/i18n/locales/ca/settings.json

@@ -189,6 +189,11 @@
 		"awsSessionToken": "Token de sessió d'AWS",
 		"awsRegion": "Regió d'AWS",
 		"awsCrossRegion": "Utilitzar inferència entre regions",
+		"awsBedrockVpc": {
+			"useCustomVpcEndpoint": "Utilitzar punt final VPC personalitzat",
+			"vpcEndpointUrlPlaceholder": "Introduïu l'URL del punt final VPC (opcional)",
+			"examples": "Exemples:"
+		},
 		"enablePromptCaching": "Habilitar emmagatzematge en caché de prompts",
 		"enablePromptCachingTitle": "Habilitar l'emmagatzematge en caché de prompts per millorar el rendiment i reduir els costos per als models compatibles.",
 		"cacheUsageNote": "Nota: Si no veieu l'ús de la caché, proveu de seleccionar un model diferent i després tornar a seleccionar el model desitjat.",

+ 5 - 0
webview-ui/src/i18n/locales/de/settings.json

@@ -189,6 +189,11 @@
 		"awsSessionToken": "AWS Sitzungstoken",
 		"awsRegion": "AWS Region",
 		"awsCrossRegion": "Regionsübergreifende Inferenz verwenden",
+		"awsBedrockVpc": {
+			"useCustomVpcEndpoint": "Benutzerdefinierten VPC-Endpunkt verwenden",
+			"vpcEndpointUrlPlaceholder": "VPC-Endpunkt-URL eingeben (optional)",
+			"examples": "Beispiele:"
+		},
 		"enablePromptCaching": "Prompt-Caching aktivieren",
 		"enablePromptCachingTitle": "Prompt-Caching aktivieren, um die Leistung zu verbessern und Kosten für unterstützte Modelle zu reduzieren.",
 		"cacheUsageNote": "Hinweis: Wenn Sie keine Cache-Nutzung sehen, versuchen Sie ein anderes Modell auszuwählen und dann Ihr gewünschtes Modell erneut auszuwählen.",

+ 5 - 0
webview-ui/src/i18n/locales/en/settings.json

@@ -189,6 +189,11 @@
 		"awsSessionToken": "AWS Session Token",
 		"awsRegion": "AWS Region",
 		"awsCrossRegion": "Use cross-region inference",
+		"awsBedrockVpc": {
+			"useCustomVpcEndpoint": "Use custom VPC endpoint",
+			"vpcEndpointUrlPlaceholder": "Enter VPC Endpoint URL (optional)",
+			"examples": "Examples:"
+		},
 		"enablePromptCaching": "Enable prompt caching",
 		"enablePromptCachingTitle": "Enable prompt caching to improve performance and reduce costs for supported models.",
 		"cacheUsageNote": "Note: If you don't see cache usage, try selecting a different model and then selecting your desired model again.",

+ 5 - 0
webview-ui/src/i18n/locales/es/settings.json

@@ -189,6 +189,11 @@
 		"awsSessionToken": "Token de sesión de AWS",
 		"awsRegion": "Región de AWS",
 		"awsCrossRegion": "Usar inferencia entre regiones",
+		"awsBedrockVpc": {
+			"useCustomVpcEndpoint": "Usar punto de conexión VPC personalizado",
+			"vpcEndpointUrlPlaceholder": "Ingrese URL del punto de conexión VPC (opcional)",
+			"examples": "Ejemplos:"
+		},
 		"enablePromptCaching": "Habilitar caché de prompts",
 		"enablePromptCachingTitle": "Habilitar el caché de prompts para mejorar el rendimiento y reducir costos para modelos compatibles.",
 		"cacheUsageNote": "Nota: Si no ve el uso del caché, intente seleccionar un modelo diferente y luego seleccionar nuevamente su modelo deseado.",

+ 5 - 0
webview-ui/src/i18n/locales/fr/settings.json

@@ -189,6 +189,11 @@
 		"awsSessionToken": "Jeton de session AWS",
 		"awsRegion": "Région AWS",
 		"awsCrossRegion": "Utiliser l'inférence inter-régions",
+		"awsBedrockVpc": {
+			"useCustomVpcEndpoint": "Utiliser un point de terminaison VPC personnalisé",
+			"vpcEndpointUrlPlaceholder": "Entrer l'URL du point de terminaison VPC (optionnel)",
+			"examples": "Exemples :"
+		},
 		"enablePromptCaching": "Activer la mise en cache des prompts",
 		"enablePromptCachingTitle": "Activer la mise en cache des prompts pour améliorer les performances et réduire les coûts pour les modèles pris en charge.",
 		"cacheUsageNote": "Remarque : Si vous ne voyez pas l'utilisation du cache, essayez de sélectionner un modèle différent puis de sélectionner à nouveau votre modèle souhaité.",

+ 5 - 0
webview-ui/src/i18n/locales/hi/settings.json

@@ -189,6 +189,11 @@
 		"awsSessionToken": "AWS सत्र टोकन",
 		"awsRegion": "AWS क्षेत्र",
 		"awsCrossRegion": "क्रॉस-क्षेत्र अनुमान का उपयोग करें",
+		"awsBedrockVpc": {
+			"useCustomVpcEndpoint": "कस्टम VPC एंडपॉइंट का उपयोग करें",
+			"vpcEndpointUrlPlaceholder": "VPC एंडपॉइंट URL दर्ज करें (वैकल्पिक)",
+			"examples": "उदाहरण:"
+		},
 		"enablePromptCaching": "प्रॉम्प्ट कैशिंग सक्षम करें",
 		"enablePromptCachingTitle": "समर्थित मॉडल के लिए प्रदर्शन में सुधार और लागत को कम करने के लिए प्रॉम्प्ट कैशिंग सक्षम करें।",
 		"cacheUsageNote": "नोट: यदि आप कैश उपयोग नहीं देखते हैं, तो एक अलग मॉडल चुनने का प्रयास करें और फिर अपने वांछित मॉडल को पुनः चुनें।",

+ 5 - 0
webview-ui/src/i18n/locales/it/settings.json

@@ -189,6 +189,11 @@
 		"awsSessionToken": "Token di sessione AWS",
 		"awsRegion": "Regione AWS",
 		"awsCrossRegion": "Usa inferenza cross-regione",
+		"awsBedrockVpc": {
+			"useCustomVpcEndpoint": "Usa endpoint VPC personalizzato",
+			"vpcEndpointUrlPlaceholder": "Inserisci URL endpoint VPC (opzionale)",
+			"examples": "Esempi:"
+		},
 		"enablePromptCaching": "Abilita cache dei prompt",
 		"enablePromptCachingTitle": "Abilita la cache dei prompt per migliorare le prestazioni e ridurre i costi per i modelli supportati.",
 		"cacheUsageNote": "Nota: Se non vedi l'utilizzo della cache, prova a selezionare un modello diverso e poi seleziona nuovamente il modello desiderato.",

+ 5 - 0
webview-ui/src/i18n/locales/ja/settings.json

@@ -189,6 +189,11 @@
 		"awsSessionToken": "AWSセッショントークン",
 		"awsRegion": "AWSリージョン",
 		"awsCrossRegion": "クロスリージョン推論を使用",
+		"awsBedrockVpc": {
+			"useCustomVpcEndpoint": "カスタムVPCエンドポイントを使用",
+			"vpcEndpointUrlPlaceholder": "VPCエンドポイントURLを入力(任意)",
+			"examples": "例:"
+		},
 		"enablePromptCaching": "プロンプトキャッシュを有効化",
 		"enablePromptCachingTitle": "サポートされているモデルのパフォーマンスを向上させ、コストを削減するためにプロンプトキャッシュを有効化します。",
 		"cacheUsageNote": "注意:キャッシュの使用が表示されない場合は、別のモデルを選択してから希望のモデルを再度選択してみてください。",

+ 5 - 0
webview-ui/src/i18n/locales/ko/settings.json

@@ -189,6 +189,11 @@
 		"awsSessionToken": "AWS 세션 토큰",
 		"awsRegion": "AWS 리전",
 		"awsCrossRegion": "교차 리전 추론 사용",
+		"awsBedrockVpc": {
+			"useCustomVpcEndpoint": "사용자 지정 VPC 엔드포인트 사용",
+			"vpcEndpointUrlPlaceholder": "VPC 엔드포인트 URL 입력 (선택사항)",
+			"examples": "예시:"
+		},
 		"enablePromptCaching": "프롬프트 캐시 활성화",
 		"enablePromptCachingTitle": "지원되는 모델의 성능을 향상시키고 비용을 절감하기 위해 프롬프트 캐시를 활성화합니다.",
 		"cacheUsageNote": "참고: 캐시 사용이 표시되지 않는 경우, 다른 모델을 선택한 다음 원하는 모델을 다시 선택해 보세요.",

+ 5 - 0
webview-ui/src/i18n/locales/nl/settings.json

@@ -189,6 +189,11 @@
 		"awsSessionToken": "AWS-sessietoken",
 		"awsRegion": "AWS-regio",
 		"awsCrossRegion": "Gebruik cross-region inference",
+		"awsBedrockVpc": {
+			"useCustomVpcEndpoint": "Aangepast VPC-eindpunt gebruiken",
+			"vpcEndpointUrlPlaceholder": "Voer VPC-eindpunt URL in (optioneel)",
+			"examples": "Voorbeelden:"
+		},
 		"enablePromptCaching": "Prompt caching inschakelen",
 		"enablePromptCachingTitle": "Schakel prompt caching in om de prestaties te verbeteren en de kosten te verlagen voor ondersteunde modellen.",
 		"cacheUsageNote": "Let op: als je geen cachegebruik ziet, probeer dan een ander model te selecteren en vervolgens weer je gewenste model.",

+ 5 - 0
webview-ui/src/i18n/locales/pl/settings.json

@@ -189,6 +189,11 @@
 		"awsSessionToken": "Token sesji AWS",
 		"awsRegion": "Region AWS",
 		"awsCrossRegion": "Użyj wnioskowania międzyregionalnego",
+		"awsBedrockVpc": {
+			"useCustomVpcEndpoint": "Użyj niestandardowego punktu końcowego VPC",
+			"vpcEndpointUrlPlaceholder": "Wprowadź URL punktu końcowego VPC (opcjonalnie)",
+			"examples": "Przykłady:"
+		},
 		"enablePromptCaching": "Włącz buforowanie podpowiedzi",
 		"enablePromptCachingTitle": "Włącz buforowanie podpowiedzi, aby poprawić wydajność i zmniejszyć koszty dla obsługiwanych modeli.",
 		"cacheUsageNote": "Uwaga: Jeśli nie widzisz użycia bufora, spróbuj wybrać inny model, a następnie ponownie wybrać żądany model.",

+ 5 - 0
webview-ui/src/i18n/locales/pt-BR/settings.json

@@ -189,6 +189,11 @@
 		"awsSessionToken": "Token de Sessão AWS",
 		"awsRegion": "Região AWS",
 		"awsCrossRegion": "Usar inferência entre regiões",
+		"awsBedrockVpc": {
+			"useCustomVpcEndpoint": "Usar endpoint VPC personalizado",
+			"vpcEndpointUrlPlaceholder": "Digite a URL do endpoint VPC (opcional)",
+			"examples": "Exemplos:"
+		},
 		"enablePromptCaching": "Ativar cache de prompts",
 		"enablePromptCachingTitle": "Ativar cache de prompts para melhorar o desempenho e reduzir custos para modelos suportados.",
 		"cacheUsageNote": "Nota: Se você não vir o uso do cache, tente selecionar um modelo diferente e depois selecionar novamente o modelo desejado.",

+ 5 - 0
webview-ui/src/i18n/locales/ru/settings.json

@@ -189,6 +189,11 @@
 		"awsSessionToken": "AWS Session Token",
 		"awsRegion": "Регион AWS",
 		"awsCrossRegion": "Использовать кросс-региональный вывод",
+		"awsBedrockVpc": {
+			"useCustomVpcEndpoint": "Использовать пользовательскую конечную точку VPC",
+			"vpcEndpointUrlPlaceholder": "Введите URL конечной точки VPC (опционально)",
+			"examples": "Примеры:"
+		},
 		"enablePromptCaching": "Включить кэширование подсказок",
 		"enablePromptCachingTitle": "Включить кэширование подсказок для повышения производительности и снижения затрат для поддерживаемых моделей.",
 		"cacheUsageNote": "Примечание: если вы не видите использование кэша, попробуйте выбрать другую модель, а затем вернуться к нужной.",

+ 5 - 0
webview-ui/src/i18n/locales/tr/settings.json

@@ -189,6 +189,11 @@
 		"awsSessionToken": "AWS Oturum Belirteci",
 		"awsRegion": "AWS Bölgesi",
 		"awsCrossRegion": "Bölgeler arası çıkarım kullan",
+		"awsBedrockVpc": {
+			"useCustomVpcEndpoint": "Özel VPC uç noktası kullan",
+			"vpcEndpointUrlPlaceholder": "VPC uç noktası URL'sini girin (isteğe bağlı)",
+			"examples": "Örnekler:"
+		},
 		"enablePromptCaching": "İstem önbelleğini etkinleştir",
 		"enablePromptCachingTitle": "Desteklenen modeller için performansı artırmak ve maliyetleri azaltmak için istem önbelleğini etkinleştir.",
 		"cacheUsageNote": "Not: Önbellek kullanımını görmüyorsanız, farklı bir model seçip ardından istediğiniz modeli tekrar seçmeyi deneyin.",

+ 5 - 0
webview-ui/src/i18n/locales/vi/settings.json

@@ -189,6 +189,11 @@
 		"awsSessionToken": "Token phiên AWS",
 		"awsRegion": "Vùng AWS",
 		"awsCrossRegion": "Sử dụng suy luận liên vùng",
+		"awsBedrockVpc": {
+			"useCustomVpcEndpoint": "Sử dụng điểm cuối VPC tùy chỉnh",
+			"vpcEndpointUrlPlaceholder": "Nhập URL điểm cuối VPC (tùy chọn)",
+			"examples": "Ví dụ:"
+		},
 		"enablePromptCaching": "Bật bộ nhớ đệm lời nhắc",
 		"enablePromptCachingTitle": "Bật bộ nhớ đệm lời nhắc để cải thiện hiệu suất và giảm chi phí cho các mô hình được hỗ trợ.",
 		"cacheUsageNote": "Lưu ý: Nếu bạn không thấy việc sử dụng bộ nhớ đệm, hãy thử chọn một mô hình khác và sau đó chọn lại mô hình mong muốn của bạn.",

+ 5 - 0
webview-ui/src/i18n/locales/zh-CN/settings.json

@@ -189,6 +189,11 @@
 		"awsSessionToken": "AWS 会话Token",
 		"awsRegion": "AWS 区域",
 		"awsCrossRegion": "使用跨区域推理",
+		"awsBedrockVpc": {
+			"useCustomVpcEndpoint": "使用自定义 VPC 端点",
+			"vpcEndpointUrlPlaceholder": "输入 VPC 端点 URL(可选)",
+			"examples": "示例:"
+		},
 		"enablePromptCaching": "启用提示缓存",
 		"enablePromptCachingTitle": "开启提示缓存可提升性能并节省成本",
 		"cacheUsageNote": "提示:若未显示缓存使用情况,请切换模型后重新选择",

+ 5 - 0
webview-ui/src/i18n/locales/zh-TW/settings.json

@@ -189,6 +189,11 @@
 		"awsSessionToken": "AWS 工作階段權杖",
 		"awsRegion": "AWS 區域",
 		"awsCrossRegion": "使用跨區域推論",
+		"awsBedrockVpc": {
+			"useCustomVpcEndpoint": "使用自訂 VPC 端點",
+			"vpcEndpointUrlPlaceholder": "輸入 VPC 端點 URL(選填)",
+			"examples": "範例:"
+		},
 		"enablePromptCaching": "啟用提示快取",
 		"enablePromptCachingTitle": "啟用提示快取以提升支援的模型效能並降低成本。",
 		"cacheUsageNote": "注意:如果您沒有看到快取使用情況,請嘗試選擇其他模型,然後重新選擇您想要的模型。",