summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJerop Kipruto <[email protected]>2025-06-15 01:48:01 -0400
committerGitHub <[email protected]>2025-06-15 01:48:01 -0400
commitab932ffaa535d9fee4872f5513a7551abc2ffe2a (patch)
tree55ca302df3dcdd1ba8c7f67c19df9f8043a40063
parente717c51aa1e46d1a1aabdb7e770c248b93e437a6 (diff)
Telemetry: Improve API response logging with function call details (#1064)
-rw-r--r--packages/core/src/core/geminiChat.ts8
-rw-r--r--packages/core/src/utils/generateContentResponseUtilities.test.ts323
-rw-r--r--packages/core/src/utils/generateContentResponseUtilities.ts112
3 files changed, 432 insertions, 11 deletions
diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts
index 1268e8c2..8a9fceab 100644
--- a/packages/core/src/core/geminiChat.ts
+++ b/packages/core/src/core/geminiChat.ts
@@ -27,8 +27,8 @@ import {
combinedUsageMetadata,
} from '../telemetry/loggers.js';
import {
- getResponseText,
- getResponseTextFromParts,
+ getStructuredResponse,
+ getStructuredResponseFromParts,
} from '../utils/generateContentResponseUtilities.js';
/**
@@ -239,7 +239,7 @@ export class GeminiChat {
await this._logApiResponse(
durationMs,
response.usageMetadata,
- getResponseText(response),
+ getStructuredResponse(response),
);
this.sendPromise = (async () => {
@@ -437,7 +437,7 @@ export class GeminiChat {
allParts.push(...content.parts);
}
}
- const fullText = getResponseTextFromParts(allParts);
+ const fullText = getStructuredResponseFromParts(allParts);
await this._logApiResponse(
durationMs,
combinedUsageMetadata(chunks),
diff --git a/packages/core/src/utils/generateContentResponseUtilities.test.ts b/packages/core/src/utils/generateContentResponseUtilities.test.ts
new file mode 100644
index 00000000..5dadab25
--- /dev/null
+++ b/packages/core/src/utils/generateContentResponseUtilities.test.ts
@@ -0,0 +1,323 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect } from 'vitest';
+import {
+ getResponseText,
+ getResponseTextFromParts,
+ getFunctionCalls,
+ getFunctionCallsFromParts,
+ getFunctionCallsAsJson,
+ getFunctionCallsFromPartsAsJson,
+ getStructuredResponse,
+ getStructuredResponseFromParts,
+} from './generateContentResponseUtilities.js';
+import {
+ GenerateContentResponse,
+ Part,
+ FinishReason,
+ SafetyRating,
+} from '@google/genai';
+
+const mockTextPart = (text: string): Part => ({ text });
+const mockFunctionCallPart = (
+ name: string,
+ args?: Record<string, unknown>,
+): Part => ({
+ functionCall: { name, args: args ?? {} },
+});
+
+const mockResponse = (
+ parts: Part[],
+ finishReason: FinishReason = FinishReason.STOP,
+ safetyRatings: SafetyRating[] = [],
+): GenerateContentResponse => ({
+ candidates: [
+ {
+ content: {
+ parts,
+ role: 'model',
+ },
+ index: 0,
+ finishReason,
+ safetyRatings,
+ },
+ ],
+ promptFeedback: {
+ safetyRatings: [],
+ },
+ text: undefined,
+ data: undefined,
+ functionCalls: undefined,
+ executableCode: undefined,
+ codeExecutionResult: undefined,
+});
+
+const minimalMockResponse = (
+ candidates: GenerateContentResponse['candidates'],
+): GenerateContentResponse => ({
+ candidates,
+ promptFeedback: { safetyRatings: [] },
+ text: undefined,
+ data: undefined,
+ functionCalls: undefined,
+ executableCode: undefined,
+ codeExecutionResult: undefined,
+});
+
+describe('generateContentResponseUtilities', () => {
+ describe('getResponseText', () => {
+ it('should return undefined for no candidates', () => {
+ expect(getResponseText(minimalMockResponse(undefined))).toBeUndefined();
+ });
+ it('should return undefined for empty candidates array', () => {
+ expect(getResponseText(minimalMockResponse([]))).toBeUndefined();
+ });
+ it('should return undefined for no parts', () => {
+ const response = mockResponse([]);
+ expect(getResponseText(response)).toBeUndefined();
+ });
+ it('should extract text from a single text part', () => {
+ const response = mockResponse([mockTextPart('Hello')]);
+ expect(getResponseText(response)).toBe('Hello');
+ });
+ it('should concatenate text from multiple text parts', () => {
+ const response = mockResponse([
+ mockTextPart('Hello '),
+ mockTextPart('World'),
+ ]);
+ expect(getResponseText(response)).toBe('Hello World');
+ });
+ it('should ignore function call parts', () => {
+ const response = mockResponse([
+ mockTextPart('Hello '),
+ mockFunctionCallPart('testFunc'),
+ mockTextPart('World'),
+ ]);
+ expect(getResponseText(response)).toBe('Hello World');
+ });
+ it('should return undefined if only function call parts exist', () => {
+ const response = mockResponse([
+ mockFunctionCallPart('testFunc'),
+ mockFunctionCallPart('anotherFunc'),
+ ]);
+ expect(getResponseText(response)).toBeUndefined();
+ });
+ });
+
+ describe('getResponseTextFromParts', () => {
+ it('should return undefined for no parts', () => {
+ expect(getResponseTextFromParts([])).toBeUndefined();
+ });
+ it('should extract text from a single text part', () => {
+ expect(getResponseTextFromParts([mockTextPart('Hello')])).toBe('Hello');
+ });
+ it('should concatenate text from multiple text parts', () => {
+ expect(
+ getResponseTextFromParts([
+ mockTextPart('Hello '),
+ mockTextPart('World'),
+ ]),
+ ).toBe('Hello World');
+ });
+ it('should ignore function call parts', () => {
+ expect(
+ getResponseTextFromParts([
+ mockTextPart('Hello '),
+ mockFunctionCallPart('testFunc'),
+ mockTextPart('World'),
+ ]),
+ ).toBe('Hello World');
+ });
+ it('should return undefined if only function call parts exist', () => {
+ expect(
+ getResponseTextFromParts([
+ mockFunctionCallPart('testFunc'),
+ mockFunctionCallPart('anotherFunc'),
+ ]),
+ ).toBeUndefined();
+ });
+ });
+
+ describe('getFunctionCalls', () => {
+ it('should return undefined for no candidates', () => {
+ expect(getFunctionCalls(minimalMockResponse(undefined))).toBeUndefined();
+ });
+ it('should return undefined for empty candidates array', () => {
+ expect(getFunctionCalls(minimalMockResponse([]))).toBeUndefined();
+ });
+ it('should return undefined for no parts', () => {
+ const response = mockResponse([]);
+ expect(getFunctionCalls(response)).toBeUndefined();
+ });
+ it('should extract a single function call', () => {
+ const func = { name: 'testFunc', args: { a: 1 } };
+ const response = mockResponse([
+ mockFunctionCallPart(func.name, func.args),
+ ]);
+ expect(getFunctionCalls(response)).toEqual([func]);
+ });
+ it('should extract multiple function calls', () => {
+ const func1 = { name: 'testFunc1', args: { a: 1 } };
+ const func2 = { name: 'testFunc2', args: { b: 2 } };
+ const response = mockResponse([
+ mockFunctionCallPart(func1.name, func1.args),
+ mockFunctionCallPart(func2.name, func2.args),
+ ]);
+ expect(getFunctionCalls(response)).toEqual([func1, func2]);
+ });
+ it('should ignore text parts', () => {
+ const func = { name: 'testFunc', args: { a: 1 } };
+ const response = mockResponse([
+ mockTextPart('Some text'),
+ mockFunctionCallPart(func.name, func.args),
+ mockTextPart('More text'),
+ ]);
+ expect(getFunctionCalls(response)).toEqual([func]);
+ });
+ it('should return undefined if only text parts exist', () => {
+ const response = mockResponse([
+ mockTextPart('Some text'),
+ mockTextPart('More text'),
+ ]);
+ expect(getFunctionCalls(response)).toBeUndefined();
+ });
+ });
+
+ describe('getFunctionCallsFromParts', () => {
+ it('should return undefined for no parts', () => {
+ expect(getFunctionCallsFromParts([])).toBeUndefined();
+ });
+ it('should extract a single function call', () => {
+ const func = { name: 'testFunc', args: { a: 1 } };
+ expect(
+ getFunctionCallsFromParts([mockFunctionCallPart(func.name, func.args)]),
+ ).toEqual([func]);
+ });
+ it('should extract multiple function calls', () => {
+ const func1 = { name: 'testFunc1', args: { a: 1 } };
+ const func2 = { name: 'testFunc2', args: { b: 2 } };
+ expect(
+ getFunctionCallsFromParts([
+ mockFunctionCallPart(func1.name, func1.args),
+ mockFunctionCallPart(func2.name, func2.args),
+ ]),
+ ).toEqual([func1, func2]);
+ });
+ it('should ignore text parts', () => {
+ const func = { name: 'testFunc', args: { a: 1 } };
+ expect(
+ getFunctionCallsFromParts([
+ mockTextPart('Some text'),
+ mockFunctionCallPart(func.name, func.args),
+ mockTextPart('More text'),
+ ]),
+ ).toEqual([func]);
+ });
+ it('should return undefined if only text parts exist', () => {
+ expect(
+ getFunctionCallsFromParts([
+ mockTextPart('Some text'),
+ mockTextPart('More text'),
+ ]),
+ ).toBeUndefined();
+ });
+ });
+
+ describe('getFunctionCallsAsJson', () => {
+ it('should return JSON string of function calls', () => {
+ const func1 = { name: 'testFunc1', args: { a: 1 } };
+ const func2 = { name: 'testFunc2', args: { b: 2 } };
+ const response = mockResponse([
+ mockFunctionCallPart(func1.name, func1.args),
+ mockTextPart('text in between'),
+ mockFunctionCallPart(func2.name, func2.args),
+ ]);
+ const expectedJson = JSON.stringify([func1, func2], null, 2);
+ expect(getFunctionCallsAsJson(response)).toBe(expectedJson);
+ });
+ it('should return undefined if no function calls', () => {
+ const response = mockResponse([mockTextPart('Hello')]);
+ expect(getFunctionCallsAsJson(response)).toBeUndefined();
+ });
+ });
+
+ describe('getFunctionCallsFromPartsAsJson', () => {
+ it('should return JSON string of function calls from parts', () => {
+ const func1 = { name: 'testFunc1', args: { a: 1 } };
+ const func2 = { name: 'testFunc2', args: { b: 2 } };
+ const parts = [
+ mockFunctionCallPart(func1.name, func1.args),
+ mockTextPart('text in between'),
+ mockFunctionCallPart(func2.name, func2.args),
+ ];
+ const expectedJson = JSON.stringify([func1, func2], null, 2);
+ expect(getFunctionCallsFromPartsAsJson(parts)).toBe(expectedJson);
+ });
+ it('should return undefined if no function calls in parts', () => {
+ const parts = [mockTextPart('Hello')];
+ expect(getFunctionCallsFromPartsAsJson(parts)).toBeUndefined();
+ });
+ });
+
+ describe('getStructuredResponse', () => {
+ it('should return only text if only text exists', () => {
+ const response = mockResponse([mockTextPart('Hello World')]);
+ expect(getStructuredResponse(response)).toBe('Hello World');
+ });
+ it('should return only function call JSON if only function calls exist', () => {
+ const func = { name: 'testFunc', args: { data: 'payload' } };
+ const response = mockResponse([
+ mockFunctionCallPart(func.name, func.args),
+ ]);
+ const expectedJson = JSON.stringify([func], null, 2);
+ expect(getStructuredResponse(response)).toBe(expectedJson);
+ });
+ it('should return text and function call JSON if both exist', () => {
+ const text = 'Consider this data:';
+ const func = { name: 'processData', args: { item: 42 } };
+ const response = mockResponse([
+ mockTextPart(text),
+ mockFunctionCallPart(func.name, func.args),
+ ]);
+ const expectedJson = JSON.stringify([func], null, 2);
+ expect(getStructuredResponse(response)).toBe(`${text}\n${expectedJson}`);
+ });
+ it('should return undefined if neither text nor function calls exist', () => {
+ const response = mockResponse([]);
+ expect(getStructuredResponse(response)).toBeUndefined();
+ });
+ });
+
+ describe('getStructuredResponseFromParts', () => {
+ it('should return only text if only text exists in parts', () => {
+ const parts = [mockTextPart('Hello World')];
+ expect(getStructuredResponseFromParts(parts)).toBe('Hello World');
+ });
+ it('should return only function call JSON if only function calls exist in parts', () => {
+ const func = { name: 'testFunc', args: { data: 'payload' } };
+ const parts = [mockFunctionCallPart(func.name, func.args)];
+ const expectedJson = JSON.stringify([func], null, 2);
+ expect(getStructuredResponseFromParts(parts)).toBe(expectedJson);
+ });
+ it('should return text and function call JSON if both exist in parts', () => {
+ const text = 'Consider this data:';
+ const func = { name: 'processData', args: { item: 42 } };
+ const parts = [
+ mockTextPart(text),
+ mockFunctionCallPart(func.name, func.args),
+ ];
+ const expectedJson = JSON.stringify([func], null, 2);
+ expect(getStructuredResponseFromParts(parts)).toBe(
+ `${text}\n${expectedJson}`,
+ );
+ });
+ it('should return undefined if neither text nor function calls exist in parts', () => {
+ const parts: Part[] = [];
+ expect(getStructuredResponseFromParts(parts)).toBeUndefined();
+ });
+ });
+});
diff --git a/packages/core/src/utils/generateContentResponseUtilities.ts b/packages/core/src/utils/generateContentResponseUtilities.ts
index d575bca8..c5125753 100644
--- a/packages/core/src/utils/generateContentResponseUtilities.ts
+++ b/packages/core/src/utils/generateContentResponseUtilities.ts
@@ -4,18 +4,116 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { GenerateContentResponse, Part } from '@google/genai';
+import { GenerateContentResponse, Part, FunctionCall } from '@google/genai';
export function getResponseText(
response: GenerateContentResponse,
): string | undefined {
- return (
- response.candidates?.[0]?.content?.parts
- ?.map((part) => part.text)
- .join('') || undefined
- );
+ const parts = response.candidates?.[0]?.content?.parts;
+ if (!parts) {
+ return undefined;
+ }
+ const textSegments = parts
+ .map((part) => part.text)
+ .filter((text): text is string => typeof text === 'string');
+
+ if (textSegments.length === 0) {
+ return undefined;
+ }
+ return textSegments.join('');
}
export function getResponseTextFromParts(parts: Part[]): string | undefined {
- return parts?.map((part) => part.text).join('') || undefined;
+ if (!parts) {
+ return undefined;
+ }
+ const textSegments = parts
+ .map((part) => part.text)
+ .filter((text): text is string => typeof text === 'string');
+
+ if (textSegments.length === 0) {
+ return undefined;
+ }
+ return textSegments.join('');
+}
+
+export function getFunctionCalls(
+ response: GenerateContentResponse,
+): FunctionCall[] | undefined {
+ const parts = response.candidates?.[0]?.content?.parts;
+ if (!parts) {
+ return undefined;
+ }
+ const functionCallParts = parts
+ .filter((part) => !!part.functionCall)
+ .map((part) => part.functionCall as FunctionCall);
+ return functionCallParts.length > 0 ? functionCallParts : undefined;
+}
+
+export function getFunctionCallsFromParts(
+ parts: Part[],
+): FunctionCall[] | undefined {
+ if (!parts) {
+ return undefined;
+ }
+ const functionCallParts = parts
+ .filter((part) => !!part.functionCall)
+ .map((part) => part.functionCall as FunctionCall);
+ return functionCallParts.length > 0 ? functionCallParts : undefined;
+}
+
+export function getFunctionCallsAsJson(
+ response: GenerateContentResponse,
+): string | undefined {
+ const functionCalls = getFunctionCalls(response);
+ if (!functionCalls) {
+ return undefined;
+ }
+ return JSON.stringify(functionCalls, null, 2);
+}
+
+export function getFunctionCallsFromPartsAsJson(
+ parts: Part[],
+): string | undefined {
+ const functionCalls = getFunctionCallsFromParts(parts);
+ if (!functionCalls) {
+ return undefined;
+ }
+ return JSON.stringify(functionCalls, null, 2);
+}
+
+export function getStructuredResponse(
+ response: GenerateContentResponse,
+): string | undefined {
+ const textContent = getResponseText(response);
+ const functionCallsJson = getFunctionCallsAsJson(response);
+
+ if (textContent && functionCallsJson) {
+ return `${textContent}\n${functionCallsJson}`;
+ }
+ if (textContent) {
+ return textContent;
+ }
+ if (functionCallsJson) {
+ return functionCallsJson;
+ }
+ return undefined;
+}
+
+export function getStructuredResponseFromParts(
+ parts: Part[],
+): string | undefined {
+ const textContent = getResponseTextFromParts(parts);
+ const functionCallsJson = getFunctionCallsFromPartsAsJson(parts);
+
+ if (textContent && functionCallsJson) {
+ return `${textContent}\n${functionCallsJson}`;
+ }
+ if (textContent) {
+ return textContent;
+ }
+ if (functionCallsJson) {
+ return functionCallsJson;
+ }
+ return undefined;
}