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,86 @@
import type { ILoadOptionsFunctions } from 'n8n-workflow';
import * as transport from '../../transport';
import { modelSearch } from '../listSearch';
jest.mock('../../transport');
describe('modelSearch', () => {
let mockContext: jest.Mocked<ILoadOptionsFunctions>;
beforeEach(() => {
mockContext = {
getCredentials: jest.fn(),
} as unknown as jest.Mocked<ILoadOptionsFunctions>;
jest.clearAllMocks();
});
describe('Official OpenAI API', () => {
it('should return filtered models when using official OpenAI API', async () => {
mockContext.getCredentials.mockResolvedValue({
url: 'https://api.openai.com/v1',
});
(transport.apiRequest as jest.Mock).mockResolvedValue({
data: [
{ id: 'gpt-4' },
{ id: 'gpt-3.5-turbo' },
{ id: 'babbage-002' },
{ id: 'whisper-1' },
{ id: 'dall-e-3' },
{ id: 'gpt-4o-realtime-preview' },
],
});
const result = await modelSearch.call(mockContext);
expect(result.results).toEqual([
{ name: 'GPT-3.5-TURBO', value: 'gpt-3.5-turbo' },
{ name: 'GPT-4', value: 'gpt-4' },
]);
});
it('should treat ai-assistant.n8n.io as official API', async () => {
mockContext.getCredentials.mockResolvedValue({
url: 'https://ai-assistant.n8n.io/v1',
});
(transport.apiRequest as jest.Mock).mockResolvedValue({
data: [{ id: 'gpt-4' }, { id: 'whisper-1' }, { id: 'dall-e-2' }],
});
const result = await modelSearch.call(mockContext);
expect(result.results).toEqual([{ name: 'GPT-4', value: 'gpt-4' }]);
});
});
describe('Custom API', () => {
it('should include all models for custom API endpoints', async () => {
mockContext.getCredentials.mockResolvedValue({
url: 'https://custom-llm-provider.com/v1',
});
(transport.apiRequest as jest.Mock).mockResolvedValue({
data: [
{ id: 'llama-3-70b' },
{ id: 'mistral-large' },
{ id: 'babbage-002' },
{ id: 'whisper-1' },
{ id: 'custom-model' },
],
});
const result = await modelSearch.call(mockContext);
expect(result.results).toEqual([
{ name: 'BABBAGE-002', value: 'babbage-002' },
{ name: 'CUSTOM-MODEL', value: 'custom-model' },
{ name: 'LLAMA-3-70B', value: 'llama-3-70b' },
{ name: 'MISTRAL-LARGE', value: 'mistral-large' },
{ name: 'WHISPER-1', value: 'whisper-1' },
]);
});
});
});
@@ -0,0 +1,2 @@
export * as listSearch from './listSearch';
export * as loadOptions from './loadOptions';
@@ -0,0 +1,151 @@
import type {
IDataObject,
ILoadOptionsFunctions,
INodeListSearchItems,
INodeListSearchResult,
} from 'n8n-workflow';
import type { Assistant } from 'openai/resources/beta/assistants';
import type { Model } from 'openai/resources/models';
import { shouldIncludeModel } from '../helpers/modelFiltering';
import { apiRequest } from '../transport';
export async function fileSearch(
this: ILoadOptionsFunctions,
filter?: string,
): Promise<INodeListSearchResult> {
const { data } = await apiRequest.call(this, 'GET', '/files');
if (filter) {
const results: INodeListSearchItems[] = [];
for (const file of data || []) {
if ((file.filename as string)?.toLowerCase().includes(filter.toLowerCase())) {
results.push({
name: file.filename as string,
value: file.id as string,
});
}
}
return {
results,
};
} else {
return {
results: (data || []).map((file: IDataObject) => ({
name: file.filename as string,
value: file.id as string,
})),
};
}
}
const getModelSearch =
(filterCondition: (model: Model) => boolean) =>
async (ctx: ILoadOptionsFunctions, filter?: string): Promise<INodeListSearchResult> => {
let { data } = (await apiRequest.call(ctx, 'GET', '/models')) as { data: Model[] };
data = data?.filter((model) => filterCondition(model));
let results: INodeListSearchItems[] = [];
if (filter) {
for (const model of data || []) {
if (model.id?.toLowerCase().includes(filter.toLowerCase())) {
results.push({
name: model.id.toUpperCase(),
value: model.id,
});
}
}
} else {
results = (data || []).map((model) => ({
name: model.id.toUpperCase(),
value: model.id,
}));
}
results = results.sort((a, b) => a.name.localeCompare(b.name));
return {
results,
};
};
export async function modelSearch(
this: ILoadOptionsFunctions,
filter?: string,
): Promise<INodeListSearchResult> {
const credentials = await this.getCredentials<{ url: string }>('openAiApi');
const url = credentials.url && new URL(credentials.url);
const isCustomAPI = !!(url && !['api.openai.com', 'ai-assistant.n8n.io'].includes(url.hostname));
return await getModelSearch((model) => shouldIncludeModel(model.id, isCustomAPI))(this, filter);
}
export async function videoModelSearch(
this: ILoadOptionsFunctions,
filter?: string,
): Promise<INodeListSearchResult> {
return await getModelSearch((model) => model.id.includes('sora'))(this, filter);
}
export async function imageModelSearch(
this: ILoadOptionsFunctions,
filter?: string,
): Promise<INodeListSearchResult> {
return await getModelSearch(
(model) => model.id.includes('vision') || model.id.includes('gpt-4o'),
)(this, filter);
}
export async function assistantSearch(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const { data, has_more, last_id } = (await apiRequest.call(this, 'GET', '/assistants', {
headers: {
'OpenAI-Beta': 'assistants=v2',
},
qs: {
limit: 100,
after: paginationToken,
},
})) as {
data: Assistant[];
has_more: boolean;
last_id: string;
first_id: string;
};
if (has_more) {
paginationToken = last_id;
} else {
paginationToken = undefined;
}
if (filter) {
const results: INodeListSearchItems[] = [];
for (const assistant of data || []) {
if (assistant.name?.toLowerCase().includes(filter.toLowerCase())) {
results.push({
name: assistant.name,
value: assistant.id,
});
}
}
return {
results,
};
} else {
return {
results: (data || []).map((assistant) => ({
name: assistant.name ?? assistant.id,
value: assistant.id,
})),
paginationToken,
};
}
}
@@ -0,0 +1,18 @@
import type { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow';
import { apiRequest } from '../transport';
export async function getFiles(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const { data } = await apiRequest.call(this, 'GET', '/files', { qs: { purpose: 'assistants' } });
const returnData: INodePropertyOptions[] = [];
for (const file of data || []) {
returnData.push({
name: file.filename as string,
value: file.id as string,
});
}
return returnData;
}