mirror of
https://github.com/n8n-io/n8n
synced 2026-04-21 15:47:20 +00:00
fix(OpenAI Node): Replace hardcoded models with RLC (#28226)
This commit is contained in:
parent
e848230947
commit
4070930e4c
9 changed files with 796 additions and 209 deletions
|
|
@ -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.1,
|
||||
defaultVersion: 2.2,
|
||||
subtitle: `={{(${prettifyOperation})($parameter.resource, $parameter.operation)}}`,
|
||||
description: 'Message an assistant or GPT, analyze images, generate audio, etc.',
|
||||
codex: {
|
||||
|
|
@ -70,6 +70,7 @@ export class OpenAi extends VersionedNodeType {
|
|||
1.8: new OpenAiV1(baseDescription),
|
||||
2: new OpenAiV2(baseDescription),
|
||||
2.1: new OpenAiV2(baseDescription),
|
||||
2.2: new OpenAiV2(baseDescription),
|
||||
};
|
||||
|
||||
super(nodeVersions, baseDescription);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { ILoadOptionsFunctions } from 'n8n-workflow';
|
||||
|
||||
import * as transport from '../../transport';
|
||||
import { modelSearch } from '../listSearch';
|
||||
import { imageGenerateModelSearch, modelSearch } from '../listSearch';
|
||||
|
||||
jest.mock('../../transport');
|
||||
|
||||
|
|
@ -84,3 +84,48 @@ describe('modelSearch', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('imageGenerateModelSearch', () => {
|
||||
let mockContext: jest.Mocked<ILoadOptionsFunctions>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockContext = {} as unknown as jest.Mocked<ILoadOptionsFunctions>;
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return only image generation models (dall-e and gpt-image)', async () => {
|
||||
(transport.apiRequest as jest.Mock).mockResolvedValue({
|
||||
data: [
|
||||
{ id: 'dall-e-2' },
|
||||
{ id: 'dall-e-3' },
|
||||
{ id: 'gpt-image-1' },
|
||||
{ id: 'gpt-image-1-mini' },
|
||||
{ id: 'gpt-4o' },
|
||||
{ id: 'whisper-1' },
|
||||
{ id: 'tts-1' },
|
||||
],
|
||||
});
|
||||
|
||||
const result = await imageGenerateModelSearch.call(mockContext);
|
||||
|
||||
expect(result.results).toEqual([
|
||||
{ name: 'DALL-E-2', value: 'dall-e-2' },
|
||||
{ name: 'DALL-E-3', value: 'dall-e-3' },
|
||||
{ name: 'GPT-IMAGE-1', value: 'gpt-image-1' },
|
||||
{ name: 'GPT-IMAGE-1-MINI', value: 'gpt-image-1-mini' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should filter results by search term', async () => {
|
||||
(transport.apiRequest as jest.Mock).mockResolvedValue({
|
||||
data: [{ id: 'dall-e-2' }, { id: 'dall-e-3' }, { id: 'gpt-image-1' }],
|
||||
});
|
||||
|
||||
const result = await imageGenerateModelSearch.call(mockContext, 'dall');
|
||||
|
||||
expect(result.results).toEqual([
|
||||
{ name: 'DALL-E-2', value: 'dall-e-2' },
|
||||
{ name: 'DALL-E-3', value: 'dall-e-3' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -98,6 +98,15 @@ export async function imageModelSearch(
|
|||
)(this, filter);
|
||||
}
|
||||
|
||||
export async function imageGenerateModelSearch(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
return await getModelSearch(
|
||||
(model) => model.id.includes('dall-e') || model.id.includes('gpt-image'),
|
||||
)(this, filter);
|
||||
}
|
||||
|
||||
export async function assistantSearch(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
|
|
|
|||
237
packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/test/v2/actions/image/generate.operation.test.ts
vendored
Normal file
237
packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/test/v2/actions/image/generate.operation.test.ts
vendored
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
import { mock, mockDeep } from 'jest-mock-extended';
|
||||
import type { IExecuteFunctions, INode } from 'n8n-workflow';
|
||||
|
||||
import * as transport from '../../../../transport';
|
||||
import { execute } from '../../../../v2/actions/image/generate.operation';
|
||||
|
||||
jest.mock('../../../../transport');
|
||||
|
||||
describe('Image Generate Operation', () => {
|
||||
let mockExecuteFunctions: jest.Mocked<IExecuteFunctions>;
|
||||
let mockNode: INode;
|
||||
const apiRequestSpy = jest.spyOn(transport, 'apiRequest');
|
||||
|
||||
const makeNode = (typeVersion: number): INode =>
|
||||
mock<INode>({
|
||||
id: 'test-node',
|
||||
name: 'OpenAI Image Generate',
|
||||
type: 'n8n-nodes-base.openAi',
|
||||
typeVersion,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
});
|
||||
|
||||
const mockBinaryData = {
|
||||
data: 'base64-encoded-image',
|
||||
mimeType: 'image/png',
|
||||
fileName: 'data',
|
||||
};
|
||||
|
||||
const b64Response = {
|
||||
data: [{ b64_json: Buffer.from('fake-image').toString('base64') }],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockExecuteFunctions = mockDeep<IExecuteFunctions>();
|
||||
mockExecuteFunctions.helpers.prepareBinaryData = jest.fn().mockResolvedValue(mockBinaryData);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('v2.1 (static model field)', () => {
|
||||
beforeEach(() => {
|
||||
mockNode = makeNode(2.1);
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
|
||||
});
|
||||
|
||||
it('should read model as plain string and include response_format for dall-e-3', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params = { model: 'dall-e-3', prompt: 'a cute cat', options: {} };
|
||||
return params[paramName as keyof typeof params];
|
||||
});
|
||||
|
||||
apiRequestSpy.mockResolvedValue(b64Response);
|
||||
|
||||
await execute.call(mockExecuteFunctions, 0);
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/images/generations', {
|
||||
body: expect.objectContaining({ model: 'dall-e-3', response_format: 'b64_json' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('should not send response_format for gpt-image-1', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params = { model: 'gpt-image-1', prompt: 'a cute cat', options: {} };
|
||||
return params[paramName as keyof typeof params];
|
||||
});
|
||||
|
||||
apiRequestSpy.mockResolvedValue(b64Response);
|
||||
|
||||
await execute.call(mockExecuteFunctions, 0);
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/images/generations', {
|
||||
body: expect.objectContaining({ model: 'gpt-image-1', response_format: undefined }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('v2.2 (RLC model field)', () => {
|
||||
beforeEach(() => {
|
||||
mockNode = makeNode(2.2);
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
|
||||
});
|
||||
|
||||
const makeRlcMock =
|
||||
(modelValue: string, extraOptions = {}) =>
|
||||
(paramName: string, _i: unknown, _default: unknown, opts?: { extractValue?: boolean }) => {
|
||||
if (paramName === 'modelId' && opts?.extractValue) return modelValue;
|
||||
const params = { prompt: 'a cute cat', options: extraOptions };
|
||||
return params[paramName as keyof typeof params];
|
||||
};
|
||||
|
||||
it('should extract model value from RLC and include response_format for dall-e-3', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(makeRlcMock('dall-e-3'));
|
||||
|
||||
apiRequestSpy.mockResolvedValue(b64Response);
|
||||
|
||||
await execute.call(mockExecuteFunctions, 0);
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/images/generations', {
|
||||
body: expect.objectContaining({ model: 'dall-e-3', response_format: 'b64_json' }),
|
||||
});
|
||||
});
|
||||
|
||||
it('should not send response_format for gpt-image-1', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(makeRlcMock('gpt-image-1'));
|
||||
|
||||
apiRequestSpy.mockResolvedValue(b64Response);
|
||||
|
||||
await execute.call(mockExecuteFunctions, 0);
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/images/generations', {
|
||||
body: expect.objectContaining({ model: 'gpt-image-1', response_format: undefined }),
|
||||
});
|
||||
});
|
||||
|
||||
it('should not send response_format for gpt-image-1-mini', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(makeRlcMock('gpt-image-1-mini'));
|
||||
|
||||
apiRequestSpy.mockResolvedValue(b64Response);
|
||||
|
||||
await execute.call(mockExecuteFunctions, 0);
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/images/generations', {
|
||||
body: expect.objectContaining({ model: 'gpt-image-1-mini', response_format: undefined }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('response handling', () => {
|
||||
beforeEach(() => {
|
||||
mockNode = makeNode(2.2);
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
|
||||
});
|
||||
|
||||
const makeRlcMock =
|
||||
(modelValue: string, options = {}) =>
|
||||
(paramName: string, _i: unknown, _default: unknown, opts?: { extractValue?: boolean }) => {
|
||||
if (paramName === 'modelId' && opts?.extractValue) return modelValue;
|
||||
const params = { prompt: 'a cute cat', options };
|
||||
return params[paramName as keyof typeof params];
|
||||
};
|
||||
|
||||
it('should return binary data by default', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(makeRlcMock('dall-e-3'));
|
||||
|
||||
apiRequestSpy.mockResolvedValue(b64Response);
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, 0);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
json: expect.objectContaining({ data: undefined }),
|
||||
binary: { data: mockBinaryData },
|
||||
pairedItem: { item: 0 },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return URLs when returnImageUrls is true and model supports it', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(
|
||||
makeRlcMock('dall-e-3', { returnImageUrls: true }),
|
||||
);
|
||||
|
||||
apiRequestSpy.mockResolvedValue({ data: [{ url: 'https://example.com/image.png' }] });
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, 0);
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/images/generations', {
|
||||
body: expect.objectContaining({ response_format: 'url' }),
|
||||
});
|
||||
expect(result).toEqual([
|
||||
{ json: { url: 'https://example.com/image.png' }, pairedItem: { item: 0 } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return binary even when returnImageUrls is true for gpt-image models', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(
|
||||
makeRlcMock('gpt-image-1-mini', { returnImageUrls: true }),
|
||||
);
|
||||
|
||||
apiRequestSpy.mockResolvedValue(b64Response);
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, 0);
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/images/generations', {
|
||||
body: expect.objectContaining({ response_format: undefined }),
|
||||
});
|
||||
expect(result[0].binary).toBeDefined();
|
||||
});
|
||||
|
||||
it('should use custom binaryPropertyOutput field name', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(
|
||||
makeRlcMock('dall-e-3', { binaryPropertyOutput: 'myImage' }),
|
||||
);
|
||||
|
||||
apiRequestSpy.mockResolvedValue(b64Response);
|
||||
|
||||
const result = await execute.call(mockExecuteFunctions, 0);
|
||||
|
||||
expect(result[0].binary).toHaveProperty('myImage');
|
||||
});
|
||||
});
|
||||
|
||||
describe('options processing', () => {
|
||||
beforeEach(() => {
|
||||
mockNode = makeNode(2.2);
|
||||
mockExecuteFunctions.getNode.mockReturnValue(mockNode);
|
||||
});
|
||||
|
||||
it('should rename dalleQuality to quality before sending', async () => {
|
||||
mockExecuteFunctions.getNodeParameter.mockImplementation(
|
||||
(paramName: string, _i: unknown, _default: unknown, opts?: { extractValue?: boolean }) => {
|
||||
if (paramName === 'modelId' && opts?.extractValue) return 'dall-e-3';
|
||||
const params = { prompt: 'a cute cat', options: { dalleQuality: 'hd' } };
|
||||
return params[paramName as keyof typeof params];
|
||||
},
|
||||
);
|
||||
|
||||
apiRequestSpy.mockResolvedValue(b64Response);
|
||||
|
||||
await execute.call(mockExecuteFunctions, 0);
|
||||
|
||||
expect(apiRequestSpy).toHaveBeenCalledWith('POST', '/images/generations', {
|
||||
body: expect.objectContaining({ quality: 'hd' }),
|
||||
});
|
||||
expect(apiRequestSpy).toHaveBeenCalledWith(
|
||||
'POST',
|
||||
'/images/generations',
|
||||
expect.objectContaining({
|
||||
body: expect.not.objectContaining({ dalleQuality: expect.anything() }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -23,7 +23,7 @@ export class OpenAiV2 implements INodeType {
|
|||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
version: [2, 2.1],
|
||||
version: [2, 2.1, 2.2],
|
||||
defaults: {
|
||||
name: 'OpenAI',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -253,3 +253,412 @@ export const messageOptions: INodePropertyCollection[] = [
|
|||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const imageGenerateOptions: INodeProperties = {
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { lt: 2.2 } }],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Number of Images',
|
||||
name: 'n',
|
||||
default: 1,
|
||||
description: 'Number of images to generate',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 10,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['dall-e-2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Quality',
|
||||
name: 'dalleQuality',
|
||||
type: 'options',
|
||||
description:
|
||||
'The quality of the image that will be generated, HD creates images with finer details and greater consistency across the image',
|
||||
options: [
|
||||
{
|
||||
name: 'HD',
|
||||
value: 'hd',
|
||||
},
|
||||
{
|
||||
name: 'Standard',
|
||||
value: 'standard',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['dall-e-3'],
|
||||
},
|
||||
},
|
||||
default: 'standard',
|
||||
},
|
||||
{
|
||||
displayName: 'Quality',
|
||||
name: 'quality',
|
||||
type: 'options',
|
||||
description:
|
||||
'The quality of the image that will be generated, High creates images with finer details and greater consistency across the image',
|
||||
options: [
|
||||
{
|
||||
name: 'High',
|
||||
value: 'high',
|
||||
},
|
||||
{
|
||||
name: 'Medium',
|
||||
value: 'medium',
|
||||
},
|
||||
{
|
||||
name: 'Low',
|
||||
value: 'low',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': [{ _cnd: { includes: 'gpt-image' } }],
|
||||
},
|
||||
},
|
||||
default: 'medium',
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Resolution',
|
||||
name: 'size',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: '256x256',
|
||||
value: '256x256',
|
||||
},
|
||||
{
|
||||
name: '512x512',
|
||||
value: '512x512',
|
||||
},
|
||||
{
|
||||
name: '1024x1024',
|
||||
value: '1024x1024',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['dall-e-2'],
|
||||
},
|
||||
},
|
||||
default: '1024x1024',
|
||||
},
|
||||
{
|
||||
displayName: 'Resolution',
|
||||
name: 'size',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: '1024x1024',
|
||||
value: '1024x1024',
|
||||
},
|
||||
{
|
||||
name: '1792x1024',
|
||||
value: '1792x1024',
|
||||
},
|
||||
{
|
||||
name: '1024x1792',
|
||||
value: '1024x1792',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['dall-e-3'],
|
||||
},
|
||||
},
|
||||
default: '1024x1024',
|
||||
},
|
||||
{
|
||||
displayName: 'Resolution',
|
||||
name: 'size',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: '1024x1024',
|
||||
value: '1024x1024',
|
||||
},
|
||||
{
|
||||
name: '1024x1536',
|
||||
value: '1024x1536',
|
||||
},
|
||||
{
|
||||
name: '1536x1024',
|
||||
value: '1536x1024',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': [{ _cnd: { includes: 'gpt-image' } }],
|
||||
},
|
||||
},
|
||||
default: '1024x1024',
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Style',
|
||||
name: 'style',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Natural',
|
||||
value: 'natural',
|
||||
description: 'Produce more natural looking images',
|
||||
},
|
||||
{
|
||||
name: 'Vivid',
|
||||
value: 'vivid',
|
||||
description: 'Lean towards generating hyper-real and dramatic images',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['dall-e-3'],
|
||||
},
|
||||
},
|
||||
default: 'vivid',
|
||||
},
|
||||
{
|
||||
displayName: 'Respond with Image URL(s)',
|
||||
name: 'returnImageUrls',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to return image URL(s) instead of binary file(s)',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
'/model': [{ _cnd: { includes: 'gpt-image' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Put Output in Field',
|
||||
name: 'binaryPropertyOutput',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
hint: 'The name of the output field to put the binary file data in',
|
||||
displayOptions: {
|
||||
show: {
|
||||
returnImageUrls: [false],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const imageGenerateOptionsRLC: INodeProperties = {
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 2.2 } }],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Number of Images',
|
||||
name: 'n',
|
||||
default: 1,
|
||||
description: 'Number of images to generate',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 10,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/modelId': ['dall-e-2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Quality',
|
||||
name: 'dalleQuality',
|
||||
type: 'options',
|
||||
description:
|
||||
'The quality of the image that will be generated, HD creates images with finer details and greater consistency across the image',
|
||||
options: [
|
||||
{
|
||||
name: 'HD',
|
||||
value: 'hd',
|
||||
},
|
||||
{
|
||||
name: 'Standard',
|
||||
value: 'standard',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/modelId': ['dall-e-3'],
|
||||
},
|
||||
},
|
||||
default: 'standard',
|
||||
},
|
||||
{
|
||||
displayName: 'Quality',
|
||||
name: 'quality',
|
||||
type: 'options',
|
||||
description:
|
||||
'The quality of the image that will be generated, High creates images with finer details and greater consistency across the image',
|
||||
options: [
|
||||
{
|
||||
name: 'High',
|
||||
value: 'high',
|
||||
},
|
||||
{
|
||||
name: 'Medium',
|
||||
value: 'medium',
|
||||
},
|
||||
{
|
||||
name: 'Low',
|
||||
value: 'low',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/modelId': [{ _cnd: { includes: 'gpt-image' } }],
|
||||
},
|
||||
},
|
||||
default: 'medium',
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Resolution',
|
||||
name: 'size',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: '256x256',
|
||||
value: '256x256',
|
||||
},
|
||||
{
|
||||
name: '512x512',
|
||||
value: '512x512',
|
||||
},
|
||||
{
|
||||
name: '1024x1024',
|
||||
value: '1024x1024',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/modelId': ['dall-e-2'],
|
||||
},
|
||||
},
|
||||
default: '1024x1024',
|
||||
},
|
||||
{
|
||||
displayName: 'Resolution',
|
||||
name: 'size',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: '1024x1024',
|
||||
value: '1024x1024',
|
||||
},
|
||||
{
|
||||
name: '1792x1024',
|
||||
value: '1792x1024',
|
||||
},
|
||||
{
|
||||
name: '1024x1792',
|
||||
value: '1024x1792',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/modelId': ['dall-e-3'],
|
||||
},
|
||||
},
|
||||
default: '1024x1024',
|
||||
},
|
||||
{
|
||||
displayName: 'Resolution',
|
||||
name: 'size',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: '1024x1024',
|
||||
value: '1024x1024',
|
||||
},
|
||||
{
|
||||
name: '1024x1536',
|
||||
value: '1024x1536',
|
||||
},
|
||||
{
|
||||
name: '1536x1024',
|
||||
value: '1536x1024',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/modelId': [{ _cnd: { includes: 'gpt-image' } }],
|
||||
},
|
||||
},
|
||||
default: '1024x1024',
|
||||
},
|
||||
{
|
||||
displayName: 'Style',
|
||||
name: 'style',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Natural',
|
||||
value: 'natural',
|
||||
description: 'Produce more natural looking images',
|
||||
},
|
||||
{
|
||||
name: 'Vivid',
|
||||
value: 'vivid',
|
||||
description: 'Lean towards generating hyper-real and dramatic images',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/modelId': ['dall-e-3'],
|
||||
},
|
||||
},
|
||||
default: 'vivid',
|
||||
},
|
||||
{
|
||||
displayName: 'Respond with Image URL(s)',
|
||||
name: 'returnImageUrls',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to return image URL(s) instead of binary file(s)',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
'/modelId': [{ _cnd: { includes: 'gpt-image' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Put Output in Field',
|
||||
name: 'binaryPropertyOutput',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
hint: 'The name of the output field to put the binary file data in',
|
||||
displayOptions: {
|
||||
show: {
|
||||
returnImageUrls: [false],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type {
|
|||
import { updateDisplayOptions } from 'n8n-workflow';
|
||||
|
||||
import { apiRequest } from '../../../transport';
|
||||
import { imageGenerateOptions, imageGenerateOptionsRLC, modelRLC } from '../descriptions';
|
||||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
|
|
@ -29,6 +30,20 @@ const properties: INodeProperties[] = [
|
|||
value: 'gpt-image-1',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { lt: 2.2 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...modelRLC('imageGenerateModelSearch'),
|
||||
default: { mode: 'list', value: 'gpt-image-1-mini' },
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 2.2 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Prompt',
|
||||
|
|
@ -42,205 +57,8 @@ const properties: INodeProperties[] = [
|
|||
rows: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Number of Images',
|
||||
name: 'n',
|
||||
default: 1,
|
||||
description: 'Number of images to generate',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
maxValue: 10,
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['dall-e-2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Quality',
|
||||
name: 'dalleQuality',
|
||||
type: 'options',
|
||||
description:
|
||||
'The quality of the image that will be generated, HD creates images with finer details and greater consistency across the image',
|
||||
options: [
|
||||
{
|
||||
name: 'HD',
|
||||
value: 'hd',
|
||||
},
|
||||
{
|
||||
name: 'Standard',
|
||||
value: 'standard',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['dall-e-3'],
|
||||
},
|
||||
},
|
||||
default: 'standard',
|
||||
},
|
||||
{
|
||||
displayName: 'Quality',
|
||||
name: 'quality',
|
||||
type: 'options',
|
||||
description:
|
||||
'The quality of the image that will be generated, High creates images with finer details and greater consistency across the image',
|
||||
options: [
|
||||
{
|
||||
name: 'High',
|
||||
value: 'high',
|
||||
},
|
||||
{
|
||||
name: 'Medium',
|
||||
value: 'medium',
|
||||
},
|
||||
{
|
||||
name: 'Low',
|
||||
value: 'low',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['gpt-image-1'],
|
||||
},
|
||||
},
|
||||
default: 'medium',
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Resolution',
|
||||
name: 'size',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: '256x256',
|
||||
value: '256x256',
|
||||
},
|
||||
{
|
||||
name: '512x512',
|
||||
value: '512x512',
|
||||
},
|
||||
{
|
||||
name: '1024x1024',
|
||||
value: '1024x1024',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['dall-e-2'],
|
||||
},
|
||||
},
|
||||
default: '1024x1024',
|
||||
},
|
||||
{
|
||||
displayName: 'Resolution',
|
||||
name: 'size',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: '1024x1024',
|
||||
value: '1024x1024',
|
||||
},
|
||||
{
|
||||
name: '1792x1024',
|
||||
value: '1792x1024',
|
||||
},
|
||||
{
|
||||
name: '1024x1792',
|
||||
value: '1024x1792',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['dall-e-3'],
|
||||
},
|
||||
},
|
||||
default: '1024x1024',
|
||||
},
|
||||
{
|
||||
displayName: 'Resolution',
|
||||
name: 'size',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: '1024x1024',
|
||||
value: '1024x1024',
|
||||
},
|
||||
{
|
||||
name: '1024x1536',
|
||||
value: '1024x1536',
|
||||
},
|
||||
{
|
||||
name: '1536x1024',
|
||||
value: '1536x1024',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['gpt-image-1'],
|
||||
},
|
||||
},
|
||||
default: '1024x1024',
|
||||
},
|
||||
|
||||
{
|
||||
displayName: 'Style',
|
||||
name: 'style',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Natural',
|
||||
value: 'natural',
|
||||
description: 'Produce more natural looking images',
|
||||
},
|
||||
{
|
||||
name: 'Vivid',
|
||||
value: 'vivid',
|
||||
description: 'Lean towards generating hyper-real and dramatic images',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'/model': ['dall-e-3'],
|
||||
},
|
||||
},
|
||||
default: 'vivid',
|
||||
},
|
||||
{
|
||||
displayName: 'Respond with Image URL(s)',
|
||||
name: 'returnImageUrls',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to return image URL(s) instead of binary file(s)',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
'/model': ['gpt-image-1'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Put Output in Field',
|
||||
name: 'binaryPropertyOutput',
|
||||
type: 'string',
|
||||
default: 'data',
|
||||
hint: 'The name of the output field to put the binary file data in',
|
||||
displayOptions: {
|
||||
show: {
|
||||
returnImageUrls: [false],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
imageGenerateOptions,
|
||||
imageGenerateOptionsRLC,
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
|
|
@ -253,13 +71,22 @@ const displayOptions = {
|
|||
export const description = updateDisplayOptions(displayOptions, properties);
|
||||
|
||||
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
|
||||
const model = this.getNodeParameter('model', i) as string;
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
|
||||
let model = '';
|
||||
if (nodeVersion >= 2.2) {
|
||||
model = this.getNodeParameter('modelId', i, '', { extractValue: true }) as string;
|
||||
} else {
|
||||
model = this.getNodeParameter('model', i) as string;
|
||||
}
|
||||
|
||||
const prompt = this.getNodeParameter('prompt', i) as string;
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
const supportsResponseFormat = !model.startsWith('gpt-image');
|
||||
let response_format = 'b64_json';
|
||||
let binaryPropertyOutput = 'data';
|
||||
|
||||
if (options.returnImageUrls) {
|
||||
if (options.returnImageUrls && supportsResponseFormat) {
|
||||
response_format = 'url';
|
||||
}
|
||||
|
||||
|
|
@ -277,7 +104,7 @@ export async function execute(this: IExecuteFunctions, i: number): Promise<INode
|
|||
const body: IDataObject = {
|
||||
prompt,
|
||||
model,
|
||||
response_format: model !== 'gpt-image-1' ? response_format : undefined, // gpt-image-1 does not support response_format
|
||||
response_format: supportsResponseFormat ? response_format : undefined,
|
||||
...options,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -374,16 +374,21 @@ export const checkConditions = (
|
|||
return (propertyValue as number) >= from && (propertyValue as number) <= to;
|
||||
}
|
||||
if (key === 'includes') {
|
||||
return (propertyValue as string).includes(targetValue);
|
||||
return typeof propertyValue === 'string' && propertyValue.includes(targetValue as string);
|
||||
}
|
||||
if (key === 'startsWith') {
|
||||
return (propertyValue as string).startsWith(targetValue);
|
||||
return (
|
||||
typeof propertyValue === 'string' && propertyValue.startsWith(targetValue as string)
|
||||
);
|
||||
}
|
||||
if (key === 'endsWith') {
|
||||
return (propertyValue as string).endsWith(targetValue);
|
||||
return typeof propertyValue === 'string' && propertyValue.endsWith(targetValue as string);
|
||||
}
|
||||
if (key === 'regex') {
|
||||
return new RegExp(targetValue as string).test(propertyValue as string);
|
||||
return (
|
||||
typeof propertyValue === 'string' &&
|
||||
new RegExp(targetValue as string).test(propertyValue)
|
||||
);
|
||||
}
|
||||
if (key === 'exists') {
|
||||
return propertyValue !== null && propertyValue !== undefined && propertyValue !== '';
|
||||
|
|
|
|||
|
|
@ -5275,6 +5275,60 @@ describe('NodeHelpers', () => {
|
|||
expect(result).toEqual(expected);
|
||||
});
|
||||
}
|
||||
|
||||
describe('_cnd string operators with undefined property value', () => {
|
||||
const node: INode = { ...testNode };
|
||||
const nodeTypeDescription: INodeTypeDescription = { ...testNodeType };
|
||||
|
||||
const baseParam: INodeProperties = {
|
||||
displayName: 'Test',
|
||||
name: 'test',
|
||||
type: 'string',
|
||||
default: '',
|
||||
};
|
||||
|
||||
test('includes returns false when property is undefined', () => {
|
||||
const param: INodeProperties = {
|
||||
...baseParam,
|
||||
displayOptions: { show: { '/missingParam': [{ _cnd: { includes: 'foo' } }] } },
|
||||
};
|
||||
expect(displayParameter({}, param, node, nodeTypeDescription)).toBe(false);
|
||||
});
|
||||
|
||||
test('startsWith returns false when property is undefined', () => {
|
||||
const param: INodeProperties = {
|
||||
...baseParam,
|
||||
displayOptions: { show: { '/missingParam': [{ _cnd: { startsWith: 'foo' } }] } },
|
||||
};
|
||||
expect(displayParameter({}, param, node, nodeTypeDescription)).toBe(false);
|
||||
});
|
||||
|
||||
test('endsWith returns false when property is undefined', () => {
|
||||
const param: INodeProperties = {
|
||||
...baseParam,
|
||||
displayOptions: { show: { '/missingParam': [{ _cnd: { endsWith: 'foo' } }] } },
|
||||
};
|
||||
expect(displayParameter({}, param, node, nodeTypeDescription)).toBe(false);
|
||||
});
|
||||
|
||||
test('regex returns false when property is undefined', () => {
|
||||
const param: INodeProperties = {
|
||||
...baseParam,
|
||||
displayOptions: { show: { '/missingParam': [{ _cnd: { regex: 'foo' } }] } },
|
||||
};
|
||||
expect(displayParameter({}, param, node, nodeTypeDescription)).toBe(false);
|
||||
});
|
||||
|
||||
test('includes returns true when property matches', () => {
|
||||
const param: INodeProperties = {
|
||||
...baseParam,
|
||||
displayOptions: { show: { '/missingParam': [{ _cnd: { includes: 'foo' } }] } },
|
||||
};
|
||||
expect(displayParameter({ missingParam: 'foobar' }, param, node, nodeTypeDescription)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeDescription', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue