diff --git a/packages/@n8n/nodes-langchain/credentials/AlibabaCloudApi.credentials.ts b/packages/@n8n/nodes-langchain/credentials/AlibabaCloudApi.credentials.ts
new file mode 100644
index 00000000000..3cb05af7ca7
--- /dev/null
+++ b/packages/@n8n/nodes-langchain/credentials/AlibabaCloudApi.credentials.ts
@@ -0,0 +1,86 @@
+import type {
+ IAuthenticateGeneric,
+ ICredentialTestRequest,
+ ICredentialType,
+ INodeProperties,
+} from 'n8n-workflow';
+
+import { BASE_URL_EXPRESSION } from '../nodes/llms/LmChatAlibabaCloud/alibaba-cloud-base-url';
+
+export class AlibabaCloudApi implements ICredentialType {
+ name = 'alibabaCloudApi';
+
+ displayName = 'Alibaba Cloud';
+
+ documentationUrl = 'alibaba';
+
+ properties: INodeProperties[] = [
+ {
+ displayName: 'API Key',
+ name: 'apiKey',
+ type: 'string',
+ typeOptions: { password: true },
+ required: true,
+ default: '',
+ },
+ {
+ displayName: 'Region',
+ name: 'region',
+ type: 'options',
+ default: 'ap-southeast-1',
+ options: [
+ {
+ name: 'Singapore (International)',
+ value: 'ap-southeast-1',
+ },
+ {
+ name: 'US (Virginia)',
+ value: 'us-east-1',
+ },
+ {
+ name: 'China (Beijing)',
+ value: 'cn-beijing',
+ },
+ {
+ name: 'Hong Kong (China)',
+ value: 'cn-hongkong',
+ },
+ {
+ name: 'Germany (Frankfurt)',
+ value: 'eu-central-1',
+ },
+ ],
+ description: 'The region for the Alibaba Cloud Model Studio API endpoint',
+ },
+ {
+ displayName: 'Workspace ID',
+ name: 'workspaceId',
+ type: 'string',
+ default: '',
+ required: true,
+ displayOptions: {
+ show: {
+ region: ['eu-central-1'],
+ },
+ },
+ description:
+ 'The Workspace ID required for the Germany (Frankfurt) region. Find it in the Model Studio console under the Germany region settings.',
+ },
+ ];
+
+ authenticate: IAuthenticateGeneric = {
+ type: 'generic',
+ properties: {
+ headers: {
+ Authorization: '=Bearer {{$credentials.apiKey}}',
+ },
+ },
+ };
+
+ test: ICredentialTestRequest = {
+ request: {
+ baseURL: BASE_URL_EXPRESSION,
+ url: '/models',
+ },
+ };
+}
diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAlibabaCloud/LmChatAlibabaCloud.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAlibabaCloud/LmChatAlibabaCloud.node.ts
new file mode 100644
index 00000000000..8c86e054223
--- /dev/null
+++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAlibabaCloud/LmChatAlibabaCloud.node.ts
@@ -0,0 +1,272 @@
+import { ChatOpenAI, type ClientOptions } from '@langchain/openai';
+import {
+ getProxyAgent,
+ makeN8nLlmFailedAttemptHandler,
+ N8nLlmTracing,
+ getConnectionHintNoticeField,
+} from '@n8n/ai-utilities';
+import {
+ NodeConnectionTypes,
+ NodeOperationError,
+ type INodeType,
+ type INodeTypeDescription,
+ type ISupplyDataFunctions,
+ type SupplyData,
+} from 'n8n-workflow';
+
+import { BASE_URL_EXPRESSION, getBaseUrl } from './alibaba-cloud-base-url';
+import { openAiFailedAttemptHandler } from '../../vendors/OpenAi/helpers/error-handling';
+
+export class LmChatAlibabaCloud implements INodeType {
+ description: INodeTypeDescription = {
+ displayName: 'Alibaba Cloud Chat Model',
+
+ name: 'lmChatAlibabaCloud',
+ icon: 'file:alibaba.svg',
+ group: ['transform'],
+ version: [1],
+ description: 'For advanced usage with an AI chain',
+ defaults: {
+ name: 'Alibaba Cloud Chat Model',
+ },
+ codex: {
+ categories: ['AI'],
+ subcategories: {
+ AI: ['Language Models', 'Root Nodes'],
+ 'Language Models': ['Chat Models (Recommended)'],
+ },
+ resources: {
+ primaryDocumentation: [
+ {
+ url: 'https://docs.n8n.io/integrations/builtin/cluster-nodes/sub-nodes/n8n-nodes-langchain.lmchatalibabacloud/',
+ },
+ ],
+ },
+ alias: ['qwen', 'dashscope', 'alibaba', 'model studio'],
+ },
+
+ inputs: [],
+
+ outputs: [NodeConnectionTypes.AiLanguageModel],
+ outputNames: ['Model'],
+ credentials: [
+ {
+ name: 'alibabaCloudApi',
+ required: true,
+ },
+ ],
+ requestDefaults: {
+ ignoreHttpStatusErrors: true,
+ baseURL: BASE_URL_EXPRESSION,
+ },
+ properties: [
+ getConnectionHintNoticeField([NodeConnectionTypes.AiChain, NodeConnectionTypes.AiAgent]),
+ {
+ displayName:
+ 'If using JSON response format, you must include word "json" in the prompt in your chain or agent.',
+ name: 'notice',
+ type: 'notice',
+ default: '',
+ displayOptions: {
+ show: {
+ '/options.responseFormat': ['json_object'],
+ },
+ },
+ },
+ {
+ displayName: 'Model',
+ name: 'model',
+ type: 'options',
+ description:
+ 'The model which will generate the completion. Learn more.',
+ typeOptions: {
+ loadOptions: {
+ routing: {
+ request: {
+ method: 'GET',
+ url: '/models',
+ },
+ output: {
+ postReceive: [
+ {
+ type: 'rootProperty',
+ properties: {
+ property: 'data',
+ },
+ },
+ {
+ type: 'setKeyValue',
+ properties: {
+ name: '={{$responseItem.id}}',
+ value: '={{$responseItem.id}}',
+ },
+ },
+ {
+ type: 'sort',
+ properties: {
+ key: 'name',
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ routing: {
+ send: {
+ type: 'body',
+ property: 'model',
+ },
+ },
+ default: 'qwen-plus',
+ },
+ {
+ displayName: 'Options',
+ name: 'options',
+ placeholder: 'Add Option',
+ description: 'Additional options to add',
+ type: 'collection',
+ default: {},
+ options: [
+ {
+ displayName: 'Frequency Penalty',
+ name: 'frequencyPenalty',
+ default: 0,
+ typeOptions: { maxValue: 2, minValue: -2, numberPrecision: 1 },
+ description:
+ "Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim",
+ type: 'number',
+ },
+ {
+ displayName: 'Maximum Number of Tokens',
+ name: 'maxTokens',
+ default: -1,
+ description:
+ 'The maximum number of tokens to generate in the completion. The limit depends on the selected model.',
+ type: 'number',
+ },
+ {
+ displayName: 'Response Format',
+ name: 'responseFormat',
+ default: 'text',
+ type: 'options',
+ options: [
+ {
+ name: 'Text',
+ value: 'text',
+ description: 'Regular text response',
+ },
+ {
+ name: 'JSON',
+ value: 'json_object',
+ description:
+ 'Enables JSON mode, which should guarantee the message the model generates is valid JSON',
+ },
+ ],
+ },
+ {
+ displayName: 'Presence Penalty',
+ name: 'presencePenalty',
+ default: 0,
+ typeOptions: { maxValue: 2, minValue: -2, numberPrecision: 1 },
+ description:
+ "Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics",
+ type: 'number',
+ },
+ {
+ displayName: 'Sampling Temperature',
+ name: 'temperature',
+ default: 0.7,
+ typeOptions: { maxValue: 2, minValue: 0, numberPrecision: 1 },
+ description:
+ 'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive.',
+ type: 'number',
+ },
+ {
+ displayName: 'Timeout',
+ name: 'timeout',
+ default: 360000,
+ description: 'Maximum amount of time a request is allowed to take in milliseconds',
+ type: 'number',
+ },
+ {
+ displayName: 'Max Retries',
+ name: 'maxRetries',
+ default: 2,
+ description: 'Maximum number of retries to attempt',
+ type: 'number',
+ },
+ {
+ displayName: 'Top P',
+ name: 'topP',
+ default: 1,
+ typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 },
+ description:
+ 'Controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered. We generally recommend altering this or temperature but not both.',
+ type: 'number',
+ },
+ ],
+ },
+ ],
+ };
+
+ async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise {
+ const credentials = await this.getCredentials<{
+ apiKey: string;
+ region: string;
+ workspaceId?: string;
+ }>('alibabaCloudApi');
+
+ const modelName = this.getNodeParameter('model', itemIndex) as string;
+
+ const options = this.getNodeParameter('options', itemIndex, {}) as {
+ frequencyPenalty?: number;
+ maxTokens?: number;
+ maxRetries: number;
+ timeout: number;
+ presencePenalty?: number;
+ temperature?: number;
+ topP?: number;
+ responseFormat?: 'text' | 'json_object';
+ };
+
+ if (credentials.region === 'eu-central-1' && !credentials.workspaceId) {
+ throw new NodeOperationError(
+ this.getNode(),
+ 'Workspace ID is required for the Germany (Frankfurt) region',
+ );
+ }
+
+ const baseURL = getBaseUrl(credentials.region, credentials.workspaceId);
+ const timeout = options.timeout;
+ const configuration: ClientOptions = {
+ baseURL,
+ fetchOptions: {
+ dispatcher: getProxyAgent(baseURL, {
+ headersTimeout: timeout,
+ bodyTimeout: timeout,
+ }),
+ },
+ };
+
+ const model = new ChatOpenAI({
+ apiKey: credentials.apiKey,
+ model: modelName,
+ ...options,
+ timeout,
+ maxRetries: options.maxRetries ?? 2,
+ configuration,
+ callbacks: [new N8nLlmTracing(this)],
+ modelKwargs: options.responseFormat
+ ? {
+ response_format: { type: options.responseFormat },
+ }
+ : undefined,
+ onFailedAttempt: makeN8nLlmFailedAttemptHandler(this, openAiFailedAttemptHandler),
+ });
+
+ return {
+ response: model,
+ };
+ }
+}
diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAlibabaCloud/alibaba-cloud-base-url.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAlibabaCloud/alibaba-cloud-base-url.ts
new file mode 100644
index 00000000000..ddfb8b42131
--- /dev/null
+++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAlibabaCloud/alibaba-cloud-base-url.ts
@@ -0,0 +1,14 @@
+export function getBaseUrl(region: string, workspaceId?: string): string {
+ const urls: Record = {
+ 'ap-southeast-1': 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
+ 'us-east-1': 'https://dashscope-us.aliyuncs.com/compatible-mode/v1',
+ 'cn-beijing': 'https://dashscope.aliyuncs.com/compatible-mode/v1',
+ 'cn-hongkong': 'https://cn-hongkong.dashscope.aliyuncs.com/compatible-mode/v1',
+ };
+ if (region === 'eu-central-1') {
+ return 'https://' + workspaceId + '.eu-central-1.maas.aliyuncs.com/compatible-mode/v1';
+ }
+ return urls[region] || urls['ap-southeast-1'];
+}
+
+export const BASE_URL_EXPRESSION = `={{ (${getBaseUrl.toString()})($credentials.region, $credentials.workspaceId) }}`;
diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAlibabaCloud/alibaba.svg b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAlibabaCloud/alibaba.svg
new file mode 100644
index 00000000000..375c6943558
--- /dev/null
+++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAlibabaCloud/alibaba.svg
@@ -0,0 +1 @@
+
diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LmChatAlibabaCloud/test/LmChatAlibabaCloud.test.ts b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAlibabaCloud/test/LmChatAlibabaCloud.test.ts
new file mode 100644
index 00000000000..843ce1ee6dc
--- /dev/null
+++ b/packages/@n8n/nodes-langchain/nodes/llms/LmChatAlibabaCloud/test/LmChatAlibabaCloud.test.ts
@@ -0,0 +1,280 @@
+/* eslint-disable n8n-nodes-base/node-filename-against-convention */
+/* eslint-disable @typescript-eslint/no-unsafe-assignment */
+/* eslint-disable @typescript-eslint/unbound-method */
+import { ChatOpenAI } from '@langchain/openai';
+import { makeN8nLlmFailedAttemptHandler, N8nLlmTracing, getProxyAgent } from '@n8n/ai-utilities';
+import { createMockExecuteFunction } from 'n8n-nodes-base/test/nodes/Helpers';
+import type { INode, ISupplyDataFunctions } from 'n8n-workflow';
+
+import { LmChatAlibabaCloud } from '../LmChatAlibabaCloud.node';
+
+jest.mock('@langchain/openai');
+jest.mock('@n8n/ai-utilities');
+
+const MockedChatOpenAI = jest.mocked(ChatOpenAI);
+const MockedN8nLlmTracing = jest.mocked(N8nLlmTracing);
+const mockedMakeN8nLlmFailedAttemptHandler = jest.mocked(makeN8nLlmFailedAttemptHandler);
+const mockedGetProxyAgent = jest.mocked(getProxyAgent);
+
+describe('LmChatAlibabaCloud', () => {
+ let node: LmChatAlibabaCloud;
+
+ const mockNodeDef: INode = {
+ id: '1',
+ name: 'Alibaba Cloud Chat Model',
+ typeVersion: 1,
+ type: '@n8n/n8n-nodes-langchain.lmChatAlibabaCloud',
+ position: [0, 0],
+ parameters: {},
+ };
+
+ const setupMockContext = (nodeOverrides: Partial = {}) => {
+ const nodeDef = { ...mockNodeDef, ...nodeOverrides };
+ const ctx = createMockExecuteFunction(
+ {},
+ nodeDef,
+ ) as jest.Mocked;
+
+ ctx.getCredentials = jest.fn().mockResolvedValue({
+ apiKey: 'test-dashscope-key',
+ region: 'ap-southeast-1',
+ });
+ ctx.getNode = jest.fn().mockReturnValue(nodeDef);
+ ctx.getNodeParameter = jest.fn().mockImplementation((paramName: string) => {
+ if (paramName === 'model') return 'qwen-plus';
+ if (paramName === 'options') return {};
+ return undefined;
+ });
+
+ MockedN8nLlmTracing.mockImplementation(() => ({}) as unknown as N8nLlmTracing);
+ mockedMakeN8nLlmFailedAttemptHandler.mockReturnValue(jest.fn());
+ mockedGetProxyAgent.mockReturnValue({} as any);
+ return ctx;
+ };
+
+ beforeEach(() => {
+ node = new LmChatAlibabaCloud();
+ jest.clearAllMocks();
+ });
+
+ describe('node description', () => {
+ it('should have correct node properties', () => {
+ expect(node.description).toMatchObject({
+ displayName: 'Alibaba Cloud Chat Model',
+ name: 'lmChatAlibabaCloud',
+ group: ['transform'],
+ version: [1],
+ });
+ });
+
+ it('should require alibabaCloudApi credentials', () => {
+ expect(node.description.credentials).toEqual([{ name: 'alibabaCloudApi', required: true }]);
+ });
+
+ it('should output ai_languageModel', () => {
+ expect(node.description.outputs).toEqual(['ai_languageModel']);
+ expect(node.description.outputNames).toEqual(['Model']);
+ });
+ });
+
+ describe('supplyData', () => {
+ it('should create ChatOpenAI with DashScope base URL', async () => {
+ const ctx = setupMockContext();
+
+ const result = await node.supplyData.call(ctx, 0);
+
+ expect(ctx.getCredentials).toHaveBeenCalledWith('alibabaCloudApi');
+ expect(MockedChatOpenAI).toHaveBeenCalledWith(
+ expect.objectContaining({
+ apiKey: 'test-dashscope-key',
+ model: 'qwen-plus',
+ maxRetries: 2,
+ callbacks: expect.arrayContaining([expect.any(Object)]),
+ onFailedAttempt: expect.any(Function),
+ configuration: expect.objectContaining({
+ baseURL: 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
+ }),
+ }),
+ );
+ expect(result).toEqual({ response: expect.any(Object) });
+ });
+
+ it('should pass options to ChatOpenAI', async () => {
+ const ctx = setupMockContext();
+ ctx.getNodeParameter = jest.fn().mockImplementation((paramName: string) => {
+ if (paramName === 'model') return 'qwen-turbo';
+ if (paramName === 'options')
+ return {
+ temperature: 0.5,
+ maxTokens: 2000,
+ topP: 0.9,
+ frequencyPenalty: 0.3,
+ presencePenalty: 0.2,
+ timeout: 60000,
+ maxRetries: 5,
+ };
+ return undefined;
+ });
+
+ await node.supplyData.call(ctx, 0);
+
+ expect(MockedChatOpenAI).toHaveBeenCalledWith(
+ expect.objectContaining({
+ model: 'qwen-turbo',
+ temperature: 0.5,
+ maxTokens: 2000,
+ topP: 0.9,
+ frequencyPenalty: 0.3,
+ presencePenalty: 0.2,
+ timeout: 60000,
+ maxRetries: 5,
+ }),
+ );
+ });
+
+ it('should set response_format in modelKwargs when responseFormat is provided', async () => {
+ const ctx = setupMockContext();
+ ctx.getNodeParameter = jest.fn().mockImplementation((paramName: string) => {
+ if (paramName === 'model') return 'qwen-plus';
+ if (paramName === 'options') return { responseFormat: 'json_object' };
+ return undefined;
+ });
+
+ await node.supplyData.call(ctx, 0);
+
+ expect(MockedChatOpenAI).toHaveBeenCalledWith(
+ expect.objectContaining({
+ modelKwargs: { response_format: { type: 'json_object' } },
+ }),
+ );
+ });
+
+ it('should not set modelKwargs when no responseFormat', async () => {
+ const ctx = setupMockContext();
+
+ await node.supplyData.call(ctx, 0);
+
+ expect(MockedChatOpenAI).toHaveBeenCalledWith(
+ expect.objectContaining({
+ modelKwargs: undefined,
+ }),
+ );
+ });
+
+ it('should configure proxy agent with credentials URL', async () => {
+ const ctx = setupMockContext();
+
+ await node.supplyData.call(ctx, 0);
+
+ expect(mockedGetProxyAgent).toHaveBeenCalledWith(
+ 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
+ expect.objectContaining({
+ headersTimeout: undefined,
+ bodyTimeout: undefined,
+ }),
+ );
+ });
+
+ it('should configure proxy agent with custom timeout', async () => {
+ const ctx = setupMockContext();
+ ctx.getNodeParameter = jest.fn().mockImplementation((paramName: string) => {
+ if (paramName === 'model') return 'qwen-plus';
+ if (paramName === 'options') return { timeout: 120000 };
+ return undefined;
+ });
+
+ await node.supplyData.call(ctx, 0);
+
+ expect(mockedGetProxyAgent).toHaveBeenCalledWith(
+ 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
+ expect.objectContaining({
+ headersTimeout: 120000,
+ bodyTimeout: 120000,
+ }),
+ );
+ });
+
+ it('should use US region base URL', async () => {
+ const ctx = setupMockContext();
+ ctx.getCredentials = jest.fn().mockResolvedValue({
+ apiKey: 'test-key',
+ region: 'us-east-1',
+ });
+
+ await node.supplyData.call(ctx, 0);
+
+ expect(MockedChatOpenAI).toHaveBeenCalledWith(
+ expect.objectContaining({
+ configuration: expect.objectContaining({
+ baseURL: 'https://dashscope-us.aliyuncs.com/compatible-mode/v1',
+ }),
+ }),
+ );
+ });
+
+ it('should use Frankfurt region base URL with workspace ID', async () => {
+ const ctx = setupMockContext();
+ ctx.getCredentials = jest.fn().mockResolvedValue({
+ apiKey: 'test-key',
+ region: 'eu-central-1',
+ workspaceId: 'ws-abc123',
+ });
+
+ await node.supplyData.call(ctx, 0);
+
+ expect(MockedChatOpenAI).toHaveBeenCalledWith(
+ expect.objectContaining({
+ configuration: expect.objectContaining({
+ baseURL: 'https://ws-abc123.eu-central-1.maas.aliyuncs.com/compatible-mode/v1',
+ }),
+ }),
+ );
+ });
+
+ it('should use China (Beijing) region base URL', async () => {
+ const ctx = setupMockContext();
+ ctx.getCredentials = jest.fn().mockResolvedValue({
+ apiKey: 'test-key',
+ region: 'cn-beijing',
+ });
+
+ await node.supplyData.call(ctx, 0);
+
+ expect(MockedChatOpenAI).toHaveBeenCalledWith(
+ expect.objectContaining({
+ configuration: expect.objectContaining({
+ baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
+ }),
+ }),
+ );
+ });
+
+ it('should use Hong Kong region base URL', async () => {
+ const ctx = setupMockContext();
+ ctx.getCredentials = jest.fn().mockResolvedValue({
+ apiKey: 'test-key',
+ region: 'cn-hongkong',
+ });
+
+ await node.supplyData.call(ctx, 0);
+
+ expect(MockedChatOpenAI).toHaveBeenCalledWith(
+ expect.objectContaining({
+ configuration: expect.objectContaining({
+ baseURL: 'https://cn-hongkong.dashscope.aliyuncs.com/compatible-mode/v1',
+ }),
+ }),
+ );
+ });
+
+ it('should throw when eu-central-1 is selected without workspaceId', async () => {
+ const ctx = setupMockContext();
+ ctx.getCredentials = jest.fn().mockResolvedValue({
+ apiKey: 'test-key',
+ region: 'eu-central-1',
+ });
+
+ await expect(node.supplyData.call(ctx, 0)).rejects.toThrow('Workspace ID');
+ });
+ });
+});
diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json
index ea50a77f178..e5129fcadd5 100644
--- a/packages/@n8n/nodes-langchain/package.json
+++ b/packages/@n8n/nodes-langchain/package.json
@@ -44,6 +44,7 @@
"n8n": {
"n8nNodesApiVersion": 1,
"credentials": [
+ "dist/credentials/AlibabaCloudApi.credentials.js",
"dist/credentials/AnthropicApi.credentials.js",
"dist/credentials/AzureAiSearchApi.credentials.js",
"dist/credentials/AzureOpenAiApi.credentials.js",
@@ -107,6 +108,7 @@
"dist/nodes/embeddings/EmbeddingsOpenAI/EmbeddingsOpenAi.node.js",
"dist/nodes/embeddings/EmbeddingsLemonade/EmbeddingsLemonade.node.js",
"dist/nodes/embeddings/EmbeddingsOllama/EmbeddingsOllama.node.js",
+ "dist/nodes/llms/LmChatAlibabaCloud/LmChatAlibabaCloud.node.js",
"dist/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.js",
"dist/nodes/llms/LmChatAzureOpenAi/LmChatAzureOpenAi.node.js",
"dist/nodes/llms/LmChatAwsBedrock/LmChatAwsBedrock.node.js",