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

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,85 @@
import {
NodeConnectionTypes,
type IExecuteFunctions,
type INodeType,
type INodeTypeBaseDescription,
type INodeTypeDescription,
} from 'n8n-workflow';
import { listSearch, loadOptions } from '../methods';
import { router } from './actions/router';
import { configureNodeInputs } from '../helpers/description';
import * as assistant from './actions/assistant';
import * as audio from './actions/audio';
import * as file from './actions/file';
import * as image from './actions/image';
import * as text from './actions/text';
export class OpenAiV1 implements INodeType {
description: INodeTypeDescription;
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
version: [1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8],
defaults: {
name: 'OpenAI',
},
inputs: `={{(${configureNodeInputs})($parameter.resource, $parameter.operation, $parameter.hideTools, $parameter.memory ?? undefined)}}`,
outputs: [NodeConnectionTypes.Main],
credentials: [
{
name: 'openAiApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
options: [
{
name: 'Assistant',
value: 'assistant',
},
{
name: 'Text',
value: 'text',
},
{
name: 'Image',
value: 'image',
},
{
name: 'Audio',
value: 'audio',
},
{
name: 'File',
value: 'file',
},
],
default: 'text',
},
...assistant.description,
...audio.description,
...file.description,
...image.description,
...text.description,
],
};
}
methods = {
listSearch,
loadOptions,
};
async execute(this: IExecuteFunctions) {
return await router.call(this);
}
}
@@ -0,0 +1,290 @@
import type {
INodeProperties,
IExecuteFunctions,
INodeExecutionData,
IDataObject,
} from 'n8n-workflow';
import { NodeOperationError, updateDisplayOptions } from 'n8n-workflow';
import { apiRequest } from '../../../transport';
import { modelRLC } from '../descriptions';
const properties: INodeProperties[] = [
modelRLC('modelSearch'),
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'The name of the assistant. The maximum length is 256 characters.',
placeholder: 'e.g. My Assistant',
required: true,
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
description: 'The description of the assistant. The maximum length is 512 characters.',
placeholder: 'e.g. My personal assistant',
},
{
displayName: 'Instructions',
name: 'instructions',
type: 'string',
description:
'The system instructions that the assistant uses. The maximum length is 32768 characters.',
default: '',
typeOptions: {
rows: 2,
},
},
{
displayName: 'Code Interpreter',
name: 'codeInterpreter',
type: 'boolean',
default: false,
description:
'Whether to enable the code interpreter that allows the assistants to write and run Python code in a sandboxed execution environment, find more <a href="https://platform.openai.com/docs/assistants/tools/code-interpreter" target="_blank">here</a>',
},
{
displayName: 'Knowledge Retrieval',
name: 'knowledgeRetrieval',
type: 'boolean',
default: false,
description:
'Whether to augments the assistant with knowledge from outside its model, such as proprietary product information or documents, find more <a href="https://platform.openai.com/docs/assistants/tools/knowledge-retrieval" target="_blank">here</a>',
},
//we want to display Files selector only when codeInterpreter true or knowledgeRetrieval true or both
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options
displayName: 'Files',
name: 'file_ids',
type: 'multiOptions',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options
description:
'The files to be used by the assistant, there can be a maximum of 20 files attached to the assistant. You can use expression to pass file IDs as an array or comma-separated string.',
typeOptions: {
loadOptionsMethod: 'getFiles',
},
default: [],
hint: "Add more files by using the 'Upload a File' operation",
displayOptions: {
show: {
codeInterpreter: [true],
},
hide: {
knowledgeRetrieval: [true],
},
},
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options
displayName: 'Files',
name: 'file_ids',
type: 'multiOptions',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options
description:
'The files to be used by the assistant, there can be a maximum of 20 files attached to the assistant',
typeOptions: {
loadOptionsMethod: 'getFiles',
},
default: [],
hint: "Add more files by using the 'Upload a File' operation",
displayOptions: {
show: {
knowledgeRetrieval: [true],
},
hide: {
codeInterpreter: [true],
},
},
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options
displayName: 'Files',
name: 'file_ids',
type: 'multiOptions',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options
description:
'The files to be used by the assistant, there can be a maximum of 20 files attached to the assistant',
typeOptions: {
loadOptionsMethod: 'getFiles',
},
default: [],
hint: "Add more files by using the 'Upload a File' operation",
displayOptions: {
show: {
knowledgeRetrieval: [true],
codeInterpreter: [true],
},
},
},
{
displayName:
'Add custom n8n tools when you <i>message</i> your assistant (rather than when creating it)',
name: 'noticeTools',
type: 'notice',
default: '',
},
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
type: 'collection',
default: {},
options: [
{
displayName: 'Output Randomness (Temperature)',
name: 'temperature',
default: 1,
typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 },
description:
'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive. We generally recommend altering this or temperature but not both.',
type: 'number',
},
{
displayName: 'Output Randomness (Top P)',
name: 'topP',
default: 1,
typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 },
description:
'An alternative to sampling with temperature, controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered. We generally recommend altering this or temperature but not both.',
type: 'number',
},
{
displayName: 'Fail if Assistant Already Exists',
name: 'failIfExists',
type: 'boolean',
default: false,
description:
'Whether to fail an operation if the assistant with the same name already exists',
},
],
},
];
const displayOptions = {
show: {
operation: ['create'],
resource: ['assistant'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const model = this.getNodeParameter('modelId', i, '', { extractValue: true }) as string;
const name = this.getNodeParameter('name', i) as string;
const assistantDescription = this.getNodeParameter('description', i) as string;
const instructions = this.getNodeParameter('instructions', i) as string;
const codeInterpreter = this.getNodeParameter('codeInterpreter', i) as boolean;
const knowledgeRetrieval = this.getNodeParameter('knowledgeRetrieval', i) as boolean;
let file_ids = this.getNodeParameter('file_ids', i, []) as string[] | string;
if (typeof file_ids === 'string') {
file_ids = file_ids.split(',').map((file_id) => file_id.trim());
}
const options = this.getNodeParameter('options', i, {});
if (options.failIfExists) {
const assistants: string[] = [];
let has_more = true;
let after: string | undefined;
do {
const response = (await apiRequest.call(this, 'GET', '/assistants', {
headers: {
'OpenAI-Beta': 'assistants=v2',
},
qs: {
limit: 100,
after,
},
})) as { data: IDataObject[]; has_more: boolean; last_id: string };
for (const assistant of response.data || []) {
assistants.push(assistant.name as string);
}
has_more = response.has_more;
if (has_more) {
after = response.last_id;
} else {
break;
}
} while (has_more);
if (assistants.includes(name)) {
throw new NodeOperationError(
this.getNode(),
`An assistant with the same name '${name}' already exists`,
{ itemIndex: i },
);
}
}
if (file_ids.length > 20) {
throw new NodeOperationError(
this.getNode(),
'The maximum number of files that can be attached to the assistant is 20',
{ itemIndex: i },
);
}
const body: IDataObject = {
model,
name,
description: assistantDescription,
instructions,
};
const tools = [];
if (codeInterpreter) {
tools.push({
type: 'code_interpreter',
});
body.tool_resources = {
...((body.tool_resources as object) ?? {}),
code_interpreter: {
file_ids,
},
};
}
if (knowledgeRetrieval) {
tools.push({
type: 'file_search',
});
body.tool_resources = {
...((body.tool_resources as object) ?? {}),
file_search: {
vector_stores: [
{
file_ids,
},
],
},
};
}
if (tools.length) {
body.tools = tools;
}
const response = await apiRequest.call(this, 'POST', '/assistants', {
body,
headers: {
'OpenAI-Beta': 'assistants=v2',
},
});
return [
{
json: response,
pairedItem: { item: i },
},
];
}
@@ -0,0 +1,33 @@
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import { apiRequest } from '../../../transport';
import { assistantRLC } from '../descriptions';
const properties: INodeProperties[] = [assistantRLC];
const displayOptions = {
show: {
operation: ['deleteAssistant'],
resource: ['assistant'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const assistantId = this.getNodeParameter('assistantId', i, '', { extractValue: true }) as string;
const response = await apiRequest.call(this, 'DELETE', `/assistants/${assistantId}`, {
headers: {
'OpenAI-Beta': 'assistants=v2',
},
});
return [
{
json: response,
pairedItem: { item: i },
},
];
}
@@ -0,0 +1,62 @@
import type { INodeProperties } from 'n8n-workflow';
import * as create from './create.operation';
import * as deleteAssistant from './deleteAssistant.operation';
import * as list from './list.operation';
import * as message from './message.operation';
import * as update from './update.operation';
export { create, deleteAssistant, message, list, update };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Create an Assistant',
value: 'create',
action: 'Create an assistant',
description: 'Create a new assistant',
},
{
name: 'Delete an Assistant',
value: 'deleteAssistant',
action: 'Delete an assistant',
description: 'Delete an assistant from the account',
},
{
name: 'List Assistants',
value: 'list',
action: 'List assistants',
description: 'List assistants in the organization',
},
{
name: 'Message an Assistant',
value: 'message',
action: 'Message an assistant',
description: 'Send messages to an assistant',
},
{
name: 'Update an Assistant',
value: 'update',
action: 'Update an assistant',
description: 'Update an existing assistant',
},
],
default: 'message',
displayOptions: {
show: {
resource: ['assistant'],
},
},
},
...create.description,
...deleteAssistant.description,
...message.description,
...list.description,
...update.description,
];
@@ -0,0 +1,76 @@
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import { apiRequest } from '../../../transport';
const properties: INodeProperties[] = [
{
displayName: 'Simplify Output',
name: 'simplify',
type: 'boolean',
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
];
const displayOptions = {
show: {
operation: ['list'],
resource: ['assistant'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
let has_more = true;
let after: string | undefined;
do {
const response = await apiRequest.call(this, 'GET', '/assistants', {
headers: {
'OpenAI-Beta': 'assistants=v2',
},
qs: {
limit: 100,
after,
},
});
for (const assistant of response.data || []) {
try {
assistant.created_at = new Date(assistant.created_at * 1000).toISOString();
} catch (error) {}
returnData.push({ json: assistant, pairedItem: { item: i } });
}
has_more = response.has_more;
if (has_more) {
after = response.last_id as string;
} else {
break;
}
} while (has_more);
const simplify = this.getNodeParameter('simplify', i) as boolean;
if (simplify) {
return returnData.map((item) => {
const { id, name, model } = item.json;
return {
json: {
id,
name,
model,
},
pairedItem: { item: i },
};
});
}
return returnData;
}
@@ -0,0 +1,318 @@
import type { BaseMessage } from '@langchain/core/messages';
import { AgentExecutor } from '@langchain/classic/agents';
import type { OpenAIToolType } from '@langchain/classic/dist/experimental/openai_assistant/schema';
import { OpenAIAssistantRunnable } from '@langchain/classic/experimental/openai_assistant';
import type { BufferWindowMemory } from '@langchain/classic/memory';
import omit from 'lodash/omit';
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import {
ApplicationError,
NodeConnectionTypes,
NodeOperationError,
updateDisplayOptions,
} from 'n8n-workflow';
import { OpenAI as OpenAIClient } from 'openai';
import { promptTypeOptionsDeprecated } from '@utils/descriptions';
import { getConnectedTools, getPromptInputByType, mergeCustomHeaders } from '@utils/helpers';
import { getTracingConfig } from '@utils/tracing';
import { formatToOpenAIAssistantTool, getChatMessages } from '../../../helpers/utils';
import { assistantRLC } from '../descriptions';
import { getProxyAgent } from '@n8n/ai-utilities';
import { Container } from '@n8n/di';
import { AiConfig } from '@n8n/config';
import { checkDomainRestrictions } from '@utils/checkDomainRestrictions';
const properties: INodeProperties[] = [
assistantRLC,
{
...promptTypeOptionsDeprecated,
name: 'prompt',
},
{
displayName: 'Prompt (User Message)',
name: 'text',
type: 'string',
default: '',
placeholder: 'e.g. Hello, how can you help me?',
typeOptions: {
rows: 2,
},
displayOptions: {
show: {
prompt: ['define'],
},
},
},
{
displayName: 'Memory',
name: 'memory',
type: 'options',
options: [
{
name: 'Use memory connector',
value: 'connector',
description: 'Connect one of the supported memory nodes',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Use thread ID',
value: 'threadId',
description: 'Specify the ID of the thread to continue',
},
],
displayOptions: {
show: {
'@version': [{ _cnd: { gte: 1.6 } }],
},
},
default: 'connector',
},
{
displayName: 'Thread ID',
name: 'threadId',
type: 'string',
default: '',
placeholder: '',
description: 'The ID of the thread to continue, a new thread will be created if not specified',
hint: 'If the thread ID is empty or undefined a new thread will be created and included in the response',
displayOptions: {
show: {
'@version': [{ _cnd: { gte: 1.6 } }],
memory: ['threadId'],
},
},
},
{
displayName: 'Connect your own custom n8n tools to this node on the canvas',
name: 'noticeTools',
type: 'notice',
default: '',
},
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
description: 'Additional options to add',
type: 'collection',
default: {},
options: [
{
displayName: 'Base URL',
name: 'baseURL',
default: 'https://api.openai.com/v1',
description: 'Override the default base URL for the API',
type: 'string',
displayOptions: {
hide: {
'@version': [{ _cnd: { gte: 1.8 } }],
},
},
},
{
displayName: 'Max Retries',
name: 'maxRetries',
default: 2,
description: 'Maximum number of retries to attempt',
type: 'number',
},
{
displayName: 'Timeout',
name: 'timeout',
default: 10000,
description: 'Maximum amount of time a request is allowed to take in milliseconds',
type: 'number',
},
{
displayName: 'Preserve Original Tools',
name: 'preserveOriginalTools',
type: 'boolean',
default: true,
description:
'Whether to preserve the original tools of the assistant after the execution of this node, otherwise the tools will be replaced with the connected tools, if any, default is true',
displayOptions: {
show: {
'@version': [{ _cnd: { gte: 1.3 } }],
},
},
},
],
},
];
const displayOptions = {
show: {
operation: ['message'],
resource: ['assistant'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
const mapChatMessageToThreadMessage = (
message: BaseMessage,
): OpenAIClient.Beta.Threads.ThreadCreateParams.Message => ({
role: message._getType() === 'ai' ? 'assistant' : 'user',
content: message.content.toString(),
});
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const credentials = await this.getCredentials('openAiApi');
const nodeVersion = this.getNode().typeVersion;
const input = getPromptInputByType({
ctx: this,
i,
inputKey: 'text',
promptTypeKey: 'prompt',
});
const assistantId = this.getNodeParameter('assistantId', i, '', { extractValue: true }) as string;
const options = this.getNodeParameter('options', i, {}) as {
baseURL?: string;
maxRetries: number;
timeout: number;
preserveOriginalTools?: boolean;
};
if (options.baseURL) {
checkDomainRestrictions(this, credentials, options.baseURL);
}
const baseURL = (options.baseURL ?? credentials.url) as string;
const { openAiDefaultHeaders } = Container.get(AiConfig);
const defaultHeaders = mergeCustomHeaders(credentials, openAiDefaultHeaders ?? {});
const timeout = options.timeout;
const client = new OpenAIClient({
apiKey: credentials.apiKey as string,
maxRetries: options.maxRetries ?? 2,
timeout: timeout ?? 10000,
baseURL,
fetchOptions: {
dispatcher: getProxyAgent(baseURL, {
headersTimeout: timeout,
bodyTimeout: timeout,
}),
},
defaultHeaders,
});
const agent = new OpenAIAssistantRunnable({ assistantId, client, asAgent: true });
const tools = await getConnectedTools(this, nodeVersion > 1, false);
let assistantTools;
if (tools.length) {
const transformedConnectedTools = tools?.map(formatToOpenAIAssistantTool) ?? [];
const nativeToolsParsed: OpenAIToolType = [];
assistantTools = (await client.beta.assistants.retrieve(assistantId)).tools;
const useCodeInterpreter = assistantTools.some((tool) => tool.type === 'code_interpreter');
if (useCodeInterpreter) {
nativeToolsParsed.push({
type: 'code_interpreter',
});
}
const useRetrieval = assistantTools.some((tool) => tool.type === 'file_search');
if (useRetrieval) {
nativeToolsParsed.push({
type: 'file_search',
});
}
await client.beta.assistants.update(assistantId, {
tools: [...nativeToolsParsed, ...transformedConnectedTools],
});
}
const agentExecutor = AgentExecutor.fromAgentAndTools({
agent,
tools: tools ?? [],
});
const useMemoryConnector =
nodeVersion >= 1.6 && this.getNodeParameter('memory', i) === 'connector';
const memory =
useMemoryConnector || nodeVersion < 1.6
? ((await this.getInputConnectionData(NodeConnectionTypes.AiMemory, 0)) as
| BufferWindowMemory
| undefined)
: undefined;
const threadId =
nodeVersion >= 1.6 && !useMemoryConnector
? (this.getNodeParameter('threadId', i) as string)
: undefined;
const chainValues: IDataObject = {
content: input,
signal: this.getExecutionCancelSignal(),
timeout: options.timeout ?? 10000,
};
let thread: OpenAIClient.Beta.Threads.Thread;
if (memory) {
const chatMessages = await getChatMessages(memory);
// Construct a new thread from the chat history to map the memory
if (chatMessages.length) {
const first32Messages = chatMessages.slice(0, 32);
// There is a undocumented limit of 32 messages per thread when creating a thread with messages
const mappedMessages: OpenAIClient.Beta.Threads.ThreadCreateParams.Message[] =
first32Messages.map(mapChatMessageToThreadMessage);
thread = await client.beta.threads.create({ messages: mappedMessages });
const overLimitMessages = chatMessages.slice(32).map(mapChatMessageToThreadMessage);
// Send the remaining messages that exceed the limit of 32 sequentially
for (const message of overLimitMessages) {
await client.beta.threads.messages.create(thread.id, message);
}
chainValues.threadId = thread.id;
}
} else if (threadId) {
chainValues.threadId = threadId;
}
let filteredResponse: IDataObject = {};
try {
const response = await agentExecutor.withConfig(getTracingConfig(this)).invoke(chainValues);
if (memory) {
await memory.saveContext({ input }, { output: response.output });
if (response.threadId && response.runId) {
const threadRun = await client.beta.threads.runs.retrieve(response.runId, {
thread_id: response.threadId,
});
response.usage = threadRun.usage;
}
}
if (
options.preserveOriginalTools !== false &&
nodeVersion >= 1.3 &&
(assistantTools ?? [])?.length
) {
await client.beta.assistants.update(assistantId, {
tools: assistantTools,
});
}
// Remove configuration properties and runId added by Langchain that are not relevant to the user
filteredResponse = omit(response, ['signal', 'timeout', 'content', 'runId']) as IDataObject;
} catch (error) {
if (!(error instanceof ApplicationError)) {
throw new NodeOperationError(this.getNode(), error.message, { itemIndex: i });
}
}
return [{ json: filteredResponse, pairedItem: { item: i } }];
}
@@ -0,0 +1,246 @@
import type {
INodeProperties,
IExecuteFunctions,
INodeExecutionData,
IDataObject,
} from 'n8n-workflow';
import { ApplicationError, NodeOperationError, updateDisplayOptions } from 'n8n-workflow';
import { apiRequest } from '../../../transport';
import { assistantRLC, modelRLC } from '../descriptions';
const properties: INodeProperties[] = [
assistantRLC,
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
type: 'collection',
default: {},
options: [
{
displayName: 'Code Interpreter',
name: 'codeInterpreter',
type: 'boolean',
default: false,
description:
'Whether to enable the code interpreter that allows the assistants to write and run Python code in a sandboxed execution environment, find more <a href="https://platform.openai.com/docs/assistants/tools/code-interpreter" target="_blank">here</a>',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
description: 'The description of the assistant. The maximum length is 512 characters.',
placeholder: 'e.g. My personal assistant',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options
displayName: 'Files',
name: 'file_ids',
type: 'multiOptions',
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options
description:
'The files to be used by the assistant, there can be a maximum of 20 files attached to the assistant. You can use expression to pass file IDs as an array or comma-separated string.',
typeOptions: {
loadOptionsMethod: 'getFiles',
},
default: [],
hint: "Add more files by using the 'Upload a File' operation, any existing files not selected here will be removed.",
},
{
displayName: 'Instructions',
name: 'instructions',
type: 'string',
description:
'The system instructions that the assistant uses. The maximum length is 32768 characters.',
default: '',
typeOptions: {
rows: 2,
},
},
{
displayName: 'Knowledge Retrieval',
name: 'knowledgeRetrieval',
type: 'boolean',
default: false,
description:
'Whether to augments the assistant with knowledge from outside its model, such as proprietary product information or documents, find more <a href="https://platform.openai.com/docs/assistants/tools/knowledge-retrieval" target="_blank">here</a>',
},
{ ...modelRLC('modelSearch'), required: false },
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'The name of the assistant. The maximum length is 256 characters.',
placeholder: 'e.g. My Assistant',
},
{
displayName: 'Remove All Custom Tools (Functions)',
name: 'removeCustomTools',
type: 'boolean',
default: false,
description: 'Whether to remove all custom tools (functions) from the assistant',
},
{
displayName: 'Output Randomness (Temperature)',
name: 'temperature',
default: 1,
typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 },
description:
'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive. We generally recommend altering this or temperature but not both.',
type: 'number',
},
{
displayName: 'Output Randomness (Top P)',
name: 'topP',
default: 1,
typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 },
description:
'An alternative to sampling with temperature, controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered. We generally recommend altering this or temperature but not both.',
type: 'number',
},
],
},
];
const displayOptions = {
show: {
operation: ['update'],
resource: ['assistant'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
function getFileIds(file_ids: unknown): string[] {
if (Array.isArray(file_ids)) {
return file_ids;
}
if (typeof file_ids === 'string') {
return file_ids.split(',').map((file_id) => file_id.trim());
}
throw new ApplicationError('Invalid file_ids type');
}
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const assistantId = this.getNodeParameter('assistantId', i, '', { extractValue: true }) as string;
const options = this.getNodeParameter('options', i, {});
const {
modelId,
name,
instructions,
codeInterpreter,
knowledgeRetrieval,
file_ids,
removeCustomTools,
temperature,
topP,
} = options;
const assistantDescription = options.description as string;
const body: IDataObject = {};
if (file_ids) {
const files = getFileIds(file_ids);
if (files.length > 20) {
throw new NodeOperationError(
this.getNode(),
'The maximum number of files that can be attached to the assistant is 20',
{ itemIndex: i },
);
}
body.tool_resources = {
...((body.tool_resources as object) ?? {}),
code_interpreter: {
file_ids: files,
},
// updating file_ids for file_search directly is not supported by OpenAI API
// only updating vector_store_ids for file_search is supported
// support for this to be added as part of ADO-2968
// https://platform.openai.com/docs/api-reference/assistants/modifyAssistant
};
}
if (modelId) {
body.model = this.getNodeParameter('options.modelId', i, '', { extractValue: true }) as string;
}
if (name) {
body.name = name;
}
if (assistantDescription) {
body.description = assistantDescription;
}
if (instructions) {
body.instructions = instructions;
}
if (temperature) {
body.temperature = temperature;
}
if (topP) {
body.topP = topP;
}
let tools =
((
await apiRequest.call(this, 'GET', `/assistants/${assistantId}`, {
headers: {
'OpenAI-Beta': 'assistants=v2',
},
})
).tools as IDataObject[]) || [];
if (codeInterpreter && !tools.find((tool) => tool.type === 'code_interpreter')) {
tools.push({
type: 'code_interpreter',
});
}
if (codeInterpreter === false && tools.find((tool) => tool.type === 'code_interpreter')) {
tools = tools.filter((tool) => tool.type !== 'code_interpreter');
}
if (knowledgeRetrieval && !tools.find((tool) => tool.type === 'file_search')) {
tools.push({
type: 'file_search',
});
}
if (knowledgeRetrieval === false && tools.find((tool) => tool.type === 'file_search')) {
tools = tools.filter((tool) => tool.type !== 'file_search');
}
if (removeCustomTools) {
tools = tools.filter((tool) => tool.type !== 'function');
}
body.tools = tools;
const response = await apiRequest.call(this, 'POST', `/assistants/${assistantId}`, {
body,
headers: {
'OpenAI-Beta': 'assistants=v2',
},
});
return [
{
json: response,
pairedItem: { item: i },
},
];
}
@@ -0,0 +1,188 @@
import type {
INodeProperties,
IExecuteFunctions,
IDataObject,
INodeExecutionData,
} from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import { apiRequest } from '../../../transport';
const properties: INodeProperties[] = [
{
displayName: 'Model',
name: 'model',
type: 'options',
default: 'tts-1',
options: [
{
name: 'TTS-1',
value: 'tts-1',
},
{
name: 'TTS-1-HD',
value: 'tts-1-hd',
},
],
},
{
displayName: 'Text Input',
name: 'input',
type: 'string',
placeholder: 'e.g. The quick brown fox jumped over the lazy dog',
description: 'The text to generate audio for. The maximum length is 4096 characters.',
default: '',
typeOptions: {
rows: 2,
},
},
{
displayName: 'Voice',
name: 'voice',
type: 'options',
default: 'alloy',
description: 'The voice to use when generating the audio',
options: [
{
name: 'Alloy',
value: 'alloy',
},
{
name: 'Echo',
value: 'echo',
},
{
name: 'Fable',
value: 'fable',
},
{
name: 'Nova',
value: 'nova',
},
{
name: 'Onyx',
value: 'onyx',
},
{
name: 'Shimmer',
value: 'shimmer',
},
],
},
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
type: 'collection',
default: {},
options: [
{
displayName: 'Response Format',
name: 'response_format',
type: 'options',
default: 'mp3',
options: [
{
name: 'MP3',
value: 'mp3',
},
{
name: 'OPUS',
value: 'opus',
},
{
name: 'AAC',
value: 'aac',
},
{
name: 'FLAC',
value: 'flac',
},
],
},
{
displayName: 'Audio Speed',
name: 'speed',
type: 'number',
default: 1,
typeOptions: {
minValue: 0.25,
maxValue: 4,
numberPrecision: 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',
},
],
},
];
const displayOptions = {
show: {
operation: ['generate'],
resource: ['audio'],
},
};
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 input = this.getNodeParameter('input', i) as string;
const voice = this.getNodeParameter('voice', i) as string;
let response_format = 'mp3';
let speed = 1;
const options = this.getNodeParameter('options', i, {});
if (options.response_format) {
response_format = options.response_format as string;
}
if (options.speed) {
speed = options.speed as number;
}
const body: IDataObject = {
model,
input,
voice,
response_format,
speed,
};
const option = {
useStream: true,
returnFullResponse: true,
encoding: 'arraybuffer',
json: false,
};
const response = await apiRequest.call(this, 'POST', '/audio/speech', { body, option });
const binaryData = await this.helpers.prepareBinaryData(
response,
`audio.${response_format}`,
`audio/${response_format}`,
);
const binaryPropertyOutput = (options.binaryPropertyOutput as string) || 'data';
const newItem: INodeExecutionData = {
json: {
...binaryData,
data: undefined,
},
pairedItem: { item: i },
binary: {
[binaryPropertyOutput]: binaryData,
},
};
return [newItem];
}
@@ -0,0 +1,57 @@
import type { INodeProperties } from 'n8n-workflow';
import * as generate from './generate.operation';
import * as transcribe from './transcribe.operation';
import * as translate from './translate.operation';
export { generate, transcribe, translate };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Generate Audio',
value: 'generate',
action: 'Generate audio',
description: 'Creates audio from a text prompt',
},
{
name: 'Transcribe a Recording',
value: 'transcribe',
action: 'Transcribe a recording',
description: 'Transcribes audio into text',
},
{
name: 'Translate a Recording',
value: 'translate',
action: 'Translate a recording',
description: 'Translates audio into text in English',
},
],
default: 'generate',
displayOptions: {
show: {
resource: ['audio'],
},
},
},
{
displayName: 'OpenAI API limits the size of the audio file to 25 MB',
name: 'fileSizeLimitNotice',
type: 'notice',
default: ' ',
displayOptions: {
show: {
resource: ['audio'],
operation: ['translate', 'transcribe'],
},
},
},
...generate.description,
...transcribe.description,
...translate.description,
];
@@ -0,0 +1,96 @@
import FormData from 'form-data';
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import { getBinaryDataFile } from '../../../helpers/binary-data';
import { apiRequest } from '../../../transport';
const properties: INodeProperties[] = [
{
displayName: 'Input Data Field Name',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
placeholder: 'e.g. data',
hint: 'The name of the input field containing the binary file data to be processed',
description:
'Name of the binary property which contains the audio file in one of these formats: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm',
},
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
type: 'collection',
default: {},
options: [
{
displayName: 'Language of the Audio File',
name: 'language',
type: 'string',
description:
'The language of the input audio. Supplying the input language in <a href="https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes" target="_blank">ISO-639-1</a> format will improve accuracy and latency.',
default: '',
},
{
displayName: 'Output Randomness (Temperature)',
name: 'temperature',
type: 'number',
default: 0,
typeOptions: {
minValue: 0,
maxValue: 1,
numberPrecision: 1,
},
},
],
},
];
const displayOptions = {
show: {
operation: ['transcribe'],
resource: ['audio'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const model = 'whisper-1';
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
const options = this.getNodeParameter('options', i, {});
const formData = new FormData();
formData.append('model', model);
if (options.language) {
formData.append('language', options.language);
}
if (options.temperature) {
formData.append('temperature', options.temperature.toString());
}
const { filename, contentType, fileContent } = await getBinaryDataFile(
this,
i,
binaryPropertyName,
);
formData.append('file', fileContent, {
filename,
contentType,
});
const response = await apiRequest.call(this, 'POST', '/audio/transcriptions', {
option: { formData },
headers: formData.getHeaders(),
});
return [
{
json: response,
pairedItem: { item: i },
},
];
}
@@ -0,0 +1,84 @@
import FormData from 'form-data';
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import { getBinaryDataFile } from '../../../helpers/binary-data';
import { apiRequest } from '../../../transport';
const properties: INodeProperties[] = [
{
displayName: 'Input Data Field Name',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
hint: 'The name of the input field containing the binary file data to be processed',
placeholder: 'e.g. data',
description:
'Name of the binary property which contains the audio file in one of these formats: flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm',
},
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
type: 'collection',
default: {},
options: [
{
displayName: 'Output Randomness (Temperature)',
name: 'temperature',
type: 'number',
default: 0,
typeOptions: {
minValue: 0,
maxValue: 1,
numberPrecision: 1,
},
},
],
},
];
const displayOptions = {
show: {
operation: ['translate'],
resource: ['audio'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const model = 'whisper-1';
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
const options = this.getNodeParameter('options', i, {});
const formData = new FormData();
formData.append('model', model);
if (options.temperature) {
formData.append('temperature', options.temperature.toString());
}
const { filename, contentType, fileContent } = await getBinaryDataFile(
this,
i,
binaryPropertyName,
);
formData.append('file', fileContent, {
filename,
contentType,
});
const response = await apiRequest.call(this, 'POST', '/audio/translations', {
option: { formData },
headers: formData.getHeaders(),
});
return [
{
json: response,
pairedItem: { item: i },
},
];
}
@@ -0,0 +1,53 @@
import type { INodeProperties } from 'n8n-workflow';
export const modelRLC = (searchListMethod: string = 'modelSearch'): INodeProperties => ({
displayName: 'Model',
name: 'modelId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod,
searchable: true,
},
},
{
displayName: 'ID',
name: 'id',
type: 'string',
placeholder: 'e.g. gpt-4',
},
],
});
export const assistantRLC: INodeProperties = {
displayName: 'Assistant',
name: 'assistantId',
type: 'resourceLocator',
description:
'Assistant to respond to the message. You can add, modify or remove assistants in the <a href="https://platform.openai.com/playground?mode=assistant" target="_blank">playground</a>.',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'assistantSearch',
searchable: true,
},
},
{
displayName: 'ID',
name: 'id',
type: 'string',
placeholder: 'e.g. asst_abc123',
},
],
};
@@ -0,0 +1,62 @@
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import { apiRequest } from '../../../transport';
const properties: INodeProperties[] = [
{
displayName: 'File',
name: 'fileId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'From List',
name: 'list',
type: 'list',
typeOptions: {
searchListMethod: 'fileSearch',
searchable: true,
},
},
{
displayName: 'ID',
name: 'id',
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: 'file-[a-zA-Z0-9]',
errorMessage: 'Not a valid File ID',
},
},
],
placeholder: 'e.g. file-1234567890',
},
],
},
];
const displayOptions = {
show: {
operation: ['deleteFile'],
resource: ['file'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const fileId = this.getNodeParameter('fileId', i, '', { extractValue: true });
const response = await apiRequest.call(this, 'DELETE', `/files/${fileId}`);
return [
{
json: response,
pairedItem: { item: i },
},
];
}
@@ -0,0 +1,46 @@
import type { INodeProperties } from 'n8n-workflow';
import * as deleteFile from './deleteFile.operation';
import * as list from './list.operation';
import * as upload from './upload.operation';
export { upload, deleteFile, list };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Delete a File',
value: 'deleteFile',
action: 'Delete a file',
description: 'Delete a file from the server',
},
{
name: 'List Files',
value: 'list',
action: 'List files',
description: "Returns a list of files that belong to the user's organization",
},
{
name: 'Upload a File',
value: 'upload',
action: 'Upload a file',
description: 'Upload a file that can be used across various endpoints',
},
],
default: 'upload',
displayOptions: {
show: {
resource: ['file'],
},
},
},
...upload.description,
...deleteFile.description,
...list.description,
];
@@ -0,0 +1,67 @@
import type {
IDataObject,
INodeProperties,
IExecuteFunctions,
INodeExecutionData,
} from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import { apiRequest } from '../../../transport';
const properties: INodeProperties[] = [
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
type: 'collection',
default: {},
options: [
{
displayName: 'Purpose',
name: 'purpose',
type: 'options',
default: 'any',
description: 'Only return files with the given purpose',
options: [
{
name: 'Any [Default]',
value: 'any',
},
{
name: 'Assistants',
value: 'assistants',
},
{
name: 'Fine-Tune',
value: 'fine-tune',
},
],
},
],
},
];
const displayOptions = {
show: {
operation: ['list'],
resource: ['file'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const options = this.getNodeParameter('options', i, {});
const qs: IDataObject = {};
if (options.purpose && options.purpose !== 'any') {
qs.purpose = options.purpose as string;
}
const { data } = await apiRequest.call(this, 'GET', '/files', { qs });
return (data || []).map((file: IDataObject) => ({
json: file,
pairedItem: { item: i },
}));
}
@@ -0,0 +1,99 @@
import FormData from 'form-data';
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { updateDisplayOptions, NodeOperationError } from 'n8n-workflow';
import { getBinaryDataFile } from '../../../helpers/binary-data';
import { apiRequest } from '../../../transport';
const properties: INodeProperties[] = [
{
displayName: 'Input Data Field Name',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
hint: 'The name of the input field containing the binary file data to be processed',
placeholder: 'e.g. data',
description:
'Name of the binary property which contains the file. The size of individual files can be a maximum of 512 MB or 2 million tokens for Assistants.',
},
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
type: 'collection',
default: {},
options: [
{
displayName: 'Purpose',
name: 'purpose',
type: 'options',
default: 'assistants',
description:
"The intended purpose of the uploaded file, the 'Fine-tuning' only supports .jsonl files",
options: [
{
name: 'Assistants',
value: 'assistants',
},
{
name: 'Fine-Tune',
value: 'fine-tune',
},
],
},
],
},
];
const displayOptions = {
show: {
operation: ['upload'],
resource: ['file'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
const options = this.getNodeParameter('options', i, {});
const formData = new FormData();
formData.append('purpose', options.purpose || 'assistants');
const { filename, contentType, fileContent } = await getBinaryDataFile(
this,
i,
binaryPropertyName,
);
formData.append('file', fileContent, {
filename,
contentType,
});
try {
const response = await apiRequest.call(this, 'POST', '/files', {
option: { formData },
headers: formData.getHeaders(),
});
return [
{
json: response,
pairedItem: { item: i },
},
];
} catch (error) {
if (
error.message.includes('Bad request') &&
error.description?.includes('Expected file to have JSONL format')
) {
throw new NodeOperationError(this.getNode(), 'The file content is not in JSONL format', {
description:
'Fine-tuning accepts only files in JSONL format, where every line is a valid JSON dictionary',
});
}
throw error;
}
}
@@ -0,0 +1,221 @@
import type {
INodeProperties,
IExecuteFunctions,
INodeExecutionData,
IDataObject,
} from 'n8n-workflow';
import { updateDisplayOptions, NodeOperationError } from 'n8n-workflow';
import { apiRequest } from '../../../transport';
import { modelRLC } from '../descriptions';
const properties: INodeProperties[] = [
{
...modelRLC('imageModelSearch'),
displayOptions: { show: { '@version': [{ _cnd: { gte: 1.4 } }] } },
},
{
displayName: 'Text Input',
name: 'text',
type: 'string',
placeholder: "e.g. What's in this image?",
default: "What's in this image?",
typeOptions: {
rows: 2,
},
},
{
displayName: 'Input Type',
name: 'inputType',
type: 'options',
default: 'url',
options: [
{
name: 'Image URL(s)',
value: 'url',
},
{
name: 'Binary File(s)',
value: 'base64',
},
],
},
{
displayName: 'URL(s)',
name: 'imageUrls',
type: 'string',
placeholder: 'e.g. https://example.com/image.jpeg',
description: 'URL(s) of the image(s) to analyze, multiple URLs can be added separated by comma',
default: '',
displayOptions: {
show: {
inputType: ['url'],
},
},
},
{
displayName: 'Input Data Field Name',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
placeholder: 'e.g. data',
hint: 'The name of the input field containing the binary file data to be processed',
description: 'Name of the binary property which contains the image(s)',
displayOptions: {
show: {
inputType: ['base64'],
},
},
},
{
displayName: 'Simplify Output',
name: 'simplify',
type: 'boolean',
default: true,
description: 'Whether to simplify the response or not',
},
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
type: 'collection',
default: {},
options: [
{
displayName: 'Detail',
name: 'detail',
type: 'options',
default: 'auto',
options: [
{
name: 'Auto',
value: 'auto',
description:
'Model will look at the image input size and decide if it should use the low or high setting',
},
{
name: 'Low',
value: 'low',
description: 'Return faster responses and consume fewer tokens',
},
{
name: 'High',
value: 'high',
description: 'Return more detailed responses, consumes more tokens',
},
],
},
{
displayName: 'Length of Description (Max Tokens)',
description: 'Fewer tokens will result in shorter, less detailed image description',
name: 'maxTokens',
type: 'number',
default: 300,
typeOptions: {
minValue: 1,
},
},
],
},
];
const displayOptions = {
show: {
operation: ['analyze'],
resource: ['image'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
let model = 'gpt-4-vision-preview';
if (this.getNode().typeVersion >= 1.4) {
model = this.getNodeParameter('modelId', i, 'gpt-4o', { extractValue: true }) as string;
}
const text = this.getNodeParameter('text', i, '') as string;
const inputType = this.getNodeParameter('inputType', i) as string;
const options = this.getNodeParameter('options', i, {});
const content: IDataObject[] = [
{
type: 'text',
text,
},
];
const detail = (options.detail as string) || 'auto';
if (inputType === 'url') {
const imageUrls = (this.getNodeParameter('imageUrls', i) as string)
.split(',')
.map((url) => url.trim());
for (const url of imageUrls) {
content.push({
type: 'image_url',
image_url: {
url,
detail,
},
});
}
} else {
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i)
.split(',')
.map((propertyName) => propertyName.trim());
for (const propertyName of binaryPropertyName) {
const binaryData = this.helpers.assertBinaryData(i, propertyName);
let fileBase64;
if (binaryData.id) {
const chunkSize = 256 * 1024;
const stream = await this.helpers.getBinaryStream(binaryData.id, chunkSize);
const buffer = await this.helpers.binaryToBuffer(stream);
fileBase64 = buffer.toString('base64');
} else {
fileBase64 = binaryData.data;
}
if (!binaryData) {
throw new NodeOperationError(this.getNode(), 'No binary data exists on item!');
}
content.push({
type: 'image_url',
image_url: {
url: `data:${binaryData.mimeType};base64,${fileBase64}`,
detail,
},
});
}
}
const body = {
model,
messages: [
{
role: 'user',
content,
},
],
max_tokens: (options.maxTokens as number) || 300,
};
let response = await apiRequest.call(this, 'POST', '/chat/completions', { body });
const simplify = this.getNodeParameter('simplify', i) as boolean;
if (simplify && response.choices) {
response = { content: response.choices[0].message.content };
}
return [
{
json: response,
pairedItem: { item: i },
},
];
}
@@ -0,0 +1,311 @@
import type {
INodeProperties,
IExecuteFunctions,
INodeExecutionData,
IDataObject,
} from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import { apiRequest } from '../../../transport';
const properties: INodeProperties[] = [
{
displayName: 'Model',
name: 'model',
type: 'options',
default: 'dall-e-3',
description: 'The model to use for image generation',
options: [
{
name: 'DALL·E 2',
value: 'dall-e-2',
},
{
name: 'DALL·E 3',
value: 'dall-e-3',
},
{
name: 'GPT Image 1',
value: 'gpt-image-1',
},
],
},
{
displayName: 'Prompt',
name: 'prompt',
type: 'string',
placeholder: 'e.g. A cute cat eating a dinosaur',
description:
'A text description of the desired image(s). The maximum length is 1000 characters for dall-e-2 and 4000 characters for dall-e-3.',
default: '',
typeOptions: {
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],
},
},
},
],
},
];
const displayOptions = {
show: {
operation: ['generate'],
resource: ['image'],
},
};
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 prompt = this.getNodeParameter('prompt', i) as string;
const options = this.getNodeParameter('options', i, {});
let response_format = 'b64_json';
let binaryPropertyOutput = 'data';
if (options.returnImageUrls) {
response_format = 'url';
}
if (options.binaryPropertyOutput) {
binaryPropertyOutput = options.binaryPropertyOutput as string;
delete options.binaryPropertyOutput;
}
if (options.dalleQuality) {
options.quality = options.dalleQuality;
delete options.dalleQuality;
}
delete options.returnImageUrls;
const body: IDataObject = {
prompt,
model,
response_format: model !== 'gpt-image-1' ? response_format : undefined, // gpt-image-1 does not support response_format
...options,
};
const { data } = await apiRequest.call(this, 'POST', '/images/generations', { body });
if (response_format === 'url') {
return ((data as IDataObject[]) || []).map((entry) => ({
json: entry,
pairedItem: { item: i },
}));
} else {
const returnData: INodeExecutionData[] = [];
for (const entry of data) {
const binaryData = await this.helpers.prepareBinaryData(
Buffer.from(entry.b64_json as string, 'base64'),
'data',
);
returnData.push({
json: Object.assign({}, binaryData, {
data: undefined,
}),
binary: {
[binaryPropertyOutput]: binaryData,
},
pairedItem: { item: i },
});
}
return returnData;
}
}
@@ -0,0 +1,37 @@
import type { INodeProperties } from 'n8n-workflow';
import * as analyze from './analyze.operation';
import * as generate from './generate.operation';
export { generate, analyze };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Analyze Image',
value: 'analyze',
action: 'Analyze image',
description: 'Take in images and answer questions about them',
},
{
name: 'Generate an Image',
value: 'generate',
action: 'Generate an image',
description: 'Creates an image from a text prompt',
},
],
default: 'generate',
displayOptions: {
show: {
resource: ['image'],
},
},
},
...generate.description,
...analyze.description,
];
@@ -0,0 +1,11 @@
import type { AllEntities } from 'n8n-workflow';
type NodeMap = {
assistant: 'message' | 'create' | 'deleteAssistant' | 'list' | 'update';
audio: 'generate' | 'transcribe' | 'translate';
file: 'upload' | 'deleteFile' | 'list';
image: 'generate' | 'analyze';
text: 'message' | 'classify';
};
export type OpenAiType = AllEntities<NodeMap>;
@@ -0,0 +1,42 @@
import { mockDeep } from 'jest-mock-extended';
import type { IExecuteFunctions, INode } from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import * as audio from './audio';
import { router } from './router';
describe('OpenAI router', () => {
const mockExecuteFunctions = mockDeep<IExecuteFunctions>();
const mockAudio = jest.spyOn(audio.transcribe, 'execute');
beforeEach(() => {
jest.clearAllMocks();
});
it('should handle NodeApiError undefined error chaining', async () => {
const errorNode: INode = {
id: 'error-node-id',
name: 'ErrorNode',
type: 'test.error',
typeVersion: 1,
position: [100, 200],
parameters: {},
};
const nodeApiError = new NodeApiError(
errorNode,
{ message: 'API error occurred', error: { error: { message: 'Rate limit exceeded' } } },
{ itemIndex: 0 },
);
mockExecuteFunctions.getNodeParameter.mockImplementation((parameter) =>
parameter === 'resource' ? 'audio' : 'transcribe',
);
mockExecuteFunctions.getInputData.mockReturnValue([{ json: {} }]);
mockExecuteFunctions.getNode.mockReturnValue(errorNode);
mockExecuteFunctions.continueOnFail.mockReturnValue(false);
mockAudio.mockRejectedValue(nodeApiError);
await expect(router.call(mockExecuteFunctions)).rejects.toThrow(NodeApiError);
});
});
@@ -0,0 +1,88 @@
import {
NodeOperationError,
type IExecuteFunctions,
type INodeExecutionData,
NodeApiError,
} from 'n8n-workflow';
import * as assistant from './assistant';
import * as audio from './audio';
import * as file from './file';
import * as image from './image';
import type { OpenAiType } from './node.type';
import * as text from './text';
import { getCustomErrorMessage } from '../../helpers/error-handling';
export async function router(this: IExecuteFunctions) {
const returnData: INodeExecutionData[] = [];
const items = this.getInputData();
const resource = this.getNodeParameter<OpenAiType>('resource', 0);
const operation = this.getNodeParameter('operation', 0);
const openAiTypeData = {
resource,
operation,
} as OpenAiType;
let execute;
switch (openAiTypeData.resource) {
case 'assistant':
execute = assistant[openAiTypeData.operation].execute;
break;
case 'audio':
execute = audio[openAiTypeData.operation].execute;
break;
case 'file':
execute = file[openAiTypeData.operation].execute;
break;
case 'image':
execute = image[openAiTypeData.operation].execute;
break;
case 'text':
execute = text[openAiTypeData.operation].execute;
break;
default:
throw new NodeOperationError(
this.getNode(),
`The operation "${operation}" is not supported!`,
);
}
for (let i = 0; i < items.length; i++) {
try {
const responseData = await execute.call(this, i);
returnData.push(...responseData);
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ json: { error: error.message }, pairedItem: { item: i } });
continue;
}
if (error instanceof NodeApiError) {
// If the error is a rate limit error, we want to handle it differently
const errorCode: string | undefined = (error.cause as any)?.error?.error?.code;
if (errorCode) {
const customErrorMessage = getCustomErrorMessage(errorCode);
if (customErrorMessage) {
error.message = customErrorMessage;
}
}
error.context = {
itemIndex: i,
};
throw error;
}
throw new NodeOperationError(this.getNode(), error, {
itemIndex: i,
description: error.description,
});
}
}
return [returnData];
}
@@ -0,0 +1,84 @@
import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { updateDisplayOptions } from 'n8n-workflow';
import { apiRequest } from '../../../transport';
const properties: INodeProperties[] = [
{
displayName: 'Text Input',
name: 'input',
type: 'string',
placeholder: 'e.g. Sample text goes here',
description: 'The input text to classify if it is violates the moderation policy',
default: '',
typeOptions: {
rows: 2,
},
},
{
displayName: 'Simplify Output',
name: 'simplify',
type: 'boolean',
default: false,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
type: 'collection',
default: {},
options: [
{
displayName: 'Use Stable Model',
name: 'useStableModel',
type: 'boolean',
default: false,
description:
'Whether to use the stable version of the model instead of the latest version, accuracy may be slightly lower',
},
],
},
];
const displayOptions = {
show: {
operation: ['classify'],
resource: ['text'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
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 body = {
input,
model,
};
const { results } = await apiRequest.call(this, 'POST', '/moderations', { body });
if (!results) return [];
const simplify = this.getNodeParameter('simplify', i) as boolean;
if (simplify && results) {
return [
{
json: { flagged: results[0].flagged },
pairedItem: { item: i },
},
];
} else {
return [
{
json: results[0],
pairedItem: { item: i },
},
];
}
}
@@ -0,0 +1,39 @@
import type { INodeProperties } from 'n8n-workflow';
import * as classify from './classify.operation';
import * as message from './message.operation';
export { classify, message };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Message a Model',
value: 'message',
action: 'Message a model',
// eslint-disable-next-line n8n-nodes-base/node-param-description-excess-final-period
description: 'Create a completion with GPT 3, 4, etc.',
},
{
name: 'Classify Text for Violations',
value: 'classify',
action: 'Classify text for violations',
description: 'Check whether content complies with usage policies',
},
],
default: 'message',
displayOptions: {
show: {
resource: ['text'],
},
},
},
...classify.description,
...message.description,
];
@@ -0,0 +1,366 @@
import type { Tool } from '@langchain/core/tools';
import _omit from 'lodash/omit';
import type {
INodeProperties,
IExecuteFunctions,
INodeExecutionData,
IDataObject,
} from 'n8n-workflow';
import { jsonParse, updateDisplayOptions } from 'n8n-workflow';
import { getConnectedTools } from '@utils/helpers';
import { MODELS_NOT_SUPPORT_FUNCTION_CALLS } from '../../../helpers/constants';
import type { ChatCompletion } from '../../../helpers/interfaces';
import { formatToOpenAIAssistantTool } from '../../../helpers/utils';
import { apiRequest } from '../../../transport';
import { modelRLC } from '../descriptions';
const properties: INodeProperties[] = [
modelRLC('modelSearch'),
{
displayName: 'Messages',
name: 'messages',
type: 'fixedCollection',
typeOptions: {
sortable: true,
multipleValues: true,
},
placeholder: 'Add Message',
default: { values: [{ content: '' }] },
options: [
{
displayName: 'Values',
name: 'values',
values: [
{
displayName: 'Prompt',
name: 'content',
type: 'string',
description: 'The content of the message to be send',
default: '',
placeholder: 'e.g. Hello, how can you help me?',
typeOptions: {
rows: 2,
},
},
{
displayName: 'Role',
name: 'role',
type: 'options',
description:
"Role in shaping the model's response, it tells the model how it should behave and interact with the user",
options: [
{
name: 'User',
value: 'user',
description: 'Send a message as a user and get a response from the model',
},
{
name: 'Assistant',
value: 'assistant',
description: 'Tell the model to adopt a specific tone or personality',
},
{
name: 'System',
value: 'system',
description:
"Usually used to set the model's behavior or context for the next user message",
},
],
default: 'user',
},
],
},
],
},
{
displayName: 'Simplify Output',
name: 'simplify',
type: 'boolean',
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
{
displayName: 'Output Content as JSON',
name: 'jsonOutput',
type: 'boolean',
description:
'Whether to attempt to return the response in JSON format. Compatible with GPT-4 Turbo and all GPT-3.5 Turbo models newer than gpt-3.5-turbo-1106.',
default: false,
},
{
displayName: 'Hide Tools',
name: 'hideTools',
type: 'hidden',
default: 'hide',
displayOptions: {
show: {
modelId: MODELS_NOT_SUPPORT_FUNCTION_CALLS,
'@version': [{ _cnd: { gte: 1.2 } }],
},
},
},
{
displayName: 'Connect your own custom n8n tools to this node on the canvas',
name: 'noticeTools',
type: 'notice',
default: '',
displayOptions: {
hide: {
hideTools: ['hide'],
},
},
},
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
type: 'collection',
default: {},
options: [
{
displayName: 'Frequency Penalty',
name: 'frequency_penalty',
default: 0,
typeOptions: { maxValue: 2, minValue: -2, numberPrecision: 1 },
description:
"Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim",
type: 'number',
},
{
displayName: 'Maximum Number of Tokens',
name: 'maxTokens',
default: 16,
description:
'The maximum number of tokens to generate in the completion. Most models have a context length of 2048 tokens (except for the newest models, which support 32,768).',
type: 'number',
typeOptions: {
maxValue: 32768,
},
},
{
displayName: 'Number of Completions',
name: 'n',
default: 1,
description:
'How many completions to generate for each prompt. Note: Because this parameter generates many completions, it can quickly consume your token quota. Use carefully and ensure that you have reasonable settings for max_tokens and stop.',
type: 'number',
},
{
displayName: 'Presence Penalty',
name: 'presence_penalty',
default: 0,
typeOptions: { maxValue: 2, minValue: -2, numberPrecision: 1 },
description:
"Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics",
type: 'number',
},
{
displayName: 'Output Randomness (Temperature)',
name: 'temperature',
default: 1,
typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 },
description:
'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive. We generally recommend altering this or temperature but not both.',
type: 'number',
},
{
displayName: 'Output Randomness (Top P)',
name: 'topP',
default: 1,
typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 },
description:
'An alternative to sampling with temperature, controls diversity via nucleus sampling: 0.5 means half of all likelihood-weighted options are considered. We generally recommend altering this or temperature but not both.',
type: 'number',
},
{
displayName: 'Reasoning Effort',
name: 'reasoning_effort',
default: 'medium',
description:
'Controls the amount of reasoning tokens to use. A value of "low" will favor speed and economical token usage, "high" will favor more complete reasoning at the cost of more tokens generated and slower responses.',
type: 'options',
options: [
{
name: 'Low',
value: 'low',
description: 'Favors speed and economical token usage',
},
{
name: 'Medium',
value: 'medium',
description: 'Balance between speed and reasoning accuracy',
},
{
name: 'High',
value: 'high',
description:
'Favors more complete reasoning at the cost of more tokens generated and slower responses',
},
],
displayOptions: {
show: {
// reasoning_effort is only available on o1, o1-versioned, or on o3-mini and beyond, and gpt-5 models. Not on o1-mini or other GPT-models.
'/modelId': [{ _cnd: { regex: '(^o1([-\\d]+)?$)|(^o[3-9].*)|(^gpt-5.*)' } }],
},
},
},
{
displayName: 'Max Tool Calls Iterations',
name: 'maxToolsIterations',
type: 'number',
default: 15,
description:
'The maximum number of tool iteration cycles the LLM will run before stopping. A single iteration can contain multiple tool calls. Set to 0 for no limit.',
displayOptions: {
show: {
'@version': [{ _cnd: { gte: 1.5 } }],
},
},
},
],
},
];
const displayOptions = {
show: {
operation: ['message'],
resource: ['text'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const nodeVersion = this.getNode().typeVersion;
const model = this.getNodeParameter('modelId', i, '', { extractValue: true });
let messages = this.getNodeParameter('messages.values', i, []) as IDataObject[];
const options = this.getNodeParameter('options', i, {});
const jsonOutput = this.getNodeParameter('jsonOutput', i, false) as boolean;
const maxToolsIterations =
nodeVersion >= 1.5 ? (this.getNodeParameter('options.maxToolsIterations', i, 15) as number) : 0;
const abortSignal = this.getExecutionCancelSignal();
if (options.maxTokens !== undefined) {
options.max_completion_tokens = options.maxTokens;
delete options.maxTokens;
}
if (options.topP !== undefined) {
options.top_p = options.topP;
delete options.topP;
}
let response_format;
if (jsonOutput) {
response_format = { type: 'json_object' };
messages = [
{
role: 'system',
content: 'You are a helpful assistant designed to output JSON.',
},
...messages,
];
}
const hideTools = this.getNodeParameter('hideTools', i, '') as string;
let tools;
let externalTools: Tool[] = [];
if (hideTools !== 'hide') {
const enforceUniqueNames = nodeVersion > 1;
externalTools = await getConnectedTools(this, enforceUniqueNames, false);
}
if (externalTools.length) {
tools = externalTools.length ? externalTools?.map(formatToOpenAIAssistantTool) : undefined;
}
const body: IDataObject = {
model,
messages,
tools,
response_format,
..._omit(options, ['maxToolsIterations']),
};
let response = (await apiRequest.call(this, 'POST', '/chat/completions', {
body,
})) as ChatCompletion;
if (!response) return [];
let currentIteration = 1;
let toolCalls = response?.choices[0]?.message?.tool_calls;
while (toolCalls?.length) {
// Break the loop if the max iterations is reached or the execution is canceled
if (
abortSignal?.aborted ||
(maxToolsIterations > 0 && currentIteration >= maxToolsIterations)
) {
break;
}
messages.push(response.choices[0].message);
for (const toolCall of toolCalls) {
const functionName = toolCall.function.name;
const functionArgs = toolCall.function.arguments;
let functionResponse;
for (const tool of externalTools ?? []) {
if (tool.name === functionName) {
const parsedArgs: { input: string } = jsonParse(functionArgs);
const functionInput = parsedArgs.input ?? parsedArgs ?? functionArgs;
functionResponse = await tool.invoke(functionInput);
}
}
if (typeof functionResponse === 'object') {
functionResponse = JSON.stringify(functionResponse);
}
messages.push({
tool_call_id: toolCall.id,
role: 'tool',
content: functionResponse,
});
}
response = (await apiRequest.call(this, 'POST', '/chat/completions', {
body,
})) as ChatCompletion;
toolCalls = response.choices[0].message.tool_calls;
currentIteration += 1;
}
if (response_format) {
response.choices = response.choices.map((choice) => {
try {
choice.message.content = JSON.parse(choice.message.content);
} catch (error) {}
return choice;
});
}
const simplify = this.getNodeParameter('simplify', i) as boolean;
const returnData: INodeExecutionData[] = [];
if (simplify) {
for (const entry of response.choices) {
returnData.push({
json: entry,
pairedItem: { item: i },
});
}
} else {
returnData.push({ json: response, pairedItem: { item: i } });
}
return returnData;
}