Skip to content

Commit 4901814

Browse files
authored
fix(AWS Bedrock Chat Model Node): Extract region from modelName ARNs (#26972)
1 parent 81142c4 commit 4901814

2 files changed

Lines changed: 180 additions & 2 deletions

File tree

packages/@n8n/nodes-langchain/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,10 +238,18 @@ export class LmChatAwsBedrock implements INodeType {
238238
maxTokensToSample: number;
239239
};
240240

241+
// If the model is specified as a full ARN, extract the region from it
242+
// ARN format: arn:aws:bedrock:<region>:<account-id>:inference-profile/<profile-id>
243+
let region = credentials.region;
244+
const arnMatch = modelName.match(/^arn:aws:bedrock:([a-z0-9-]+):/);
245+
if (arnMatch) {
246+
region = arnMatch[1];
247+
}
248+
241249
// We set-up client manually to pass httpAgent and httpsAgent
242250
const proxyAgent = getNodeProxyAgent();
243251
const clientConfig: BedrockRuntimeClientConfig = {
244-
region: credentials.region,
252+
region,
245253
credentials: {
246254
secretAccessKey: credentials.secretAccessKey,
247255
accessKeyId: credentials.accessKeyId,
@@ -262,7 +270,7 @@ export class LmChatAwsBedrock implements INodeType {
262270
const model = new ChatBedrockConverse({
263271
client,
264272
model: modelName,
265-
region: credentials.region,
273+
region,
266274
temperature: options.temperature,
267275
maxTokens: options.maxTokensToSample,
268276
callbacks: [new N8nLlmTracing(this)],
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { BedrockRuntimeClient } from '@aws-sdk/client-bedrock-runtime';
2+
import { ChatBedrockConverse } from '@langchain/aws';
3+
import {
4+
makeN8nLlmFailedAttemptHandler,
5+
N8nLlmTracing,
6+
getNodeProxyAgent,
7+
} from '@n8n/ai-utilities';
8+
import { createMockExecuteFunction } from 'n8n-nodes-base/test/nodes/Helpers';
9+
import type { INode, ISupplyDataFunctions } from 'n8n-workflow';
10+
11+
import { LmChatAwsBedrock } from '../LmChatAwsBedrock.node';
12+
13+
jest.mock('@aws-sdk/client-bedrock-runtime');
14+
jest.mock('@langchain/aws');
15+
jest.mock('@n8n/ai-utilities', () => ({
16+
getConnectionHintNoticeField: jest
17+
.fn()
18+
.mockReturnValue({ displayName: '', name: 'notice', type: 'notice', default: '' }),
19+
makeN8nLlmFailedAttemptHandler: jest.fn(),
20+
N8nLlmTracing: jest.fn(),
21+
getNodeProxyAgent: jest.fn(),
22+
}));
23+
24+
const MockedBedrockRuntimeClient = jest.mocked(BedrockRuntimeClient);
25+
const MockedChatBedrockConverse = jest.mocked(ChatBedrockConverse);
26+
const MockedN8nLlmTracing = jest.mocked(N8nLlmTracing);
27+
const mockedMakeN8nLlmFailedAttemptHandler = jest.mocked(makeN8nLlmFailedAttemptHandler);
28+
const mockedGetNodeProxyAgent = jest.mocked(getNodeProxyAgent);
29+
30+
describe('LmChatAwsBedrock', () => {
31+
let node: LmChatAwsBedrock;
32+
let mockContext: jest.Mocked<ISupplyDataFunctions>;
33+
34+
const mockNode: INode = {
35+
id: '1',
36+
name: 'AWS Bedrock Chat Model',
37+
typeVersion: 1.1,
38+
type: 'n8n-nodes-langchain.lmChatAwsBedrock',
39+
position: [0, 0],
40+
parameters: {},
41+
};
42+
43+
const defaultCredentials = {
44+
region: 'us-east-1',
45+
secretAccessKey: 'test-secret',
46+
accessKeyId: 'test-key',
47+
sessionToken: '',
48+
};
49+
50+
const setupMockContext = (overrides: { credentials?: Record<string, unknown> } = {}) => {
51+
mockContext = createMockExecuteFunction<ISupplyDataFunctions>(
52+
{},
53+
mockNode,
54+
) as jest.Mocked<ISupplyDataFunctions>;
55+
56+
mockContext.getCredentials = jest
57+
.fn()
58+
.mockResolvedValue(overrides.credentials ?? defaultCredentials);
59+
mockContext.getNode = jest.fn().mockReturnValue(mockNode);
60+
mockContext.getNodeParameter = jest.fn();
61+
62+
MockedN8nLlmTracing.mockImplementation(() => ({}) as N8nLlmTracing);
63+
mockedMakeN8nLlmFailedAttemptHandler.mockReturnValue(jest.fn());
64+
mockedGetNodeProxyAgent.mockReturnValue(undefined);
65+
MockedBedrockRuntimeClient.mockImplementation(() => ({}) as BedrockRuntimeClient);
66+
MockedChatBedrockConverse.mockImplementation(() => ({}) as unknown as ChatBedrockConverse);
67+
68+
return mockContext;
69+
};
70+
71+
beforeEach(() => {
72+
node = new LmChatAwsBedrock();
73+
jest.clearAllMocks();
74+
});
75+
76+
describe('supplyData', () => {
77+
it('should use credential region for standard model IDs', async () => {
78+
const ctx = setupMockContext();
79+
ctx.getNodeParameter = jest.fn().mockImplementation((paramName: string) => {
80+
if (paramName === 'model') return 'amazon.nova-pro-v1:0';
81+
if (paramName === 'options') return {};
82+
return undefined;
83+
});
84+
85+
await node.supplyData.call(ctx, 0);
86+
87+
expect(MockedBedrockRuntimeClient).toHaveBeenCalledWith(
88+
expect.objectContaining({ region: 'us-east-1' }),
89+
);
90+
expect(MockedChatBedrockConverse).toHaveBeenCalledWith(
91+
expect.objectContaining({ region: 'us-east-1' }),
92+
);
93+
});
94+
95+
it('should use credential region for inference profile IDs (not ARNs)', async () => {
96+
const ctx = setupMockContext();
97+
ctx.getNodeParameter = jest.fn().mockImplementation((paramName: string) => {
98+
if (paramName === 'model') return 'eu.amazon.nova-pro-v1:0';
99+
if (paramName === 'options') return {};
100+
return undefined;
101+
});
102+
103+
await node.supplyData.call(ctx, 0);
104+
105+
expect(MockedBedrockRuntimeClient).toHaveBeenCalledWith(
106+
expect.objectContaining({ region: 'us-east-1' }),
107+
);
108+
expect(MockedChatBedrockConverse).toHaveBeenCalledWith(
109+
expect.objectContaining({ region: 'us-east-1' }),
110+
);
111+
});
112+
113+
it('should extract region from inference profile ARN and use it', async () => {
114+
const ctx = setupMockContext();
115+
ctx.getNodeParameter = jest.fn().mockImplementation((paramName: string) => {
116+
if (paramName === 'model')
117+
return 'arn:aws:bedrock:eu-west-3:851725222089:inference-profile/eu.amazon.nova-pro-v1:0';
118+
if (paramName === 'options') return {};
119+
return undefined;
120+
});
121+
122+
await node.supplyData.call(ctx, 0);
123+
124+
expect(MockedBedrockRuntimeClient).toHaveBeenCalledWith(
125+
expect.objectContaining({ region: 'eu-west-3' }),
126+
);
127+
expect(MockedChatBedrockConverse).toHaveBeenCalledWith(
128+
expect.objectContaining({ region: 'eu-west-3' }),
129+
);
130+
});
131+
132+
it('should extract region from foundation model ARN', async () => {
133+
const ctx = setupMockContext();
134+
ctx.getNodeParameter = jest.fn().mockImplementation((paramName: string) => {
135+
if (paramName === 'model')
136+
return 'arn:aws:bedrock:ap-southeast-1::foundation-model/anthropic.claude-v2';
137+
if (paramName === 'options') return {};
138+
return undefined;
139+
});
140+
141+
await node.supplyData.call(ctx, 0);
142+
143+
expect(MockedBedrockRuntimeClient).toHaveBeenCalledWith(
144+
expect.objectContaining({ region: 'ap-southeast-1' }),
145+
);
146+
expect(MockedChatBedrockConverse).toHaveBeenCalledWith(
147+
expect.objectContaining({ region: 'ap-southeast-1' }),
148+
);
149+
});
150+
151+
it('should pass model name and options to ChatBedrockConverse', async () => {
152+
const ctx = setupMockContext();
153+
ctx.getNodeParameter = jest.fn().mockImplementation((paramName: string) => {
154+
if (paramName === 'model') return 'amazon.nova-pro-v1:0';
155+
if (paramName === 'options') return { temperature: 0.5, maxTokensToSample: 1000 };
156+
return undefined;
157+
});
158+
159+
await node.supplyData.call(ctx, 0);
160+
161+
expect(MockedChatBedrockConverse).toHaveBeenCalledWith(
162+
expect.objectContaining({
163+
model: 'amazon.nova-pro-v1:0',
164+
temperature: 0.5,
165+
maxTokens: 1000,
166+
}),
167+
);
168+
});
169+
});
170+
});

0 commit comments

Comments
 (0)