diff --git a/.changeset/blue-waves-travel.md b/.changeset/blue-waves-travel.md new file mode 100644 index 00000000000..9f7821250cb --- /dev/null +++ b/.changeset/blue-waves-travel.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/ai-constructs': minor +--- + +add support for bedrock system cross-region inference profiles diff --git a/.eslint_dictionary.json b/.eslint_dictionary.json index 73bc88ff5ff..0bcb1afc224 100644 --- a/.eslint_dictionary.json +++ b/.eslint_dictionary.json @@ -8,6 +8,7 @@ "ampx", "anonymize", "anthropic", + "apac", "apns", "apollo", "appleid", diff --git a/packages/ai-constructs/src/conversation/conversation_handler_construct.test.ts b/packages/ai-constructs/src/conversation/conversation_handler_construct.test.ts index d274e169f0f..09e41d07170 100644 --- a/packages/ai-constructs/src/conversation/conversation_handler_construct.test.ts +++ b/packages/ai-constructs/src/conversation/conversation_handler_construct.test.ts @@ -414,4 +414,145 @@ void describe('Conversation Handler Function construct', () => { }); }); }); + + void describe('cross-region models', () => { + void it('creates handler with access to eu cross-region models and inference profiles', () => { + const app = new App(); + const stack = new Stack(app, 'TestStack', { + env: { region: 'eu-west-1' }, + }); + new ConversationHandlerFunction(stack, 'conversationHandler', { + models: [ + { + modelId: 'eu.anthropic.claude-3-5-sonnet-20240620-v1:0', + }, + ], + }); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'bedrock:InvokeModel', + 'bedrock:InvokeModelWithResponseStream', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:bedrock:eu-west-1:', + { + Ref: 'AWS::AccountId', + }, + ':inference-profile/eu.anthropic.claude-3-5-sonnet-20240620-v1:0', + ], + ], + }, + 'arn:aws:bedrock:eu-west-1::foundation-model/anthropic.claude-3-5-sonnet-20240620-v1:0', + 'arn:aws:bedrock:eu-west-3::foundation-model/anthropic.claude-3-5-sonnet-20240620-v1:0', + 'arn:aws:bedrock:eu-central-1::foundation-model/anthropic.claude-3-5-sonnet-20240620-v1:0', + ], + }, + ], + }, + }); + }); + + void it('creates handler with access to us cross-region models and inference profiles', () => { + const app = new App(); + const stack = new Stack(app, 'TestStack', { + env: { region: 'us-east-1' }, + }); + new ConversationHandlerFunction(stack, 'conversationHandler', { + models: [ + { + modelId: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0', + }, + ], + }); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'bedrock:InvokeModel', + 'bedrock:InvokeModelWithResponseStream', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:bedrock:us-east-1:', + { + Ref: 'AWS::AccountId', + }, + ':inference-profile/us.anthropic.claude-3-7-sonnet-20250219-v1:0', + ], + ], + }, + 'arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-7-sonnet-20250219-v1:0', + 'arn:aws:bedrock:us-east-2::foundation-model/anthropic.claude-3-7-sonnet-20250219-v1:0', + 'arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-3-7-sonnet-20250219-v1:0', + ], + }, + ], + }, + }); + }); + + void it('creates handler with access to apac cross-region models and inference profiles', () => { + const app = new App(); + const stack = new Stack(app, 'TestStack', { + env: { region: 'ap-northeast-1' }, + }); + new ConversationHandlerFunction(stack, 'conversationHandler', { + models: [ + { + modelId: 'apac.anthropic.claude-3-haiku-20240307-v1:0', + }, + ], + }); + + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'bedrock:InvokeModel', + 'bedrock:InvokeModelWithResponseStream', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:aws:bedrock:ap-northeast-1:', + { + Ref: 'AWS::AccountId', + }, + ':inference-profile/apac.anthropic.claude-3-haiku-20240307-v1:0', + ], + ], + }, + 'arn:aws:bedrock:ap-northeast-1::foundation-model/anthropic.claude-3-haiku-20240307-v1:0', + 'arn:aws:bedrock:ap-northeast-2::foundation-model/anthropic.claude-3-haiku-20240307-v1:0', + 'arn:aws:bedrock:ap-southeast-1::foundation-model/anthropic.claude-3-haiku-20240307-v1:0', + 'arn:aws:bedrock:ap-southeast-2::foundation-model/anthropic.claude-3-haiku-20240307-v1:0', + ], + }, + ], + }, + }); + }); + }); }); diff --git a/packages/ai-constructs/src/conversation/conversation_handler_construct.ts b/packages/ai-constructs/src/conversation/conversation_handler_construct.ts index 36c19c66d35..ecaaa260d63 100644 --- a/packages/ai-constructs/src/conversation/conversation_handler_construct.ts +++ b/packages/ai-constructs/src/conversation/conversation_handler_construct.ts @@ -130,12 +130,7 @@ export class ConversationHandlerFunction ); if (this.props.models && this.props.models.length > 0) { - const resources = this.props.models.map( - (model) => - `arn:aws:bedrock:${ - model.region ?? Stack.of(this).region - }::foundation-model/${model.modelId}` - ); + const resources = this.generateIamPolicyResourceBlocks(this.props.models); conversationHandler.addToRolePolicy( new PolicyStatement({ effect: Effect.ALLOW, @@ -160,6 +155,53 @@ export class ConversationHandlerFunction this.storeOutput(this.props.outputStorageStrategy); } + private generateIamPolicyResourceBlocks = ( + models: { modelId: string; region?: string }[] + ): string[] => { + const crossRegionInferenceProfileRegions: Record = { + 'eu.': ['eu-west-1', 'eu-west-3', 'eu-central-1'], + 'us.': ['us-east-1', 'us-east-2', 'us-west-2'], + 'apac.': [ + 'ap-northeast-1', + 'ap-northeast-2', + 'ap-southeast-1', + 'ap-southeast-2', + ], + }; + const resourceBlocks = models.map((model) => { + const region = model.region ?? Stack.of(this).region; + const modelPrefix = Object.keys(crossRegionInferenceProfileRegions).find( + (prefix) => model.modelId.startsWith(prefix) + ); + + if (modelPrefix) { + // For cross-region models, generate resource blocks for all supported regions + const foundationModelResourceBlocks = + crossRegionInferenceProfileRegions[modelPrefix].map( + (region) => + `arn:aws:bedrock:${region}::foundation-model/${model.modelId.substring( + modelPrefix.length + )}` + ); + + // For cross-region models, also generate inference profile resource blocks + const inferenceProfileResourceBlock = `arn:aws:bedrock:${region}:${ + Stack.of(this).account + }:inference-profile/${model.modelId}`; + + return [ + inferenceProfileResourceBlock, + ...foundationModelResourceBlocks, + ]; + } + + // For non-cross-region models, use the specified or stack region + return [`arn:aws:bedrock:${region}::foundation-model/${model.modelId}`]; + }); + + return resourceBlocks.flat(); + }; + /** * Append conversation handler to defined functions. */