n8n-nodes-teamorouter/nodes/Teamorouter/Teamorouter.node.ts

952 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',
codex: {
categories: ['AI'],
subcategories: {
AI: ['Root Nodes'],
},
},
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',
description:
'Name of the binary property that holds the image. Used when Source is Binary File.',
},
{
displayName: 'Image URL',
name: 'url',
type: 'string',
default: '',
placeholder: 'https://example.com/cat.png',
description:
'Publicly reachable image link, or a data:image/...;base64,... URL. Used when Source is Image 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',
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. Only used when editing an image.',
},
{
displayName: 'Moderation',
name: 'moderation',
type: 'options',
default: 'auto',
options: [
{ name: 'Auto', value: 'auto', description: 'Standard content filter' },
{ name: 'Low', value: 'low', description: 'Relaxed content filter' },
],
description: 'Content filter applied by TeamoRouter. Only used when generating an image.',
},
{
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];
}
}