diff --git a/src/api/providers/__tests__/bedrock.spec.ts b/src/api/providers/__tests__/bedrock.spec.ts index cccec3818f..8d2cc524b4 100644 --- a/src/api/providers/__tests__/bedrock.spec.ts +++ b/src/api/providers/__tests__/bedrock.spec.ts @@ -360,6 +360,158 @@ describe("AwsBedrockHandler", () => { expect(result.modelId).toBe("ap.anthropic.claude-3-5-sonnet-20241022-v2:0") // Should be preserved as-is }) }) + + describe("GovCloud and China partition ARN parsing", () => { + it("should parse GovCloud ARNs correctly", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "test", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-gov-west-1", + }) + + const parseArn = (handler as any).parseArn.bind(handler) + + const result = parseArn( + "arn:aws-us-gov:bedrock:us-gov-west-1:123456789012:inference-profile/us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0", + ) + + expect(result.isValid).toBe(true) + expect(result.region).toBe("us-gov-west-1") + expect(result.modelType).toBe("inference-profile") + expect(result.modelId).toBe("us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0") + }) + + it("should parse GovCloud ARNs without account ID", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "test", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-gov-east-1", + }) + + const parseArn = (handler as any).parseArn.bind(handler) + + const result = parseArn("arn:aws-us-gov:bedrock:us-gov-east-1::foundation-model/anthropic.claude-v2") + + expect(result.isValid).toBe(true) + expect(result.region).toBe("us-gov-east-1") + expect(result.modelType).toBe("foundation-model") + expect(result.modelId).toBe("anthropic.claude-v2") + }) + + it("should parse China region ARNs correctly", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "test", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "cn-north-1", + }) + + const parseArn = (handler as any).parseArn.bind(handler) + + const result = parseArn( + "arn:aws-cn:bedrock:cn-north-1:123456789012:foundation-model/anthropic.claude-3-sonnet-20240229-v1:0", + ) + + expect(result.isValid).toBe(true) + expect(result.region).toBe("cn-north-1") + expect(result.modelType).toBe("foundation-model") + expect(result.modelId).toBe("anthropic.claude-3-sonnet-20240229-v1:0") + }) + + it("should parse China region ARNs without account ID", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "test", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "cn-northwest-1", + }) + + const parseArn = (handler as any).parseArn.bind(handler) + + const result = parseArn("arn:aws-cn:bedrock:cn-northwest-1::inference-profile/custom-model") + + expect(result.isValid).toBe(true) + expect(result.region).toBe("cn-northwest-1") + expect(result.modelType).toBe("inference-profile") + expect(result.modelId).toBe("custom-model") + }) + + it("should handle GovCloud Sagemaker ARNs", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "test", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-gov-west-1", + }) + + const parseArn = (handler as any).parseArn.bind(handler) + + const result = parseArn("arn:aws-us-gov:sagemaker:us-gov-west-1:123456789012:endpoint/gov-endpoint") + + expect(result.isValid).toBe(true) + expect(result.region).toBe("us-gov-west-1") + expect(result.modelType).toBe("endpoint") + expect(result.modelId).toBe("gov-endpoint") + }) + + it("should handle China Sagemaker ARNs", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "test", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "cn-north-1", + }) + + const parseArn = (handler as any).parseArn.bind(handler) + + const result = parseArn("arn:aws-cn:sagemaker:cn-north-1:123456789012:endpoint/china-endpoint") + + expect(result.isValid).toBe(true) + expect(result.region).toBe("cn-north-1") + expect(result.modelType).toBe("endpoint") + expect(result.modelId).toBe("china-endpoint") + }) + + it("should detect region mismatch for GovCloud ARNs", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "test", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-gov-west-1", + }) + + const parseArn = (handler as any).parseArn.bind(handler) + + const result = parseArn( + "arn:aws-us-gov:bedrock:us-gov-east-1::foundation-model/anthropic.claude-v2", + "us-gov-west-1", + ) + + expect(result.isValid).toBe(true) + expect(result.region).toBe("us-gov-east-1") + expect(result.errorMessage).toContain("region") + expect(result.errorMessage).toContain("us-gov-east-1") + expect(result.errorMessage).toContain("us-gov-west-1") + }) + + it("should reject invalid partition ARNs", () => { + const handler = new AwsBedrockHandler({ + apiModelId: "test", + awsAccessKey: "test", + awsSecretKey: "test", + awsRegion: "us-east-1", + }) + + const parseArn = (handler as any).parseArn.bind(handler) + + const result = parseArn("arn:aws-invalid:bedrock:us-east-1::foundation-model/anthropic.claude-v2") + + expect(result.isValid).toBe(false) + expect(result.errorMessage).toContain("Invalid ARN format") + }) + }) }) describe("image handling", () => { diff --git a/src/api/providers/bedrock.ts b/src/api/providers/bedrock.ts index faaee0360f..38f28a5867 100644 --- a/src/api/providers/bedrock.ts +++ b/src/api/providers/bedrock.ts @@ -808,6 +808,8 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH * * This matches ARNs like: * - Foundation Model: arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-v2 + * - GovCloud Foundation Model: arn:aws-us-gov:bedrock:us-gov-west-1::foundation-model/anthropic.claude-v2 + * - China Foundation Model: arn:aws-cn:bedrock:cn-north-1::foundation-model/anthropic.claude-v2 * - Prompt Router: arn:aws:bedrock:us-west-2:123456789012:prompt-router/anthropic-claude * - Inference Profile: arn:aws:bedrock:us-west-2:123456789012:inference-profile/anthropic.claude-v2 * - Cross Region Inference Profile: arn:aws:bedrock:us-west-2:123456789012:inference-profile/us.anthropic.claude-3-5-sonnet-20241022-v2:0 @@ -815,13 +817,15 @@ export class AwsBedrockHandler extends BaseProvider implements SingleCompletionH * - Imported Model: arn:aws:bedrock:us-west-2:123456789012:imported-model/my-imported-model * * match[0] - The entire matched string - * match[1] - The region (e.g., "us-east-1") + * match[1] - The region (e.g., "us-east-1", "us-gov-west-1", "cn-north-1") * match[2] - The account ID (can be empty string for AWS-managed resources) * match[3] - The resource type (e.g., "foundation-model") * match[4] - The resource ID (e.g., "anthropic.claude-3-sonnet-20240229-v1:0") */ - const arnRegex = /^arn:aws:(?:bedrock|sagemaker):([^:]+):([^:]*):(?:([^\/]+)\/([\w\.\-:]+)|([^\/]+))$/ + // Support standard AWS (aws), GovCloud (aws-us-gov), and China (aws-cn) partitions + const arnRegex = + /^arn:(?:aws|aws-us-gov|aws-cn):(?:bedrock|sagemaker):([^:]+):([^:]*):(?:([^\/]+)\/([\w\.\-:]+)|([^\/]+))$/ let match = arn.match(arnRegex) if (match && match[1] && match[3] && match[4]) { diff --git a/webview-ui/src/utils/__tests__/validate.test.ts b/webview-ui/src/utils/__tests__/validate.spec.ts similarity index 100% rename from webview-ui/src/utils/__tests__/validate.test.ts rename to webview-ui/src/utils/__tests__/validate.spec.ts diff --git a/webview-ui/src/utils/__tests__/validateBedrockArn.spec.ts b/webview-ui/src/utils/__tests__/validateBedrockArn.spec.ts new file mode 100644 index 0000000000..d9d5c326d2 --- /dev/null +++ b/webview-ui/src/utils/__tests__/validateBedrockArn.spec.ts @@ -0,0 +1,190 @@ +import { vi } from "vitest" +import { validateBedrockArn } from "../validate" + +// Mock i18next to return predictable error messages +vi.mock("i18next", () => ({ + default: { + t: (key: string, params?: any) => { + if (key === "settings:validation.arn.invalidFormat") { + return "Invalid ARN format" + } + if (key === "settings:validation.arn.regionMismatch") { + return `Region mismatch: ARN region ${params?.arnRegion} does not match ${params?.region}` + } + return key + }, + }, +})) + +describe("validateBedrockArn", () => { + describe("Standard AWS partition ARNs", () => { + it("should validate standard AWS Bedrock ARNs", () => { + const arn = "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-sonnet-20240229-v1:0" + const result = validateBedrockArn(arn) + + expect(result.isValid).toBe(true) + expect(result.arnRegion).toBe("us-east-1") + expect(result.errorMessage).toBeUndefined() + }) + + it("should validate ARNs with account IDs", () => { + const arn = "arn:aws:bedrock:us-west-2:123456789012:inference-profile/custom-profile" + const result = validateBedrockArn(arn) + + expect(result.isValid).toBe(true) + expect(result.arnRegion).toBe("us-west-2") + expect(result.errorMessage).toBeUndefined() + }) + + it("should validate Sagemaker ARNs", () => { + const arn = "arn:aws:sagemaker:eu-west-1:123456789012:endpoint/my-endpoint" + const result = validateBedrockArn(arn) + + expect(result.isValid).toBe(true) + expect(result.arnRegion).toBe("eu-west-1") + expect(result.errorMessage).toBeUndefined() + }) + }) + + describe("AWS GovCloud partition ARNs", () => { + it("should validate GovCloud Bedrock ARNs", () => { + const arn = + "arn:aws-us-gov:bedrock:us-gov-west-1:123456789012:inference-profile/us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0" + const result = validateBedrockArn(arn) + + expect(result.isValid).toBe(true) + expect(result.arnRegion).toBe("us-gov-west-1") + expect(result.errorMessage).toBeUndefined() + }) + + it("should validate GovCloud ARNs without account ID", () => { + const arn = "arn:aws-us-gov:bedrock:us-gov-east-1::foundation-model/anthropic.claude-v2" + const result = validateBedrockArn(arn) + + expect(result.isValid).toBe(true) + expect(result.arnRegion).toBe("us-gov-east-1") + expect(result.errorMessage).toBeUndefined() + }) + + it("should validate GovCloud Sagemaker ARNs", () => { + const arn = "arn:aws-us-gov:sagemaker:us-gov-west-1:123456789012:endpoint/gov-endpoint" + const result = validateBedrockArn(arn) + + expect(result.isValid).toBe(true) + expect(result.arnRegion).toBe("us-gov-west-1") + expect(result.errorMessage).toBeUndefined() + }) + }) + + describe("AWS China partition ARNs", () => { + it("should validate China Bedrock ARNs", () => { + const arn = "arn:aws-cn:bedrock:cn-north-1:123456789012:foundation-model/anthropic.claude-v2" + const result = validateBedrockArn(arn) + + expect(result.isValid).toBe(true) + expect(result.arnRegion).toBe("cn-north-1") + expect(result.errorMessage).toBeUndefined() + }) + + it("should validate China ARNs without account ID", () => { + const arn = "arn:aws-cn:bedrock:cn-northwest-1::inference-profile/custom-model" + const result = validateBedrockArn(arn) + + expect(result.isValid).toBe(true) + expect(result.arnRegion).toBe("cn-northwest-1") + expect(result.errorMessage).toBeUndefined() + }) + + it("should validate China Sagemaker ARNs", () => { + const arn = "arn:aws-cn:sagemaker:cn-north-1:123456789012:endpoint/china-endpoint" + const result = validateBedrockArn(arn) + + expect(result.isValid).toBe(true) + expect(result.arnRegion).toBe("cn-north-1") + expect(result.errorMessage).toBeUndefined() + }) + }) + + describe("Region validation", () => { + it("should detect region mismatch for standard AWS", () => { + const arn = "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-v2" + const result = validateBedrockArn(arn, "us-west-2") + + expect(result.isValid).toBe(true) + expect(result.arnRegion).toBe("us-east-1") + // Error message should be defined when there's a region mismatch + expect(result.errorMessage).toBeDefined() + }) + + it("should detect region mismatch for GovCloud", () => { + const arn = "arn:aws-us-gov:bedrock:us-gov-west-1::foundation-model/anthropic.claude-v2" + const result = validateBedrockArn(arn, "us-gov-east-1") + + expect(result.isValid).toBe(true) + expect(result.arnRegion).toBe("us-gov-west-1") + // Error message should be defined when there's a region mismatch + expect(result.errorMessage).toBeDefined() + }) + + it("should detect region mismatch for China", () => { + const arn = "arn:aws-cn:bedrock:cn-north-1::foundation-model/anthropic.claude-v2" + const result = validateBedrockArn(arn, "cn-northwest-1") + + expect(result.isValid).toBe(true) + expect(result.arnRegion).toBe("cn-north-1") + // Error message should be defined when there's a region mismatch + expect(result.errorMessage).toBeDefined() + }) + + it("should pass when regions match", () => { + const arn = "arn:aws-us-gov:bedrock:us-gov-west-1::foundation-model/anthropic.claude-v2" + const result = validateBedrockArn(arn, "us-gov-west-1") + + expect(result.isValid).toBe(true) + expect(result.arnRegion).toBe("us-gov-west-1") + expect(result.errorMessage).toBeUndefined() + }) + }) + + describe("Invalid ARN formats", () => { + it("should reject invalid ARN format", () => { + const arn = "not-an-arn" + const result = validateBedrockArn(arn) + + expect(result.isValid).toBe(false) + expect(result.arnRegion).toBeUndefined() + // Error message should be defined for invalid ARN + expect(result.errorMessage).toBeDefined() + }) + + it("should reject ARN with invalid partition", () => { + const arn = "arn:aws-invalid:bedrock:us-east-1::foundation-model/anthropic.claude-v2" + const result = validateBedrockArn(arn) + + expect(result.isValid).toBe(false) + expect(result.arnRegion).toBeUndefined() + // Error message should be defined for invalid ARN + expect(result.errorMessage).toBeDefined() + }) + + it("should reject ARN with invalid service", () => { + const arn = "arn:aws:invalid-service:us-east-1::foundation-model/anthropic.claude-v2" + const result = validateBedrockArn(arn) + + expect(result.isValid).toBe(false) + expect(result.arnRegion).toBeUndefined() + // Error message should be defined for invalid ARN + expect(result.errorMessage).toBeDefined() + }) + + it("should reject ARN missing resource", () => { + const arn = "arn:aws:bedrock:us-east-1:123456789012:" + const result = validateBedrockArn(arn) + + expect(result.isValid).toBe(false) + expect(result.arnRegion).toBeUndefined() + // Error message should be defined for invalid ARN + expect(result.errorMessage).toBeDefined() + }) + }) +}) diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index d15f82e4ca..81bb014aea 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -225,7 +225,9 @@ function getModelIdForProvider(apiConfiguration: ProviderSettings, provider: Pro */ export function validateBedrockArn(arn: string, region?: string) { // Validate ARN format. - const arnRegex = /^arn:aws:(?:bedrock|sagemaker):([^:]+):([^:]*):(?:([^/]+)\/([\w.\-:]+)|([^/]+))$/ + // Support standard AWS (aws), GovCloud (aws-us-gov), and China (aws-cn) partitions + const arnRegex = + /^arn:(?:aws|aws-us-gov|aws-cn):(?:bedrock|sagemaker):([^:]+):([^:]*):(?:([^/]+)\/([\w.\-:]+)|([^/]+))$/ const match = arn.match(arnRegex) if (!match) {