mirror of
https://github.com/n8n-io/n8n
synced 2026-04-21 15:47:20 +00:00
fix(Alibaba Cloud Chat Model Node): Add credential-level url field for AI gateway compatibility (#28697)
This commit is contained in:
parent
d14f2546a1
commit
dd6c28c6d1
6 changed files with 62 additions and 67 deletions
|
|
@ -5,7 +5,10 @@ import type {
|
|||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { BASE_URL_EXPRESSION } from '../nodes/llms/LmChatAlibabaCloud/alibaba-cloud-base-url';
|
||||
import {
|
||||
COMPATIBLE_MODE_SUFFIX,
|
||||
REGION_BASE_HOSTS,
|
||||
} from '../nodes/llms/LmChatAlibabaCloud/alibaba-cloud-base-url';
|
||||
|
||||
export class AlibabaCloudApi implements ICredentialType {
|
||||
name = 'alibabaCloudApi';
|
||||
|
|
@ -66,6 +69,12 @@ export class AlibabaCloudApi implements ICredentialType {
|
|||
description:
|
||||
'The Workspace ID required for the Germany (Frankfurt) region. Find it in the Model Studio console under the Germany region settings.',
|
||||
},
|
||||
{
|
||||
displayName: 'Base URL',
|
||||
name: 'url',
|
||||
type: 'hidden',
|
||||
default: `={{ (() => { const hosts = ${JSON.stringify(REGION_BASE_HOSTS)}; const region = $self.region; if (region === "eu-central-1") { return "https://" + $self.workspaceId + ".eu-central-1.maas.aliyuncs.com"; } return hosts[region] || hosts["ap-southeast-1"]; })() }}`,
|
||||
},
|
||||
];
|
||||
|
||||
authenticate: IAuthenticateGeneric = {
|
||||
|
|
@ -79,8 +88,8 @@ export class AlibabaCloudApi implements ICredentialType {
|
|||
|
||||
test: ICredentialTestRequest = {
|
||||
request: {
|
||||
baseURL: BASE_URL_EXPRESSION,
|
||||
url: '/models',
|
||||
baseURL: '={{ $credentials.url }}',
|
||||
url: `${COMPATIBLE_MODE_SUFFIX}/models`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
type SupplyData,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { BASE_URL_EXPRESSION, getBaseUrl } from './alibaba-cloud-base-url';
|
||||
import { COMPATIBLE_MODE_SUFFIX } from './alibaba-cloud-base-url';
|
||||
import { openAiFailedAttemptHandler } from '../../vendors/OpenAi/helpers/error-handling';
|
||||
|
||||
export class LmChatAlibabaCloud implements INodeType {
|
||||
|
|
@ -57,7 +57,7 @@ export class LmChatAlibabaCloud implements INodeType {
|
|||
],
|
||||
requestDefaults: {
|
||||
ignoreHttpStatusErrors: true,
|
||||
baseURL: BASE_URL_EXPRESSION,
|
||||
baseURL: `={{ $credentials?.url + "${COMPATIBLE_MODE_SUFFIX}" }}`,
|
||||
},
|
||||
properties: [
|
||||
getConnectionHintNoticeField([NodeConnectionTypes.AiChain, NodeConnectionTypes.AiAgent]),
|
||||
|
|
@ -213,6 +213,7 @@ export class LmChatAlibabaCloud implements INodeType {
|
|||
async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise<SupplyData> {
|
||||
const credentials = await this.getCredentials<{
|
||||
apiKey: string;
|
||||
url: string;
|
||||
region: string;
|
||||
workspaceId?: string;
|
||||
}>('alibabaCloudApi');
|
||||
|
|
@ -237,7 +238,7 @@ export class LmChatAlibabaCloud implements INodeType {
|
|||
);
|
||||
}
|
||||
|
||||
const baseURL = getBaseUrl(credentials.region, credentials.workspaceId);
|
||||
const baseURL = credentials.url + COMPATIBLE_MODE_SUFFIX;
|
||||
const timeout = options.timeout;
|
||||
const configuration: ClientOptions = {
|
||||
baseURL,
|
||||
|
|
|
|||
|
|
@ -1,28 +1,8 @@
|
|||
const REGION_BASE_HOSTS: Record<string, string> = {
|
||||
export const REGION_BASE_HOSTS: Record<string, string> = {
|
||||
'ap-southeast-1': 'https://dashscope-intl.aliyuncs.com',
|
||||
'us-east-1': 'https://dashscope-us.aliyuncs.com',
|
||||
'cn-beijing': 'https://dashscope.aliyuncs.com',
|
||||
'cn-hongkong': 'https://cn-hongkong.dashscope.aliyuncs.com',
|
||||
};
|
||||
|
||||
const COMPATIBLE_MODE_SUFFIX = '/compatible-mode/v1';
|
||||
|
||||
export function getBaseHost(region: string, workspaceId?: string): string {
|
||||
if (region === 'eu-central-1') {
|
||||
return `https://${workspaceId}.eu-central-1.maas.aliyuncs.com`;
|
||||
}
|
||||
return REGION_BASE_HOSTS[region] ?? REGION_BASE_HOSTS['ap-southeast-1'];
|
||||
}
|
||||
|
||||
export function getBaseUrl(region: string, workspaceId?: string): string {
|
||||
return getBaseHost(region, workspaceId) + COMPATIBLE_MODE_SUFFIX;
|
||||
}
|
||||
|
||||
export const BASE_URL_EXPRESSION = `={{ (() => {
|
||||
const hosts = ${JSON.stringify(REGION_BASE_HOSTS)};
|
||||
const region = $credentials.region;
|
||||
if (region === 'eu-central-1') {
|
||||
return 'https://' + $credentials.workspaceId + '.eu-central-1.maas.aliyuncs.com' + '${COMPATIBLE_MODE_SUFFIX}';
|
||||
}
|
||||
return (hosts[region] || hosts['ap-southeast-1']) + '${COMPATIBLE_MODE_SUFFIX}';
|
||||
})() }}`;
|
||||
export const COMPATIBLE_MODE_SUFFIX = '/compatible-mode/v1';
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ describe('LmChatAlibabaCloud', () => {
|
|||
ctx.getCredentials = jest.fn().mockResolvedValue({
|
||||
apiKey: 'test-dashscope-key',
|
||||
region: 'ap-southeast-1',
|
||||
url: 'https://dashscope-intl.aliyuncs.com',
|
||||
});
|
||||
ctx.getNode = jest.fn().mockReturnValue(nodeDef);
|
||||
ctx.getNodeParameter = jest.fn().mockImplementation((paramName: string) => {
|
||||
|
|
@ -199,6 +200,7 @@ describe('LmChatAlibabaCloud', () => {
|
|||
ctx.getCredentials = jest.fn().mockResolvedValue({
|
||||
apiKey: 'test-key',
|
||||
region: 'us-east-1',
|
||||
url: 'https://dashscope-us.aliyuncs.com',
|
||||
});
|
||||
|
||||
await node.supplyData.call(ctx, 0);
|
||||
|
|
@ -218,6 +220,7 @@ describe('LmChatAlibabaCloud', () => {
|
|||
apiKey: 'test-key',
|
||||
region: 'eu-central-1',
|
||||
workspaceId: 'ws-abc123',
|
||||
url: 'https://ws-abc123.eu-central-1.maas.aliyuncs.com',
|
||||
});
|
||||
|
||||
await node.supplyData.call(ctx, 0);
|
||||
|
|
@ -236,6 +239,7 @@ describe('LmChatAlibabaCloud', () => {
|
|||
ctx.getCredentials = jest.fn().mockResolvedValue({
|
||||
apiKey: 'test-key',
|
||||
region: 'cn-beijing',
|
||||
url: 'https://dashscope.aliyuncs.com',
|
||||
});
|
||||
|
||||
await node.supplyData.call(ctx, 0);
|
||||
|
|
@ -254,6 +258,7 @@ describe('LmChatAlibabaCloud', () => {
|
|||
ctx.getCredentials = jest.fn().mockResolvedValue({
|
||||
apiKey: 'test-key',
|
||||
region: 'cn-hongkong',
|
||||
url: 'https://cn-hongkong.dashscope.aliyuncs.com',
|
||||
});
|
||||
|
||||
await node.supplyData.call(ctx, 0);
|
||||
|
|
@ -267,11 +272,31 @@ describe('LmChatAlibabaCloud', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should use gateway URL when provided via credentials', async () => {
|
||||
const ctx = setupMockContext();
|
||||
ctx.getCredentials = jest.fn().mockResolvedValue({
|
||||
apiKey: 'gateway-jwt-token',
|
||||
url: 'https://gateway.example.com/v1/gateway/alibaba',
|
||||
});
|
||||
|
||||
await node.supplyData.call(ctx, 0);
|
||||
|
||||
expect(MockedChatOpenAI).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
apiKey: 'gateway-jwt-token',
|
||||
configuration: expect.objectContaining({
|
||||
baseURL: 'https://gateway.example.com/v1/gateway/alibaba/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',
|
||||
url: 'https://undefined.eu-central-1.maas.aliyuncs.com',
|
||||
});
|
||||
|
||||
await expect(node.supplyData.call(ctx, 0)).rejects.toThrow('Workspace ID');
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { mockDeep } from 'jest-mock-extended';
|
|||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest, getBaseUrl, pollTaskResult } from '../transport';
|
||||
import { apiRequest, pollTaskResult } from '../transport';
|
||||
|
||||
jest.mock('n8n-workflow', () => {
|
||||
const actual = jest.requireActual('n8n-workflow');
|
||||
|
|
@ -19,7 +19,7 @@ describe('AlicloudModelStudio Transport', () => {
|
|||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
apiKey: 'test-key',
|
||||
region: 'ap-southeast-1',
|
||||
url: 'https://dashscope-intl.aliyuncs.com',
|
||||
});
|
||||
mockExecuteFunctions.getNode.mockReturnValue({
|
||||
id: 'test-node-id',
|
||||
|
|
@ -92,7 +92,7 @@ describe('AlicloudModelStudio Transport', () => {
|
|||
it('should resolve US (Virginia) region to correct base URL', async () => {
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
apiKey: 'test-key',
|
||||
region: 'us-east-1',
|
||||
url: 'https://dashscope-us.aliyuncs.com',
|
||||
});
|
||||
mockExecuteFunctions.helpers.httpRequestWithAuthentication.mockResolvedValue({});
|
||||
|
||||
|
|
@ -109,8 +109,7 @@ describe('AlicloudModelStudio Transport', () => {
|
|||
it('should resolve EU (Frankfurt) region with workspaceId to correct base URL', async () => {
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
apiKey: 'test-key',
|
||||
region: 'eu-central-1',
|
||||
workspaceId: 'ws123',
|
||||
url: 'https://ws123.eu-central-1.maas.aliyuncs.com',
|
||||
});
|
||||
mockExecuteFunctions.helpers.httpRequestWithAuthentication.mockResolvedValue({});
|
||||
|
||||
|
|
@ -123,31 +122,23 @@ describe('AlicloudModelStudio Transport', () => {
|
|||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBaseUrl', () => {
|
||||
it('should default to Singapore when no region is provided', () => {
|
||||
expect(getBaseUrl({})).toBe('https://dashscope-intl.aliyuncs.com');
|
||||
});
|
||||
it('should use gateway URL when provided via credentials', async () => {
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
apiKey: 'gateway-jwt-token',
|
||||
url: 'https://gateway.example.com/v1/gateway/alibaba',
|
||||
});
|
||||
mockExecuteFunctions.helpers.httpRequestWithAuthentication.mockResolvedValue({});
|
||||
|
||||
it.each([
|
||||
['ap-southeast-1', 'https://dashscope-intl.aliyuncs.com'],
|
||||
['us-east-1', 'https://dashscope-us.aliyuncs.com'],
|
||||
['cn-beijing', 'https://dashscope.aliyuncs.com'],
|
||||
['cn-hongkong', 'https://cn-hongkong.dashscope.aliyuncs.com'],
|
||||
])('should return correct URL for region %s', (region, expectedUrl) => {
|
||||
expect(getBaseUrl({ region })).toBe(expectedUrl);
|
||||
});
|
||||
await apiRequest.call(mockExecuteFunctions, 'GET', '/api/v1/tasks/789');
|
||||
|
||||
it('should construct Frankfurt URL with workspaceId', () => {
|
||||
expect(getBaseUrl({ region: 'eu-central-1', workspaceId: 'ws123' })).toBe(
|
||||
'https://ws123.eu-central-1.maas.aliyuncs.com',
|
||||
expect(mockExecuteFunctions.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith(
|
||||
'alibabaCloudApi',
|
||||
expect.objectContaining({
|
||||
url: 'https://gateway.example.com/v1/gateway/alibaba/api/v1/tasks/789',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when eu-central-1 is selected without workspaceId', () => {
|
||||
expect(() => getBaseUrl({ region: 'eu-central-1' })).toThrow('Workspace ID');
|
||||
});
|
||||
});
|
||||
|
||||
describe('pollTaskResult', () => {
|
||||
|
|
@ -158,7 +149,7 @@ describe('AlicloudModelStudio Transport', () => {
|
|||
};
|
||||
mockExecuteFunctions.getCredentials.mockResolvedValue({
|
||||
apiKey: 'test-key',
|
||||
region: 'ap-southeast-1',
|
||||
url: 'https://dashscope-intl.aliyuncs.com',
|
||||
});
|
||||
mockExecuteFunctions.helpers.httpRequestWithAuthentication.mockResolvedValue(
|
||||
succeededResponse,
|
||||
|
|
|
|||
|
|
@ -4,9 +4,7 @@ import type {
|
|||
IHttpRequestMethods,
|
||||
ILoadOptionsFunctions,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeOperationError, UserError, sleep } from 'n8n-workflow';
|
||||
|
||||
import { getBaseHost } from '../../../llms/LmChatAlibabaCloud/alibaba-cloud-base-url';
|
||||
import { NodeOperationError, sleep } from 'n8n-workflow';
|
||||
|
||||
type RequestParameters = {
|
||||
headers?: IDataObject;
|
||||
|
|
@ -15,14 +13,6 @@ type RequestParameters = {
|
|||
option?: IDataObject;
|
||||
};
|
||||
|
||||
export function getBaseUrl(credentials: IDataObject): string {
|
||||
const region = (credentials.region as string) || 'ap-southeast-1';
|
||||
if (region === 'eu-central-1' && !credentials.workspaceId) {
|
||||
throw new UserError('Workspace ID is required for the Germany (Frankfurt) region');
|
||||
}
|
||||
return getBaseHost(region, credentials.workspaceId as string);
|
||||
}
|
||||
|
||||
const TERMINAL_STATUSES = ['SUCCEEDED', 'FAILED', 'CANCELED'];
|
||||
const DEFAULT_POLL_INTERVAL_MS = 15_000;
|
||||
const MAX_POLL_ATTEMPTS = 60; // ~15 min with 15s interval
|
||||
|
|
@ -37,8 +27,7 @@ export async function apiRequest(
|
|||
|
||||
const credentials = await this.getCredentials('alibabaCloudApi');
|
||||
|
||||
const baseUrl = getBaseUrl(credentials as IDataObject);
|
||||
const uri = `${baseUrl}${endpoint}`;
|
||||
const uri = `${credentials.url as string}${endpoint}`;
|
||||
const headers = parameters?.headers ?? {};
|
||||
|
||||
const options = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue