From 207c29f8e3e720435f75f270b56217e585547445 Mon Sep 17 00:00:00 2001 From: Jaakko Husso Date: Tue, 18 Aug 2026 10:47:43 +0000 Subject: [PATCH] fix(OpenAI Node): Filter non-chat models out of the chat model dropdown (#35785) Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: Ricardo Espinoza --- .../__tests__/model-discovery.test.ts | 2 ++ .../model-discovery/__tests__/openai.test.ts} | 20 ++++++++++--- .../ai-utilities/src/model-discovery/index.ts | 3 +- .../src/model-discovery/providers/openai.ts | 16 ++++++++-- .../vendors/OpenAi/helpers/modelFiltering.ts | 30 ------------------- .../methods/__tests__/listSearch.test.ts | 2 ++ .../vendors/OpenAi/methods/listSearch.ts | 12 ++++++-- 7 files changed, 44 insertions(+), 41 deletions(-) rename packages/@n8n/{nodes-langchain/nodes/vendors/OpenAi/helpers/__tests__/modelFiltering.test.ts => ai-utilities/src/model-discovery/__tests__/openai.test.ts} (63%) delete mode 100644 packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/helpers/modelFiltering.ts diff --git a/packages/@n8n/ai-utilities/src/model-discovery/__tests__/model-discovery.test.ts b/packages/@n8n/ai-utilities/src/model-discovery/__tests__/model-discovery.test.ts index a2fb138d160..7157b048b90 100644 --- a/packages/@n8n/ai-utilities/src/model-discovery/__tests__/model-discovery.test.ts +++ b/packages/@n8n/ai-utilities/src/model-discovery/__tests__/model-discovery.test.ts @@ -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' }, ], diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/helpers/__tests__/modelFiltering.test.ts b/packages/@n8n/ai-utilities/src/model-discovery/__tests__/openai.test.ts similarity index 63% rename from packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/helpers/__tests__/modelFiltering.test.ts rename to packages/@n8n/ai-utilities/src/model-discovery/__tests__/openai.test.ts index 0b6ac55d733..3dea052c9e6 100644 --- a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/helpers/__tests__/modelFiltering.test.ts +++ b/packages/@n8n/ai-utilities/src/model-discovery/__tests__/openai.test.ts @@ -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); }); }); }); diff --git a/packages/@n8n/ai-utilities/src/model-discovery/index.ts b/packages/@n8n/ai-utilities/src/model-discovery/index.ts index cd4b38c509c..eaddeb177ff 100644 --- a/packages/@n8n/ai-utilities/src/model-discovery/index.ts +++ b/packages/@n8n/ai-utilities/src/model-discovery/index.ts @@ -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'; diff --git a/packages/@n8n/ai-utilities/src/model-discovery/providers/openai.ts b/packages/@n8n/ai-utilities/src/model-discovery/providers/openai.ts index 667ae7fe748..1fdc845d9c7 100644 --- a/packages/@n8n/ai-utilities/src/model-discovery/providers/openai.ts +++ b/packages/@n8n/ai-utilities/src/model-discovery/providers/openai.ts @@ -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') || diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/helpers/modelFiltering.ts b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/helpers/modelFiltering.ts deleted file mode 100644 index 1ffc2eace31..00000000000 --- a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/helpers/modelFiltering.ts +++ /dev/null @@ -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')) - ); -} diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/methods/__tests__/listSearch.test.ts b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/methods/__tests__/listSearch.test.ts index 7424890fc61..2ed59ce9d4e 100644 --- a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/methods/__tests__/listSearch.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/methods/__tests__/listSearch.test.ts @@ -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' }, ], }); diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/methods/listSearch.ts b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/methods/listSearch.ts index beaef9d5732..76b47734a3e 100644 --- a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/methods/listSearch.ts +++ b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/methods/listSearch.ts @@ -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 { 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(