n8n-nodes-teamorouter/nodes/Teamorouter/Teamorouter.node.ts
ed0ss 11619aaa7b Send and return images in the TeamoRouter action node
The action node now speaks the whole TeamoRouter surface instead of a
single prompt: chat messages carry text plus input images (binary or URL)
and can hand the answer to a Chat Trigger as an output field, images can
be generated or edited with the full parameter set (size, quality, n,
output format and compression, background, fidelity, mask) and every
result comes back as binary, as markdown for the chat, or both.
2026-10-22 16:40:04 +03:00

948 lines
29 KiB
TypeScript

import type {
IBinaryData,
IDataObject,
IExecuteFunctions,
IHttpRequestOptions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { modelLoader } from '../shared/loadModels';
import {
errorMessage,
hintForError,
normalizeBaseUrl,
parseExtraBody,
type TeamorouterModel,
} from '../shared/teamorouter';
interface ChatContentPart extends IDataObject {
type?: string;
text?: string;
image_url?: { url?: string };
}
interface ChatCompletionResponse extends IDataObject {
model?: string;
choices?: Array<{
finish_reason?: string;
message?: {
content?: string | null | ChatContentPart[];
images?: Array<{ image_url?: { url?: string }; url?: string }>;
};
}>;
usage?: IDataObject;
}
interface ImageResponse extends IDataObject {
data?: Array<{ b64_json?: string; url?: string }>;
}
interface ChatOptions {
systemPrompt?: string;
temperature?: number;
maxTokens?: number;
topP?: number;
forceJsonResponse?: boolean;
serviceTier?: 'auto' | 'fast';
extraBody?: unknown;
timeout?: number;
}
interface ImageOptions {
size?: string;
quality?: string;
numberOfImages?: number;
outputFormat?: string;
outputCompression?: number;
background?: string;
moderation?: string;
inputFidelity?: string;
timeout?: number;
}
interface InputImage {
source?: string;
binaryProperty?: string;
url?: string;
}
interface BinaryPart {
buffer: Buffer;
fileName: string;
mimeType: string;
}
interface MultipartPart {
name: string;
value?: string;
buffer?: Buffer;
fileName?: string;
mimeType?: string;
}
const MIME_BY_FORMAT: Record<string, string> = {
png: 'image/png',
jpeg: 'image/jpeg',
webp: 'image/webp',
};
const EXTENSION_BY_FORMAT: Record<string, string> = { png: 'png', jpeg: 'jpeg', webp: 'webp' };
/** Reads the binary property behind an input item and turns it into a data URL. */
async function binaryDataUrl(
context: IExecuteFunctions,
itemIndex: number,
propertyName: string,
): Promise<string> {
const binary = await readBinary(context, itemIndex, propertyName);
return `data:${binary.mimeType};base64,${binary.buffer.toString('base64')}`;
}
async function readBinary(
context: IExecuteFunctions,
itemIndex: number,
propertyName: string,
): Promise<BinaryPart> {
const metadata = context.getInputData()[itemIndex]?.binary?.[propertyName];
if (metadata === undefined) {
throw new NodeOperationError(
context.getNode(),
`No binary data on the input item field "${propertyName}"`,
{ itemIndex },
);
}
return {
buffer: await context.helpers.getBinaryDataBuffer(itemIndex, propertyName),
fileName: metadata.fileName ?? `${propertyName}.png`,
mimeType: metadata.mimeType ?? 'image/png',
};
}
/**
* Hand-rolled multipart body: n8n's httpRequest wants the `form-data` package, which a community
* node cannot rely on, and /v1/images/edits is documented with `-F image=@file`.
*/
function buildMultipart(parts: MultipartPart[]): { body: Buffer; contentType: string } {
const boundary = `----n8nTeamorouter${Date.now().toString(16)}${Math.random().toString(16).slice(2)}`;
const chunks: Buffer[] = [];
for (const part of parts) {
let header = `--${boundary}\r\nContent-Disposition: form-data; name="${part.name}"`;
if (part.fileName !== undefined) header += `; filename="${part.fileName}"`;
header += '\r\n';
if (part.mimeType !== undefined) header += `Content-Type: ${part.mimeType}\r\n`;
chunks.push(
Buffer.from(`${header}\r\n`, 'utf8'),
part.buffer ?? Buffer.from(part.value ?? '', 'utf8'),
Buffer.from('\r\n', 'utf8'),
);
}
chunks.push(Buffer.from(`--${boundary}--\r\n`, 'utf8'));
return { body: Buffer.concat(chunks), contentType: `multipart/form-data; boundary=${boundary}` };
}
function splitDataUrl(url: string): { mimeType: string; base64: string } | undefined {
const match = /^data:([^;,]+);base64,(.*)$/s.exec(url);
return match === null ? undefined : { mimeType: match[1], base64: match[2] };
}
/** Adds TeamoRouter specific hints to failed requests and keeps the original error otherwise. */
async function callApi<T>(context: IExecuteFunctions, requestOptions: IHttpRequestOptions): Promise<T> {
try {
return (await context.helpers.httpRequestWithAuthentication.call(
context,
'teamorouterApi',
requestOptions,
)) as T;
} catch (error) {
const hint = hintForError(error);
if (hint === undefined) throw error;
throw new NodeOperationError(context.getNode(), `${hint} ${errorMessage(error)}`);
}
}
/** Text plus optional images, in the shape every OpenAI-compatible vision model accepts. */
async function buildUserContent(
context: IExecuteFunctions,
itemIndex: number,
prompt: string,
images: InputImage[],
): Promise<string | IDataObject[]> {
if (images.length === 0) return prompt;
const content: IDataObject[] = [];
if (prompt !== '') content.push({ type: 'text', text: prompt });
for (const image of images) {
const url =
image.source === 'url'
? (image.url ?? '').trim()
: await binaryDataUrl(context, itemIndex, image.binaryProperty ?? 'data');
if (url === '') {
throw new NodeOperationError(context.getNode(), 'An input image has no URL', { itemIndex });
}
content.push({ type: 'image_url', image_url: { url } });
}
return content;
}
function buildChatBody(
context: IExecuteFunctions,
model: string,
content: string | IDataObject[],
options: ChatOptions,
): IDataObject {
const messages: IDataObject[] = [];
if (typeof options.systemPrompt === 'string' && options.systemPrompt !== '') {
messages.push({ role: 'system', content: options.systemPrompt });
}
messages.push({ role: 'user', content });
const body: IDataObject = { model, messages };
if (typeof options.temperature === 'number') body.temperature = options.temperature;
if (typeof options.topP === 'number') body.top_p = options.topP;
if (typeof options.maxTokens === 'number' && options.maxTokens > 0) body.max_tokens = options.maxTokens;
if (options.forceJsonResponse === true) body.response_format = { type: 'json_object' };
if (options.serviceTier === 'fast') body.service_tier = 'fast';
return Object.assign(body, parseExtraBody(options.extraBody, context.getNode()));
}
/** The image parameters shared by /images/generations and /images/edits. */
function buildImageBody(model: string, prompt: string, options: ImageOptions): IDataObject {
const body: IDataObject = { model, prompt };
if (options.size !== undefined && options.size !== '') body.size = options.size;
if (options.quality !== undefined && options.quality !== '') body.quality = options.quality;
if (typeof options.numberOfImages === 'number' && options.numberOfImages > 1) {
body.n = options.numberOfImages;
}
if (options.outputFormat !== undefined && options.outputFormat !== '') {
body.output_format = options.outputFormat;
}
if (typeof options.outputCompression === 'number' && options.outputCompression >= 0) {
body.output_compression = options.outputCompression;
}
if (options.background !== undefined && options.background !== '') body.background = options.background;
if (options.moderation !== undefined && options.moderation !== '') body.moderation = options.moderation;
if (options.inputFidelity !== undefined && options.inputFidelity !== '') {
body.input_fidelity = options.inputFidelity;
}
return body;
}
function extractChatText(response: ChatCompletionResponse): string {
const content = response.choices?.[0]?.message?.content;
if (typeof content === 'string') return content;
if (Array.isArray(content)) {
return content
.filter((part) => typeof part?.text === 'string')
.map((part) => part.text ?? '')
.join('');
}
return '';
}
/** Image URLs a multimodal chat model may answer with, in either common response shape. */
function extractChatImages(response: ChatCompletionResponse): string[] {
const message = response.choices?.[0]?.message;
const urls: string[] = [];
for (const image of message?.images ?? []) {
const url = image?.image_url?.url ?? image?.url;
if (typeof url === 'string' && url !== '') urls.push(url);
}
if (Array.isArray(message?.content)) {
for (const part of message.content) {
const url = part?.image_url?.url;
if (typeof url === 'string' && url !== '') urls.push(url);
}
}
return urls;
}
export class Teamorouter implements INodeType {
description: INodeTypeDescription = {
displayName: 'TeamoRouter',
name: 'teamorouter',
icon: 'file:teamorouter.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + " " + $parameter["resource"]}}',
description:
'Call the TeamoRouter gateway: chat completions with text and images, image generation and editing, model list (GPT, Claude, Gemini, DeepSeek, GLM, Grok, Kimi)',
defaults: {
name: 'TeamoRouter',
},
usableAsTool: true,
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'teamorouterApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Chat',
value: 'chat',
description: 'Send text and images to a chat model and get its answer',
},
{
name: 'Image',
value: 'image',
description: 'Generate an image from a prompt, or edit an existing image',
},
{
name: 'Model',
value: 'model',
description: 'Read the model list from the router',
},
],
default: 'chat',
},
// ---------------------------------------------------------------- chat
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: { show: { resource: ['chat'] } },
options: [
{
name: 'Message a Model',
value: 'message',
description: 'Send a prompt and get the completion',
action: 'Send a chat completion request',
},
],
default: 'message',
},
{
displayName: 'Model',
name: 'model',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getChatModels',
},
default: '',
required: true,
displayOptions: { show: { resource: ['chat'] } },
description:
'Model ID to use. The list is read live from GET /v1/models. For an ID that is missing from the list, switch this field to expression mode and enter the ID.',
},
{
displayName: 'Prompt',
name: 'prompt',
type: 'string',
default: '',
typeOptions: { rows: 4 },
displayOptions: { show: { resource: ['chat'] } },
description: 'Message to send to the model. Optional when input images are attached.',
},
{
displayName: 'Input Images',
name: 'images',
type: 'fixedCollection',
typeOptions: { multipleValues: true },
placeholder: 'Add Image',
default: {},
displayOptions: { show: { resource: ['chat'] } },
description:
'Images to send along with the prompt. Needs a vision model, e.g. GPT, Claude or Gemini.',
options: [
{
name: 'image',
displayName: 'Image',
values: [
{
displayName: 'Source',
name: 'source',
type: 'options',
default: 'binary',
options: [
{
name: 'Binary File',
value: 'binary',
description: 'An image coming from an earlier node in the workflow',
},
{
name: 'Image URL',
value: 'url',
description: 'A link to the image, or a data:image/... URL',
},
],
},
{
displayName: 'Binary Property',
name: 'binaryProperty',
type: 'string',
default: 'data',
displayOptions: { show: { source: ['binary'] } },
description: 'Name of the binary property that holds the image',
},
{
displayName: 'Image URL',
name: 'url',
type: 'string',
default: '',
placeholder: 'https://example.com/cat.png',
displayOptions: { show: { source: ['url'] } },
description: 'Publicly reachable image link, or a data:image/...;base64,... URL',
},
],
},
],
},
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
type: 'collection',
default: {},
displayOptions: { show: { resource: ['chat'] } },
options: [
{
displayName: 'Extra Body',
name: 'extraBody',
type: 'json',
default: '',
description:
'Extra JSON properties to merge into the request body, e.g. {"top_k": 40}. Overrides the fields set by the options above.',
},
{
displayName: 'Force JSON Response',
name: 'forceJsonResponse',
type: 'boolean',
default: false,
description:
'Whether to request a JSON object from the model. The prompt must mention JSON.',
},
{
displayName: 'Maximum Number of Tokens',
name: 'maxTokens',
type: 'number',
default: -1,
description: 'Maximum number of tokens to generate. -1 leaves it to the model.',
},
{
displayName: 'Sampling Temperature',
name: 'temperature',
type: 'number',
default: 0.7,
typeOptions: { maxValue: 2, minValue: 0, numberPrecision: 1 },
description:
'Controls randomness: lower values make the output more deterministic and repetitive',
},
{
displayName: 'Service Tier',
name: 'serviceTier',
type: 'options',
default: 'auto',
options: [
{ name: 'Auto', value: 'auto', description: 'Standard processing' },
{
name: 'Fast',
value: 'fast',
description: 'TeamoRouter Fast mode: up to 2.5x faster, billed at 2x the standard rate',
},
],
description: 'Processing mode requested from TeamoRouter',
},
{
displayName: 'System Prompt',
name: 'systemPrompt',
type: 'string',
default: '',
typeOptions: { rows: 3 },
description: 'Optional instruction that sets the behaviour of the assistant',
},
{
displayName: 'Timeout',
name: 'timeout',
type: 'number',
default: 600000,
description:
'Time to wait for an answer in milliseconds. TeamoRouter allows responses up to 600 seconds.',
},
{
displayName: 'Top P',
name: 'topP',
type: 'number',
default: 1,
typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 2 },
description:
'Controls diversity via nucleus sampling. Change this or the temperature, not both.',
},
],
},
// --------------------------------------------------------------- image
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: { show: { resource: ['image'] } },
options: [
{
name: 'Generate an Image',
value: 'generate',
description: 'Create an image from a text prompt',
action: 'Generate an image',
},
{
name: 'Edit an Image',
value: 'edit',
description: 'Change an existing image with a text instruction',
action: 'Edit an image',
},
],
default: 'generate',
},
{
displayName: 'Model',
name: 'imageModel',
type: 'options',
typeOptions: {
loadOptionsMethod: 'getImageModels',
},
default: 'gpt-image-2',
required: true,
displayOptions: { show: { resource: ['image'] } },
description:
'Image model ID. The list is read live from GET /v1/models. For an ID that is missing from the list, switch this field to expression mode and enter the ID.',
},
{
displayName: 'Prompt',
name: 'imagePrompt',
type: 'string',
default: '',
required: true,
typeOptions: { rows: 4 },
displayOptions: { show: { resource: ['image'] } },
description: 'Description of the image to generate, or of the change to apply when editing',
},
{
displayName: 'Input Image (Binary Property)',
name: 'inputBinaryProperty',
type: 'string',
default: 'data',
required: true,
displayOptions: { show: { resource: ['image'], operation: ['edit'] } },
description: 'Name of the binary property that holds the image to edit',
},
{
displayName: 'Mask (Binary Property)',
name: 'maskBinaryProperty',
type: 'string',
default: '',
displayOptions: { show: { resource: ['image'], operation: ['edit'] } },
description:
'Optional binary property with a mask: transparent pixels mark the area to edit',
},
{
displayName: 'Put Output File in Field',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: { show: { resource: ['image'] } },
description:
'Name of the binary property that holds the generated image. With several images, the next ones are suffixed with a number.',
},
{
displayName: 'Options',
name: 'options',
placeholder: 'Add Option',
type: 'collection',
default: {},
displayOptions: { show: { resource: ['image'] } },
options: [
{
displayName: 'Background',
name: 'background',
type: 'options',
default: 'auto',
options: [
{ name: 'Auto', value: 'auto', description: 'Let the model decide' },
{
name: 'Opaque',
value: 'opaque',
description: 'Solid background, supported by every output format',
},
{
name: 'Transparent',
value: 'transparent',
description: 'Needs the PNG or WebP output format',
},
],
description: 'Transparency of the generated image',
},
{
displayName: 'Input Fidelity',
name: 'inputFidelity',
type: 'options',
default: 'high',
displayOptions: { show: { operation: ['edit'] } },
options: [
{
name: 'High',
value: 'high',
description: 'Keep the details of the source image, e.g. faces',
},
{
name: 'Low',
value: 'low',
description: 'Allow the model to reinterpret the source image more freely',
},
],
description: 'How closely the edit follows the source image',
},
{
displayName: 'Moderation',
name: 'moderation',
type: 'options',
default: 'auto',
displayOptions: { show: { operation: ['generate'] } },
options: [
{ name: 'Auto', value: 'auto', description: 'Standard content filter' },
{ name: 'Low', value: 'low', description: 'Relaxed content filter' },
],
description: 'Content filter applied by TeamoRouter',
},
{
displayName: 'Number of Images',
name: 'numberOfImages',
type: 'number',
default: 1,
typeOptions: { minValue: 1 },
description: 'How many images to generate from this prompt',
},
{
displayName: 'Output Compression',
name: 'outputCompression',
type: 'number',
default: -1,
typeOptions: { minValue: -1, maxValue: 100 },
description:
'Compression level 0-100 for JPEG and WebP. -1 leaves the default.',
},
{
displayName: 'Output Format',
name: 'outputFormat',
type: 'options',
default: 'png',
options: [
{ name: 'PNG', value: 'png', description: 'Lossless, supports transparency' },
{ name: 'JPEG', value: 'jpeg', description: 'Smaller files, no transparency' },
{ name: 'WebP', value: 'webp', description: 'Small files, supports transparency' },
],
description: 'File format the router returns',
},
{
displayName: 'Quality',
name: 'quality',
type: 'options',
default: 'auto',
options: [
{ name: 'Auto', value: 'auto' },
{ name: 'Low', value: 'low', description: 'Fastest and cheapest' },
{ name: 'Medium', value: 'medium' },
{ name: 'High', value: 'high' },
],
description: 'Rendering effort spent on the image',
},
{
displayName: 'Size',
name: 'size',
type: 'options',
default: '1024x1024',
options: [
{ name: 'Auto', value: 'auto', description: 'Let the model pick a size' },
{ name: '1024x1024 (Square)', value: '1024x1024' },
{ name: '1536x1024 (Landscape)', value: '1536x1024' },
{ name: '1024x1536 (Portrait)', value: '1024x1536' },
{ name: '2048x2048', value: '2048x2048' },
],
description:
'Image size. Other values are allowed through an expression: both sides must be multiples of 16, the longest side at most 3840 and the aspect ratio at most 3:1.',
},
{
displayName: 'Timeout',
name: 'timeout',
type: 'number',
default: 300000,
description:
'Time to wait for the image in milliseconds. Generation usually takes 20-40 seconds.',
},
],
},
{
displayName:
'Image generation can take a while. The request waits up to 5 minutes before it fails.',
name: 'imageNotice',
type: 'notice',
default: '',
displayOptions: { show: { resource: ['image'] } },
},
// --------------------------------------------------------------- model
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: { show: { resource: ['model'] } },
options: [
{
name: 'List Models',
value: 'list',
description: 'Read the models currently offered by TeamoRouter',
action: 'List models',
},
],
default: 'list',
},
// -------------------------------------------------------------- output
{
displayName: 'Chat Reply',
name: 'chatReply',
type: 'boolean',
default: false,
noDataExpression: true,
displayOptions: { show: { resource: ['chat', 'image'] } },
description:
'Whether to put the answer into an <code>output</code> field that a Chat Trigger sends back to the chat: the model text, or the generated image as markdown.',
},
],
};
methods = {
loadOptions: {
getChatModels: modelLoader('chat'),
getImageModels: modelLoader('image'),
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const credentials = await this.getCredentials('teamorouterApi');
const baseURL = normalizeBaseUrl(credentials.baseUrl as string);
for (let i = 0; i < items.length; i++) {
try {
const resource = this.getNodeParameter('resource', i) as string;
const chatReply = this.getNodeParameter('chatReply', i, false) as boolean;
if (resource === 'chat') {
const model = this.getNodeParameter('model', i) as string;
const prompt = (this.getNodeParameter('prompt', i, '') as string) ?? '';
const images = (this.getNodeParameter('images', i, {}) as { image?: InputImage[] })
.image ?? [];
const options = this.getNodeParameter('options', i, {}) as ChatOptions;
if (prompt.trim() === '' && images.length === 0) {
throw new NodeOperationError(
this.getNode(),
'Enter a Prompt or attach at least one input image',
{ itemIndex: i },
);
}
const response = await callApi<ChatCompletionResponse>(this, {
method: 'POST',
baseURL,
url: '/chat/completions',
body: buildChatBody(
this,
model,
await buildUserContent(this, i, prompt, images),
options,
),
json: true,
timeout:
typeof options.timeout === 'number' && options.timeout > 0 ? options.timeout : 600000,
});
const text = extractChatText(response);
const imageUrls = extractChatImages(response);
const json: IDataObject = { text, model: response.model ?? model };
if (response.usage !== undefined) json.usage = response.usage;
const finishReason = response.choices?.[0]?.finish_reason;
if (typeof finishReason === 'string') json.finishReason = finishReason;
const binary: Record<string, IBinaryData> = {};
const markdown: string[] = [];
for (const [index, url] of imageUrls.entries()) {
const inline = splitDataUrl(url);
if (inline === undefined) {
markdown.push(`![image](${url})`);
continue;
}
const propertyName = index === 0 ? 'data' : `data${index}`;
binary[propertyName] = await this.helpers.prepareBinaryData(
Buffer.from(inline.base64, 'base64'),
`teamo-image-${Date.now()}-${index}.${EXTENSION_BY_FORMAT[inline.mimeType.split('/')[1]] ?? 'png'}`,
inline.mimeType,
);
markdown.push(`![image](${url})`);
}
if (imageUrls.length > 0) json.images = imageUrls;
const reply = [text, ...markdown].filter((part) => part !== '').join('\n\n');
if (chatReply) json.output = reply;
returnData.push({
json,
...(Object.keys(binary).length > 0 ? { binary } : {}),
pairedItem: { item: i },
});
continue;
}
if (resource === 'image') {
const operation = this.getNodeParameter('operation', i) as string;
const imageModel = this.getNodeParameter('imageModel', i) as string;
const prompt = this.getNodeParameter('imagePrompt', i) as string;
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string;
const options = this.getNodeParameter('options', i, {}) as ImageOptions;
const timeout =
typeof options.timeout === 'number' && options.timeout > 0 ? options.timeout : 300000;
const requestBody: IHttpRequestOptions = {
method: 'POST',
baseURL,
url: '',
json: true,
timeout,
};
if (operation === 'edit') {
// The edits endpoint wants the picture itself, so it always goes as multipart.
const parts: MultipartPart[] = [];
for (const [name, value] of Object.entries(buildImageBody(imageModel, prompt, options))) {
parts.push({ name, value: String(value) });
}
const input = await readBinary(
this,
i,
this.getNodeParameter('inputBinaryProperty', i, 'data') as string,
);
parts.push({ name: 'image', ...input });
const maskProperty = this.getNodeParameter('maskBinaryProperty', i, '') as string;
if (maskProperty !== '') {
parts.push({ name: 'mask', ...(await readBinary(this, i, maskProperty)) });
}
const multipart = buildMultipart(parts);
requestBody.url = '/images/edits';
requestBody.body = multipart.body;
requestBody.headers = { 'content-type': multipart.contentType };
} else {
requestBody.url = '/images/generations';
requestBody.body = buildImageBody(imageModel, prompt, options);
}
const response = await callApi<ImageResponse>(this, requestBody);
const results = (response.data ?? []).filter(
(entry) =>
(typeof entry?.b64_json === 'string' && entry.b64_json !== '') ||
(typeof entry?.url === 'string' && entry.url !== ''),
);
if (results.length === 0) {
throw new NodeOperationError(
this.getNode(),
'TeamoRouter returned no image data (expected data[].b64_json)',
{ itemIndex: i },
);
}
const format = options.outputFormat ?? 'png';
const mimeType = MIME_BY_FORMAT[format] ?? 'image/png';
const extension = EXTENSION_BY_FORMAT[format] ?? 'png';
const markdown: string[] = [];
const binary: Record<string, IBinaryData> = {};
for (const [index, result] of results.entries()) {
const propertyName = index === 0 ? binaryPropertyName : `${binaryPropertyName}${index}`;
if (typeof result.b64_json === 'string' && result.b64_json !== '') {
binary[propertyName] = await this.helpers.prepareBinaryData(
Buffer.from(result.b64_json, 'base64'),
`teamo-image-${Date.now()}-${index}.${extension}`,
mimeType,
);
markdown.push(`![image](data:${mimeType};base64,${result.b64_json})`);
} else if (typeof result.url === 'string') {
markdown.push(`![image](${result.url})`);
}
}
if (chatReply) {
returnData.push({
json: { model: imageModel, prompt, output: markdown.join('\n\n') },
...(Object.keys(binary).length > 0 ? { binary } : {}),
pairedItem: { item: i },
});
continue;
}
results.forEach((result, index) => {
const propertyName = index === 0 ? binaryPropertyName : `${binaryPropertyName}${index}`;
const json: IDataObject = { model: imageModel, prompt };
if (results.length > 1) json.index = index;
if (typeof result.url === 'string' && result.url !== '') json.url = result.url;
returnData.push({
json,
...(binary[propertyName] !== undefined
? { binary: { [propertyName]: binary[propertyName] } }
: {}),
pairedItem: { item: i },
});
});
continue;
}
const response = await callApi<{ data?: TeamorouterModel[] } | TeamorouterModel[]>(this, {
method: 'GET',
baseURL,
url: '/models',
json: true,
timeout: 60000,
});
const models = Array.isArray(response) ? response : response?.data ?? [];
returnData.push(
...this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(models as unknown as IDataObject[]),
{ itemData: { item: i } },
),
);
} catch (error) {
if (this.continueOnFail()) {
returnData.push(
...this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ error: errorMessage(error) }),
{ itemData: { item: i } },
),
);
continue;
}
throw error;
}
}
return [returnData];
}
}