feat(Anthropic Chat Model Node): Fetch models dynamically & support thinking (#13543)

This commit is contained in:
oleg
2025-02-27 15:40:58 +01:00
committed by GitHub
parent 615a42afd5
commit 461df371f7
5 changed files with 316 additions and 30 deletions

View File

@@ -0,0 +1,105 @@
import type { ILoadOptionsFunctions } from 'n8n-workflow';
import { searchModels, type AnthropicModel } from '../searchModels';
describe('searchModels', () => {
let mockContext: jest.Mocked<ILoadOptionsFunctions>;
const mockModels: AnthropicModel[] = [
{
id: 'claude-3-opus-20240229',
display_name: 'Claude 3 Opus',
type: 'model',
created_at: '2024-02-29T00:00:00Z',
},
{
id: 'claude-3-sonnet-20240229',
display_name: 'Claude 3 Sonnet',
type: 'model',
created_at: '2024-02-29T00:00:00Z',
},
{
id: 'claude-3-haiku-20240307',
display_name: 'Claude 3 Haiku',
type: 'model',
created_at: '2024-03-07T00:00:00Z',
},
{
id: 'claude-2.1',
display_name: 'Claude 2.1',
type: 'model',
created_at: '2023-11-21T00:00:00Z',
},
{
id: 'claude-2.0',
display_name: 'Claude 2.0',
type: 'model',
created_at: '2023-07-11T00:00:00Z',
},
];
beforeEach(() => {
mockContext = {
helpers: {
httpRequestWithAuthentication: jest.fn().mockResolvedValue({
data: mockModels,
}),
},
} as unknown as jest.Mocked<ILoadOptionsFunctions>;
});
afterEach(() => {
jest.clearAllMocks();
});
it('should fetch models from Anthropic API', async () => {
const result = await searchModels.call(mockContext);
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 sort models by created_at date, most recent first', async () => {
const result = await searchModels.call(mockContext);
const sortedResults = result.results;
expect(sortedResults[0].value).toBe('claude-3-haiku-20240307');
expect(sortedResults[1].value).toBe('claude-3-opus-20240229');
expect(sortedResults[2].value).toBe('claude-3-sonnet-20240229');
expect(sortedResults[3].value).toBe('claude-2.1');
expect(sortedResults[4].value).toBe('claude-2.0');
});
it('should filter models based on search term', async () => {
const result = await searchModels.call(mockContext, 'claude-3');
expect(result.results).toHaveLength(3);
expect(result.results).toEqual([
{ name: 'Claude 3 Haiku', value: 'claude-3-haiku-20240307' },
{ name: 'Claude 3 Opus', value: 'claude-3-opus-20240229' },
{ name: 'Claude 3 Sonnet', value: 'claude-3-sonnet-20240229' },
]);
});
it('should handle case-insensitive search', async () => {
const result = await searchModels.call(mockContext, 'CLAUDE-3');
expect(result.results).toHaveLength(3);
expect(result.results).toEqual([
{ name: 'Claude 3 Haiku', value: 'claude-3-haiku-20240307' },
{ name: 'Claude 3 Opus', value: 'claude-3-opus-20240229' },
{ name: 'Claude 3 Sonnet', value: 'claude-3-sonnet-20240229' },
]);
});
it('should handle when no models match the filter', async () => {
const result = await searchModels.call(mockContext, 'nonexistent-model');
expect(result.results).toHaveLength(0);
});
});

View File

@@ -0,0 +1,60 @@
import type {
ILoadOptionsFunctions,
INodeListSearchItems,
INodeListSearchResult,
} from 'n8n-workflow';
export interface AnthropicModel {
id: string;
display_name: string;
type: string;
created_at: string;
}
export async function searchModels(
this: ILoadOptionsFunctions,
filter?: string,
): Promise<INodeListSearchResult> {
const response = (await this.helpers.httpRequestWithAuthentication.call(this, 'anthropicApi', {
url: 'https://api.anthropic.com/v1/models',
headers: {
'anthropic-version': '2023-06-01',
},
})) as { data: AnthropicModel[] };
const models = response.data || [];
let results: INodeListSearchItems[] = [];
if (filter) {
for (const model of models) {
if (model.id.toLowerCase().includes(filter.toLowerCase())) {
results.push({
name: model.display_name,
value: model.id,
});
}
}
} else {
results = models.map((model) => ({
name: model.display_name,
value: model.id,
}));
}
// Sort models with more recent ones first (claude-3 before claude-2)
results = results.sort((a, b) => {
const modelA = models.find((m) => m.id === a.value);
const modelB = models.find((m) => m.id === b.value);
if (!modelA || !modelB) return 0;
// Sort by created_at date, most recent first
const dateA = new Date(modelA.created_at);
const dateB = new Date(modelB.created_at);
return dateB.getTime() - dateA.getTime();
});
return {
results,
};
}