first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,142 @@
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 = {
getCredentials: jest.fn().mockResolvedValue({}),
helpers: {
httpRequestWithAuthentication: jest.fn().mockResolvedValue({
data: mockModels,
}),
},
} as unknown as jest.Mocked<ILoadOptionsFunctions>;
});
afterEach(() => {
jest.clearAllMocks();
// Reset the getCredentials mock to its default value
mockContext.getCredentials = jest.fn().mockResolvedValue({});
});
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: {
'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,63 @@
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 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: `${baseURL}/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,
};
}