mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
feat(core): Log invalid stream type (#9168)
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user