mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(Anthropic Chat Model Node): Fetch models dynamically & support thinking (#13543)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user