diff --git a/packages/core/src/core/geminiChat.test.ts b/packages/core/src/core/geminiChat.test.ts index 6cdc6ebdfa..d85609fec5 100644 --- a/packages/core/src/core/geminiChat.test.ts +++ b/packages/core/src/core/geminiChat.test.ts @@ -13,7 +13,7 @@ import type { import type { ContentGenerator } from '../core/contentGenerator.js'; import { GeminiChat, - EmptyStreamError, + InvalidStreamError, StreamEventType, type StreamEvent, } from './geminiChat.js'; @@ -237,7 +237,7 @@ describe('GeminiChat', () => { /* consume stream */ } })(), - ).rejects.toThrow(EmptyStreamError); + ).rejects.toThrow(InvalidStreamError); }); it('should succeed if the stream ends with an invalid part but has a finishReason and contained a valid part', async () => { @@ -501,14 +501,14 @@ describe('GeminiChat', () => { 'prompt-id-stream-1', ); - // 4. Assert: The stream processing should throw an EmptyStreamError. + // 4. Assert: The stream processing should throw an InvalidStreamError. await expect( (async () => { for await (const _ of stream) { // This loop consumes the stream to trigger the internal logic. } })(), - ).rejects.toThrow(EmptyStreamError); + ).rejects.toThrow(InvalidStreamError); }); it('should succeed when there is a tool call without finish reason', async () => { @@ -554,7 +554,7 @@ describe('GeminiChat', () => { ).resolves.not.toThrow(); }); - it('should throw EmptyStreamError when no tool call and no finish reason', async () => { + it('should throw InvalidStreamError when no tool call and no finish reason', async () => { // Setup: Stream with text but no finish reason and no tool call const streamWithoutFinishReason = (async function* () { yield { @@ -586,10 +586,10 @@ describe('GeminiChat', () => { // consume stream } })(), - ).rejects.toThrow(EmptyStreamError); + ).rejects.toThrow(InvalidStreamError); }); - it('should throw EmptyStreamError when no tool call and empty response text', async () => { + it('should throw InvalidStreamError when no tool call and empty response text', async () => { // Setup: Stream with finish reason but empty response (only thoughts) const streamWithEmptyResponse = (async function* () { yield { @@ -621,7 +621,7 @@ describe('GeminiChat', () => { // consume stream } })(), - ).rejects.toThrow(EmptyStreamError); + ).rejects.toThrow(InvalidStreamError); }); it('should succeed when there is finish reason and response text', async () => { @@ -889,7 +889,7 @@ describe('GeminiChat', () => { for await (const _ of stream) { // Must loop to trigger the internal logic that throws. } - }).rejects.toThrow(EmptyStreamError); + }).rejects.toThrow(InvalidStreamError); // Should be called 3 times (initial + 2 retries) expect(mockContentGenerator.generateContentStream).toHaveBeenCalledTimes( diff --git a/packages/core/src/core/geminiChat.ts b/packages/core/src/core/geminiChat.ts index 77831c574a..8088facb94 100644 --- a/packages/core/src/core/geminiChat.ts +++ b/packages/core/src/core/geminiChat.ts @@ -161,13 +161,16 @@ function extractCuratedHistory(comprehensiveHistory: Content[]): Content[] { } /** - * Custom error to signal that a stream completed without valid content, + * Custom error to signal that a stream completed with invalid content, * which should trigger a retry. */ -export class EmptyStreamError extends Error { - constructor(message: string) { +export class InvalidStreamError extends Error { + readonly type: 'NO_FINISH_REASON' | 'NO_RESPONSE_TEXT'; + + constructor(message: string, type: 'NO_FINISH_REASON' | 'NO_RESPONSE_TEXT') { super(message); - this.name = 'EmptyStreamError'; + this.name = 'InvalidStreamError'; + this.type = type; } } @@ -284,7 +287,7 @@ export class GeminiChat { break; } catch (error) { lastError = error; - const isContentError = error instanceof EmptyStreamError; + const isContentError = error instanceof InvalidStreamError; if (isContentError) { // Check if we have more attempts left. @@ -293,7 +296,7 @@ export class GeminiChat { self.config, new ContentRetryEvent( attempt, - 'EmptyStreamError', + (error as InvalidStreamError).type, INVALID_CONTENT_RETRY_OPTIONS.initialDelayMs, ), ); @@ -312,12 +315,12 @@ export class GeminiChat { } if (lastError) { - if (lastError instanceof EmptyStreamError) { + if (lastError instanceof InvalidStreamError) { logContentRetryFailure( self.config, new ContentRetryFailureEvent( INVALID_CONTENT_RETRY_OPTIONS.maxAttempts, - 'EmptyStreamError', + (lastError as InvalidStreamError).type, ), ); } @@ -564,9 +567,17 @@ export class GeminiChat { // - No finish reason, OR // - Empty response text (e.g., only thoughts with no actual content) if (!hasToolCall && (!hasFinishReason || !responseText)) { - throw new EmptyStreamError( - 'Model stream ended with an invalid chunk or missing finish reason.', - ); + if (!hasFinishReason) { + throw new InvalidStreamError( + 'Model stream ended without a finish reason.', + 'NO_FINISH_REASON', + ); + } else { + throw new InvalidStreamError( + 'Model stream ended with empty response text.', + 'NO_RESPONSE_TEXT', + ); + } } this.history.push({ role: 'model', parts: consolidatedParts });