From 7c4771e62379cbe49f0fdf909f0d268a2d332b37 Mon Sep 17 00:00:00 2001 From: RomanDavydchuk Date: Fri, 21 Nov 2025 19:02:10 +0200 Subject: [PATCH] fix(OpenAI Node): Use new model for text classification (#22130) --- .../nodes/vendors/OpenAi/OpenAi.node.ts | 3 +- .../actions/text/classify.operation.test.ts | 102 ++++++++++++++++++ .../nodes/vendors/OpenAi/v2/OpenAiV2.node.ts | 2 +- .../v2/actions/text/classify.operation.ts | 13 ++- 4 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/test/v2/actions/text/classify.operation.test.ts diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/OpenAi.node.ts b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/OpenAi.node.ts index 730a4fbda36..2efac849e92 100644 --- a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/OpenAi.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/OpenAi.node.ts @@ -15,7 +15,7 @@ export class OpenAi extends VersionedNodeType { name: 'openAi', icon: { light: 'file:openAi.svg', dark: 'file:openAi.dark.svg' }, group: ['transform'], - defaultVersion: 2, + defaultVersion: 2.1, subtitle: `={{(${prettifyOperation})($parameter.resource, $parameter.operation)}}`, description: 'Message an assistant or GPT, analyze images, generate audio, etc.', codex: { @@ -55,6 +55,7 @@ export class OpenAi extends VersionedNodeType { 1.7: new OpenAiV1(baseDescription), 1.8: new OpenAiV1(baseDescription), 2: new OpenAiV2(baseDescription), + 2.1: new OpenAiV2(baseDescription), }; super(nodeVersions, baseDescription); diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/test/v2/actions/text/classify.operation.test.ts b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/test/v2/actions/text/classify.operation.test.ts new file mode 100644 index 00000000000..20963adeb34 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/test/v2/actions/text/classify.operation.test.ts @@ -0,0 +1,102 @@ +import { mockDeep } from 'jest-mock-extended'; +import type { IExecuteFunctions, INode } from 'n8n-workflow'; + +import * as transport from '../../../../transport'; +import * as classify from '../../../../v2/actions/text/classify.operation'; + +describe('OpenAI Classify Operation', () => { + const executeFunctions = mockDeep(); + const node = { + id: '123', + name: 'OpenAI Node', + type: '@n8n/n8n-nodes-langchain.openAi', + typeVersion: 2.1, + position: [0, 0], + parameters: {}, + } as INode; + const apiRequestSpy = jest.spyOn(transport, 'apiRequest'); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should use omni-moderation-latest model when version is 2.1', async () => { + executeFunctions.getNode.mockReturnValue(node); + executeFunctions.getNodeParameter.mockImplementation((param: string) => { + const params = { + input: 'Lorem ipsum', + simplify: false, + }; + return params[param as keyof typeof params]; + }); + apiRequestSpy.mockResolvedValueOnce({ results: [{ flagged: true }] }); + + const result = await classify.execute.call(executeFunctions, 0); + + expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/moderations', { + body: { input: 'Lorem ipsum', model: 'omni-moderation-latest' }, + }); + expect(result).toEqual([ + { + json: { flagged: true }, + pairedItem: { item: 0 }, + }, + ]); + }); + + it('should use text-moderation-stable model when version is less than 2.1 and useStableModel is true', async () => { + executeFunctions.getNode.mockReturnValue({ + ...node, + typeVersion: 2, + }); + executeFunctions.getNodeParameter.mockImplementation((param: string) => { + const params = { + input: 'Lorem ipsum', + simplify: false, + options: { useStableModel: true }, + }; + return params[param as keyof typeof params]; + }); + apiRequestSpy.mockResolvedValueOnce({ results: [{ flagged: true }] }); + + const result = await classify.execute.call(executeFunctions, 0); + + expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/moderations', { + body: { input: 'Lorem ipsum', model: 'text-moderation-stable' }, + }); + expect(result).toEqual([ + { + json: { flagged: true }, + pairedItem: { item: 0 }, + }, + ]); + }); + + it('should use text-moderation-latest model when version is less than 2.1 and useStableModel is false', async () => { + executeFunctions.getNode.mockReturnValue({ + ...node, + typeVersion: 2, + }); + executeFunctions.getNodeParameter.mockImplementation((param: string) => { + const params = { + input: 'Lorem ipsum', + simplify: false, + options: { useStableModel: false }, + }; + return params[param as keyof typeof params]; + }); + apiRequestSpy.mockResolvedValueOnce({ results: [{ flagged: true }] }); + + const result = await classify.execute.call(executeFunctions, 0); + + expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/moderations', { + body: { input: 'Lorem ipsum', model: 'text-moderation-latest' }, + }); + expect(result).toEqual([ + { + json: { flagged: true }, + pairedItem: { item: 0 }, + }, + ]); + }); +}); diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/v2/OpenAiV2.node.ts b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/v2/OpenAiV2.node.ts index 934c5c622df..df865ba02f5 100644 --- a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/v2/OpenAiV2.node.ts +++ b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/v2/OpenAiV2.node.ts @@ -23,7 +23,7 @@ export class OpenAiV2 implements INodeType { constructor(baseDescription: INodeTypeBaseDescription) { this.description = { ...baseDescription, - version: [2], + version: [2, 2.1], defaults: { name: 'OpenAI', }, diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/v2/actions/text/classify.operation.ts b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/v2/actions/text/classify.operation.ts index a8ce228f00c..d0296ea0880 100644 --- a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/v2/actions/text/classify.operation.ts +++ b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/v2/actions/text/classify.operation.ts @@ -38,6 +38,11 @@ const properties: INodeProperties[] = [ 'Whether to use the stable version of the model instead of the latest version, accuracy may be slightly lower', }, ], + displayOptions: { + show: { + '@version': [{ _cnd: { lt: 2.1 } }], + }, + }, }, ]; @@ -52,8 +57,12 @@ export const description = updateDisplayOptions(displayOptions, properties); export async function execute(this: IExecuteFunctions, i: number): Promise { const input = this.getNodeParameter('input', i) as string; - const options = this.getNodeParameter('options', i); - const model = options.useStableModel ? 'text-moderation-stable' : 'text-moderation-latest'; + const version = this.getNode().typeVersion; + let model = 'omni-moderation-latest'; + if (version < 2.1) { + const options = this.getNodeParameter('options', i); + model = options.useStableModel ? 'text-moderation-stable' : 'text-moderation-latest'; + } const body = { input,