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
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:
+86
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user