Include Schema Error Handling for Vertex and Google Auth methods (#5780)

Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
Adam Weidman 2025-08-07 20:21:39 +00:00 committed by GitHub
parent 8e6a565adb
commit 3a3b138195
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 39 additions and 29 deletions

View File

@ -33,7 +33,7 @@ import {
} from '../telemetry/types.js'; } from '../telemetry/types.js';
import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js'; import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js';
import { hasCycleInSchema } from '../tools/tools.js'; import { hasCycleInSchema } from '../tools/tools.js';
import { isStructuredError } from '../utils/quotaErrorDetection.js'; import { StructuredError } from './turn.js';
/** /**
* Returns true if the response is valid, false otherwise. * Returns true if the response is valid, false otherwise.
@ -352,7 +352,6 @@ export class GeminiChat {
} catch (error) { } catch (error) {
const durationMs = Date.now() - startTime; const durationMs = Date.now() - startTime;
this._logApiError(durationMs, error, prompt_id); this._logApiError(durationMs, error, prompt_id);
await this.maybeIncludeSchemaDepthContext(error);
this.sendPromise = Promise.resolve(); this.sendPromise = Promise.resolve();
throw error; throw error;
} }
@ -452,7 +451,6 @@ export class GeminiChat {
const durationMs = Date.now() - startTime; const durationMs = Date.now() - startTime;
this._logApiError(durationMs, error, prompt_id); this._logApiError(durationMs, error, prompt_id);
this.sendPromise = Promise.resolve(); this.sendPromise = Promise.resolve();
await this.maybeIncludeSchemaDepthContext(error);
throw error; throw error;
} }
} }
@ -523,6 +521,34 @@ export class GeminiChat {
return lastChunkWithMetadata?.usageMetadata; return lastChunkWithMetadata?.usageMetadata;
} }
async maybeIncludeSchemaDepthContext(error: StructuredError): Promise<void> {
// Check for potentially problematic cyclic tools with cyclic schemas
// and include a recommendation to remove potentially problematic tools.
if (
isSchemaDepthError(error.message) ||
isInvalidArgumentError(error.message)
) {
const tools = (await this.config.getToolRegistry()).getAllTools();
const cyclicSchemaTools: string[] = [];
for (const tool of tools) {
if (
(tool.schema.parametersJsonSchema &&
hasCycleInSchema(tool.schema.parametersJsonSchema)) ||
(tool.schema.parameters && hasCycleInSchema(tool.schema.parameters))
) {
cyclicSchemaTools.push(tool.displayName);
}
}
if (cyclicSchemaTools.length > 0) {
const extraDetails =
`\n\nThis error was probably caused by cyclic schema references in one of the following tools, try disabling them with excludeTools:\n\n - ` +
cyclicSchemaTools.join(`\n - `) +
`\n`;
error.message += extraDetails;
}
}
}
private async *processStreamResponse( private async *processStreamResponse(
streamResponse: AsyncGenerator<GenerateContentResponse>, streamResponse: AsyncGenerator<GenerateContentResponse>,
inputContent: Content, inputContent: Content,
@ -684,34 +710,13 @@ export class GeminiChat {
content.parts[0].thought === true content.parts[0].thought === true
); );
} }
private async maybeIncludeSchemaDepthContext(error: unknown): Promise<void> {
// Check for potentially problematic cyclic tools with cyclic schemas
// and include a recommendation to remove potentially problematic tools.
if (isStructuredError(error) && isSchemaDepthError(error.message)) {
const tools = (await this.config.getToolRegistry()).getAllTools();
const cyclicSchemaTools: string[] = [];
for (const tool of tools) {
if (
(tool.schema.parametersJsonSchema &&
hasCycleInSchema(tool.schema.parametersJsonSchema)) ||
(tool.schema.parameters && hasCycleInSchema(tool.schema.parameters))
) {
cyclicSchemaTools.push(tool.displayName);
}
}
if (cyclicSchemaTools.length > 0) {
const extraDetails =
`\n\nThis error was probably caused by cyclic schema references in one of the following tools, try disabling them:\n\n - ` +
cyclicSchemaTools.join(`\n - `) +
`\n`;
error.message += extraDetails;
}
}
}
} }
/** Visible for Testing */ /** Visible for Testing */
export function isSchemaDepthError(errorMessage: string): boolean { export function isSchemaDepthError(errorMessage: string): boolean {
return errorMessage.includes('maximum schema depth exceeded'); return errorMessage.includes('maximum schema depth exceeded');
} }
export function isInvalidArgumentError(errorMessage: string): boolean {
return errorMessage.includes('Request contains an invalid argument');
}

View File

@ -17,12 +17,14 @@ import { GeminiChat } from './geminiChat.js';
const mockSendMessageStream = vi.fn(); const mockSendMessageStream = vi.fn();
const mockGetHistory = vi.fn(); const mockGetHistory = vi.fn();
const mockMaybeIncludeSchemaDepthContext = vi.fn();
vi.mock('@google/genai', async (importOriginal) => { vi.mock('@google/genai', async (importOriginal) => {
const actual = await importOriginal<typeof import('@google/genai')>(); const actual = await importOriginal<typeof import('@google/genai')>();
const MockChat = vi.fn().mockImplementation(() => ({ const MockChat = vi.fn().mockImplementation(() => ({
sendMessageStream: mockSendMessageStream, sendMessageStream: mockSendMessageStream,
getHistory: mockGetHistory, getHistory: mockGetHistory,
maybeIncludeSchemaDepthContext: mockMaybeIncludeSchemaDepthContext,
})); }));
return { return {
...actual, ...actual,
@ -46,6 +48,7 @@ describe('Turn', () => {
type MockedChatInstance = { type MockedChatInstance = {
sendMessageStream: typeof mockSendMessageStream; sendMessageStream: typeof mockSendMessageStream;
getHistory: typeof mockGetHistory; getHistory: typeof mockGetHistory;
maybeIncludeSchemaDepthContext: typeof mockMaybeIncludeSchemaDepthContext;
}; };
let mockChatInstance: MockedChatInstance; let mockChatInstance: MockedChatInstance;
@ -54,6 +57,7 @@ describe('Turn', () => {
mockChatInstance = { mockChatInstance = {
sendMessageStream: mockSendMessageStream, sendMessageStream: mockSendMessageStream,
getHistory: mockGetHistory, getHistory: mockGetHistory,
maybeIncludeSchemaDepthContext: mockMaybeIncludeSchemaDepthContext,
}; };
turn = new Turn(mockChatInstance as unknown as GeminiChat, 'prompt-id-1'); turn = new Turn(mockChatInstance as unknown as GeminiChat, 'prompt-id-1');
mockGetHistory.mockReturnValue([]); mockGetHistory.mockReturnValue([]);
@ -200,7 +204,7 @@ describe('Turn', () => {
{ role: 'model', parts: [{ text: 'Previous history' }] }, { role: 'model', parts: [{ text: 'Previous history' }] },
]; ];
mockGetHistory.mockReturnValue(historyContent); mockGetHistory.mockReturnValue(historyContent);
mockMaybeIncludeSchemaDepthContext.mockResolvedValue(undefined);
const events = []; const events = [];
for await (const event of turn.run( for await (const event of turn.run(
reqParts, reqParts,

View File

@ -275,6 +275,7 @@ export class Turn {
message: getErrorMessage(error), message: getErrorMessage(error),
status, status,
}; };
await this.chat.maybeIncludeSchemaDepthContext(structuredError);
yield { type: GeminiEventType.Error, value: { error: structuredError } }; yield { type: GeminiEventType.Error, value: { error: structuredError } };
return; return;
} }