From 4b5f045281837e7cc29a57a1b9360e87cc3805f7 Mon Sep 17 00:00:00 2001 From: oleg Date: Mon, 5 May 2025 09:57:10 +0200 Subject: [PATCH] feat(Anthropic Chat Model Node): Add configurable base URL for Anthropic API (#15063) --- .../credentials/AnthropicApi.credentials.ts | 9 ++++- .../LMChatAnthropic/LmChatAnthropic.node.ts | 9 +++-- .../methods/__tests__/searchModels.test.ts | 39 ++++++++++++++++++- .../LMChatAnthropic/methods/searchModels.ts | 5 ++- 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/packages/@n8n/nodes-langchain/credentials/AnthropicApi.credentials.ts b/packages/@n8n/nodes-langchain/credentials/AnthropicApi.credentials.ts index 80bea68713..bbc135c54a 100644 --- a/packages/@n8n/nodes-langchain/credentials/AnthropicApi.credentials.ts +++ b/packages/@n8n/nodes-langchain/credentials/AnthropicApi.credentials.ts @@ -21,6 +21,13 @@ export class AnthropicApi implements ICredentialType { required: true, default: '', }, + { + displayName: 'Base URL', + name: 'url', + type: 'string', + default: 'https://api.anthropic.com', + description: 'Override the default base URL for the API', + }, ]; authenticate: IAuthenticateGeneric = { @@ -34,7 +41,7 @@ export class AnthropicApi implements ICredentialType { test: ICredentialTestRequest = { request: { - baseURL: 'https://api.anthropic.com', + baseURL: '={{$credentials?.url}}', url: '/v1/messages', method: 'POST', headers: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts index 46d6a3c2ee..129d221a22 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/LmChatAnthropic.node.ts @@ -266,8 +266,10 @@ export class LmChatAnthropic implements INodeType { }; async supplyData(this: ISupplyDataFunctions, itemIndex: number): Promise { - const credentials = await this.getCredentials('anthropicApi'); - + const credentials = await this.getCredentials<{ url?: string; apiKey?: string }>( + 'anthropicApi', + ); + const baseURL = credentials.url ?? 'https://api.anthropic.com'; const version = this.getNode().typeVersion; const modelName = version >= 1.3 @@ -317,8 +319,9 @@ export class LmChatAnthropic implements INodeType { } const model = new ChatAnthropic({ - anthropicApiKey: credentials.apiKey as string, + anthropicApiKey: credentials.apiKey, modelName, + anthropicApiUrl: baseURL, maxTokens: options.maxTokensToSample, temperature: options.temperature, topK: options.topK, diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/__tests__/searchModels.test.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/__tests__/searchModels.test.ts index e1cc8e75a3..fce9aaa5e2 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/__tests__/searchModels.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/__tests__/searchModels.test.ts @@ -40,6 +40,7 @@ describe('searchModels', () => { beforeEach(() => { mockContext = { + getCredentials: jest.fn().mockResolvedValue({}), helpers: { httpRequestWithAuthentication: jest.fn().mockResolvedValue({ data: mockModels, @@ -50,11 +51,47 @@ describe('searchModels', () => { afterEach(() => { jest.clearAllMocks(); + // Reset the getCredentials mock to its default value + mockContext.getCredentials = jest.fn().mockResolvedValue({}); }); - it('should fetch models from Anthropic API', async () => { + it('should fetch models from default Anthropic API URL when no custom URL is provided', async () => { const result = await searchModels.call(mockContext); + expect(mockContext.getCredentials).toHaveBeenCalledWith('anthropicApi'); + expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('anthropicApi', { + url: 'https://api.anthropic.com/v1/models', + headers: { + 'anthropic-version': '2023-06-01', + }, + }); + expect(result.results).toHaveLength(5); + }); + + it('should fetch models from custom Anthropic API URL when provided in credentials', async () => { + const customUrl = 'https://custom-anthropic-api.example.com'; + // Override the default mock to return credentials with a custom URL + mockContext.getCredentials = jest.fn().mockResolvedValue({ url: customUrl }); + + const result = await searchModels.call(mockContext); + + expect(mockContext.getCredentials).toHaveBeenCalledWith('anthropicApi'); + expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('anthropicApi', { + url: `${customUrl}/v1/models`, + headers: { + 'anthropic-version': '2023-06-01', + }, + }); + expect(result.results).toHaveLength(5); + }); + + it('should use default URL when empty URL is provided in credentials', async () => { + // Override the default mock to return credentials with an empty URL + mockContext.getCredentials = jest.fn().mockResolvedValue({ url: null }); + + const result = await searchModels.call(mockContext); + + expect(mockContext.getCredentials).toHaveBeenCalledWith('anthropicApi'); expect(mockContext.helpers.httpRequestWithAuthentication).toHaveBeenCalledWith('anthropicApi', { url: 'https://api.anthropic.com/v1/models', headers: { diff --git a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/searchModels.ts b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/searchModels.ts index 8b6c7f469f..94a926abf5 100644 --- a/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/searchModels.ts +++ b/packages/@n8n/nodes-langchain/nodes/llms/LMChatAnthropic/methods/searchModels.ts @@ -15,8 +15,11 @@ export async function searchModels( this: ILoadOptionsFunctions, filter?: string, ): Promise { + const credentials = await this.getCredentials<{ url?: string }>('anthropicApi'); + + const baseURL = credentials.url ?? 'https://api.anthropic.com'; const response = (await this.helpers.httpRequestWithAuthentication.call(this, 'anthropicApi', { - url: 'https://api.anthropic.com/v1/models', + url: `${baseURL}/v1/models`, headers: { 'anthropic-version': '2023-06-01', },