mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
fix(OpenAI Node): Filter non-chat models out of the chat model dropdown (#35785)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Ricardo Espinoza <ricardo@n8n.io>
This commit is contained in:
parent
6155d8e882
commit
207c29f8e3
|
|
@ -73,6 +73,8 @@ describe('model-discovery', () => {
|
|||
{ id: 'gpt-5' },
|
||||
{ id: 'whisper-1' },
|
||||
{ id: 'dall-e-3' },
|
||||
{ id: 'gpt-image-2' },
|
||||
{ id: 'gpt-4o-transcribe' },
|
||||
{ id: 'text-embedding-3-small' },
|
||||
{ id: 'gpt-4o' },
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { shouldIncludeModel } from '../modelFiltering';
|
||||
import { shouldIncludeOpenAiModel } from '../providers/openai';
|
||||
|
||||
describe('shouldIncludeModel', () => {
|
||||
describe('shouldIncludeOpenAiModel', () => {
|
||||
const testCases: Array<{ modelId: string; officialAPI: boolean }> = [
|
||||
// Excluded model types
|
||||
{ modelId: 'babbage-002', officialAPI: false },
|
||||
|
|
@ -15,9 +15,21 @@ describe('shouldIncludeModel', () => {
|
|||
{ modelId: 'gpt-4o-realtime-preview', officialAPI: false }, // infix check for -realtime
|
||||
{ modelId: 'gpt-3.5-turbo-instruct', officialAPI: false }, // gpt-* with instruct
|
||||
|
||||
// Non-chat families named gpt-*, so caught by infix rather than prefix
|
||||
{ modelId: 'gpt-image-2', officialAPI: false },
|
||||
{ modelId: 'chatgpt-image-latest', officialAPI: false },
|
||||
{ modelId: 'gpt-transcribe', officialAPI: false },
|
||||
{ modelId: 'gpt-4o-transcribe-diarize', officialAPI: false },
|
||||
// Every diarization model OpenAI ships today is also *-transcribe-diarize,
|
||||
// so this pins the -diarize clause on its own
|
||||
{ modelId: 'gpt-4o-diarize', officialAPI: false },
|
||||
|
||||
// Included models (standard chat models)
|
||||
{ modelId: 'gpt-4', officialAPI: true },
|
||||
{ modelId: 'gpt-4o', officialAPI: true },
|
||||
// Unsupported on responses, but supported on chat/completions, which the
|
||||
// OpenAI node's "Message a Model" uses, so it must stay selectable
|
||||
{ modelId: 'gpt-audio', officialAPI: true },
|
||||
{ modelId: 'o1-preview', officialAPI: true },
|
||||
{ modelId: 'ft:gpt-3.5-turbo', officialAPI: true }, // fine-tuned models
|
||||
|
||||
|
|
@ -28,7 +40,7 @@ describe('shouldIncludeModel', () => {
|
|||
|
||||
describe('Custom API behavior', () => {
|
||||
it.each(testCases)('should include "$modelId"', ({ modelId }) => {
|
||||
expect(shouldIncludeModel(modelId, true)).toBe(true);
|
||||
expect(shouldIncludeOpenAiModel(modelId, true)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -39,7 +51,7 @@ describe('shouldIncludeModel', () => {
|
|||
}));
|
||||
|
||||
it.each(testCasesWithAction)('should $action "$modelId"', ({ modelId, officialAPI }) => {
|
||||
expect(shouldIncludeModel(modelId, false)).toBe(officialAPI);
|
||||
expect(shouldIncludeOpenAiModel(modelId, false)).toBe(officialAPI);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -59,9 +59,10 @@ export { listGroqModels } from './providers/groq';
|
|||
export { listMistralModels } from './providers/mistral';
|
||||
export { listNvidiaModels } from './providers/nvidia';
|
||||
export {
|
||||
isOpenAiCustomEndpoint,
|
||||
listOpenAiModels,
|
||||
shouldIncludeOpenAiModel,
|
||||
OFFICIAL_OPENAI_HOSTNAMES,
|
||||
isOpenAiCustomEndpoint,
|
||||
} from './providers/openai';
|
||||
export { listOpenRouterModels } from './providers/openrouter';
|
||||
export { listVercelModels } from './providers/vercel';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { baseUrl, bearerHeaders, byName, getJson, idsToModels, type IdItem } from '../request';
|
||||
import type { ListModelsFn } from '../types';
|
||||
|
||||
const OFFICIAL_OPENAI_HOSTNAMES = ['api.openai.com', 'ai-assistant.n8n.io'];
|
||||
export const OFFICIAL_OPENAI_HOSTNAMES = ['api.openai.com', 'ai-assistant.n8n.io'];
|
||||
|
||||
export function isOpenAiCustomEndpoint(baseURL?: string): boolean {
|
||||
if (baseURL === undefined) return false;
|
||||
|
|
@ -14,8 +14,11 @@ export function isOpenAiCustomEndpoint(baseURL?: string): boolean {
|
|||
}
|
||||
|
||||
/**
|
||||
* Source: LMChatOpenAi `methods/loadModels.ts` — on the official API, exclude
|
||||
* non-chat model families; on custom (proxy/self-hosted) hosts include all.
|
||||
* On the official API, exclude non-chat model families; on custom
|
||||
* (proxy/self-hosted) hosts include all, since their naming is not OpenAI's.
|
||||
*
|
||||
* Single source of truth for every OpenAI model dropdown: the LMChatOpenAi
|
||||
* sub-node, the OpenAI node's `modelSearch`, and the agents model catalog.
|
||||
*/
|
||||
export function shouldIncludeOpenAiModel(modelId: string, isCustomApi: boolean): boolean {
|
||||
if (isCustomApi) return true;
|
||||
|
|
@ -24,6 +27,13 @@ export function shouldIncludeOpenAiModel(modelId: string, isCustomApi: boolean):
|
|||
modelId.startsWith('davinci') ||
|
||||
modelId.startsWith('computer-use') ||
|
||||
modelId.startsWith('dall-e') ||
|
||||
// Newer non-chat families are named gpt-*, so they clear the gpt- prefix and
|
||||
// have to be matched by infix. Only list families unsupported on *both*
|
||||
// chat/completions and responses: gpt-audio-* is absent on purpose, it works
|
||||
// on chat/completions.
|
||||
modelId.includes('-image') ||
|
||||
modelId.includes('-transcribe') ||
|
||||
modelId.includes('-diarize') ||
|
||||
modelId.startsWith('text-embedding') ||
|
||||
modelId.startsWith('tts') ||
|
||||
modelId.includes('-tts') ||
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
/**
|
||||
* Determines whether a model should be included in the model list based on
|
||||
* whether it's a custom API and the model's ID.
|
||||
*
|
||||
* @param modelId - The ID of the model to check
|
||||
* @param isCustomAPI - Whether this is a custom API (not official OpenAI)
|
||||
* @returns true if the model should be included, false otherwise
|
||||
*/
|
||||
export function shouldIncludeModel(modelId: string, isCustomAPI: boolean): boolean {
|
||||
// For custom APIs, include all models
|
||||
if (isCustomAPI) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For official OpenAI API, exclude certain model types
|
||||
return !(
|
||||
modelId.startsWith('babbage') ||
|
||||
modelId.startsWith('davinci') ||
|
||||
modelId.startsWith('computer-use') ||
|
||||
modelId.startsWith('dall-e') ||
|
||||
modelId.startsWith('text-embedding') ||
|
||||
modelId.startsWith('tts') ||
|
||||
modelId.includes('-tts') ||
|
||||
modelId.startsWith('whisper') ||
|
||||
modelId.startsWith('omni-moderation') ||
|
||||
modelId.startsWith('sora') ||
|
||||
modelId.includes('-realtime') ||
|
||||
(modelId.startsWith('gpt-') && modelId.includes('instruct'))
|
||||
);
|
||||
}
|
||||
|
|
@ -30,6 +30,8 @@ describe('modelSearch', () => {
|
|||
{ id: 'babbage-002' },
|
||||
{ id: 'whisper-1' },
|
||||
{ id: 'dall-e-3' },
|
||||
{ id: 'gpt-image-2' },
|
||||
{ id: 'gpt-4o-transcribe' },
|
||||
{ id: 'gpt-4o-realtime-preview' },
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
import {
|
||||
OFFICIAL_OPENAI_HOSTNAMES,
|
||||
shouldIncludeOpenAiModel,
|
||||
} from '@n8n/ai-utilities/model-discovery';
|
||||
import type {
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
|
|
@ -7,7 +11,6 @@ import type {
|
|||
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(
|
||||
|
|
@ -78,8 +81,11 @@ export async function modelSearch(
|
|||
): 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);
|
||||
const isCustomAPI = !!(url && !OFFICIAL_OPENAI_HOSTNAMES.includes(url.hostname));
|
||||
return await getModelSearch((model) => shouldIncludeOpenAiModel(model.id, isCustomAPI))(
|
||||
this,
|
||||
filter,
|
||||
);
|
||||
}
|
||||
|
||||
export async function videoModelSearch(
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user