Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as helpers from '@utils/helpers';
import { mockDeep } from 'jest-mock-extended';
import type { IExecuteFunctions, IBinaryData, INode } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';

import * as helpers from '@utils/helpers';

import * as audio from './actions/audio';
import * as file from './actions/file';
import * as image from './actions/image';
Expand Down Expand Up @@ -730,6 +731,54 @@ describe('GoogleGemini Node', () => {
);
});

it('should skip undefined parts when parsing tool calls', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe better name
"should handle undefined entries in parts array"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, updated it

switch (parameter) {
case 'modelId':
return 'models/gemini-2.5-flash';
case 'messages.values':
return [{ role: 'user', content: 'foo' }];
case 'simplify':
return true;
case 'jsonOutput':
return false;
case 'builtInTools':
return {};
case 'options':
return {};
case 'options.maxToolsIterations':
return 15;
default:
return undefined;
}
});
executeFunctionsMock.getNodeInputs.mockReturnValue([{ type: 'main' }]);
apiRequestMock.mockResolvedValue({
candidates: [
{
content: {
parts: [undefined, { text: 'bar' }],
role: 'model',
},
},
],
});

const result = await text.message.execute.call(executeFunctionsMock, 0);

expect(result).toEqual([
{
json: {
content: {
parts: [undefined, { text: 'bar' }],
role: 'model',
},
},
pairedItem: { item: 0 },
},
]);
});

describe('includeMergedResponse', () => {
it('should include mergedResponse per candidate when enabled and simplify is true', async () => {
executeFunctionsMock.getNodeParameter.mockImplementation((parameter: string) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { getConnectedTools } from '@utils/helpers';
import {
type IDataObject,
type IExecuteFunctions,
Expand All @@ -11,13 +10,17 @@ import {
} from 'n8n-workflow';
import zodToJsonSchema from 'zod-to-json-schema';

import { getConnectedTools } from '@utils/helpers';

import type {
GenerateContentRequest,
GenerateContentResponse,
Content,
Tool,
GenerateContentGenerationConfig,
BuiltInTools,
FunctionCallPart,
TextPart,
} from '../../helpers/interfaces';
import { apiRequest } from '../../transport';
import { modelRLC } from '../descriptions';
Expand Down Expand Up @@ -341,8 +344,16 @@ const displayOptions = {

export const description = updateDisplayOptions(displayOptions, properties);

function isFunctionCallPart(part: unknown): part is FunctionCallPart {
return !!part && typeof part === 'object' && 'functionCall' in part;
}

function isTextPart(part: unknown): part is TextPart {
return !!part && typeof part === 'object' && 'text' in part;
}

function getToolCalls(response: GenerateContentResponse) {
return response.candidates.flatMap((c) => c.content.parts).filter((p) => 'functionCall' in p);
return response.candidates.flatMap((c) => c?.content?.parts ?? []).filter(isFunctionCallPart);
}

export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
Expand Down Expand Up @@ -538,7 +549,10 @@ export async function execute(this: IExecuteFunctions, i: number): Promise<INode
break;
}

contents.push(...response.candidates.map((c) => c.content));
contents.push.apply(
contents,
response.candidates.map((c) => c.content),
);
Comment on lines +557 to +560
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude:
push.apply offers no performance advantage over spread in modern JS engines. Both translate to the same operation: passing array elements as individual arguments to push.

The old push.apply pattern came from pre-ES6 days when spread didn't exist. It was used as a workaround. Today, contents.push(...arr) is the idiomatic equivalent and compiles to the same bytecode in V8.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this got changed because I ran eslint autofix, and we have a lint rule to prefer apply over spread

The reasoning: if the array being spread is very big, it can cause stack overflow, because each array item is converted into a function argument, therefor it takes a lot of memory on the stack

This doesn't apply here, because contents is always rather small, but I'll leave it like this to keep the linter happy


for (const { functionCall } of toolCalls) {
let toolResponse;
Expand Down Expand Up @@ -575,9 +589,9 @@ export async function execute(this: IExecuteFunctions, i: number): Promise<INode
const candidates = options.includeMergedResponse
? response.candidates.map((candidate) => ({
...candidate,
mergedResponse: candidate.content.parts
.filter((part) => 'text' in part)
.map((part) => (part as { text: string }).text)
mergedResponse: (candidate?.content?.parts ?? [])
.filter(isTextPart)
.map((part) => part.text)
.join(''),
}))
: response.candidates;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,34 +51,46 @@ export interface Content {
role: string;
}

export interface TextPart {
text: string;
}

export interface InlineDataPart {
inlineData: {
mimeType: string;
data: string;
};
}

export interface FunctionCallPart {
functionCall: {
id?: string;
name: string;
args?: IDataObject;
};
}

export interface FunctionResponsePart {
functionResponse: {
id?: string;
name: string;
response: IDataObject;
};
}

export interface FileDataPart {
fileData?: {
mimeType?: string;
fileUri?: string;
};
}

export type Part =
| { text: string }
| {
inlineData: {
mimeType: string;
data: string;
};
}
| {
functionCall: {
id?: string;
name: string;
args?: IDataObject;
};
}
| {
functionResponse: {
id?: string;
name: string;
response: IDataObject;
};
}
| {
fileData?: {
mimeType?: string;
fileUri?: string;
};
};
| TextPart
| InlineDataPart
| FunctionCallPart
| FunctionResponsePart
| FileDataPart;

export interface ImagenResponse {
predictions: Array<{
Expand Down
Loading