fix: handle empty responses from Gemini API

- Add tracking for whether any text content was yielded
- Throw specific error when Gemini API returns empty response
- Add translation keys for empty response error in English and Chinese
- Add test coverage for empty response scenario

Fixes #7046
This commit is contained in:
Roo Code 2025-08-13 09:39:50 +00:00
parent 962df868bd
commit 351dc5f448
4 changed files with 38 additions and 2 deletions

View file

@ -90,6 +90,24 @@ describe("GeminiHandler", () => {
)
})
it("should handle empty response from API", async () => {
// Setup the mock to return an async generator with no text content
;(handler["client"].models.generateContentStream as any).mockResolvedValue({
[Symbol.asyncIterator]: async function* () {
// Yield only usage metadata, no text content
yield { usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 0 } }
},
})
const stream = handler.createMessage(systemPrompt, mockMessages)
await expect(async () => {
for await (const _chunk of stream) {
// Should throw before yielding any chunks
}
}).rejects.toThrow(t("common:errors.gemini.generate_stream"))
})
it("should handle API errors", async () => {
const mockError = new Error("Gemini API error")
;(handler["client"].models.generateContentStream as any).mockRejectedValue(mockError)

View file

@ -94,6 +94,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
let pendingGroundingMetadata: GroundingMetadata | undefined
let hasYieldedContent = false // Track if we've yielded any text content
for await (const chunk of result) {
// Process candidates and their parts to separate thoughts from content
@ -115,6 +116,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
// This is regular content
if (part.text) {
yield { type: "text", text: part.text }
hasYieldedContent = true
}
}
}
@ -124,6 +126,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
// Fallback to the original text property if no candidates structure
else if (chunk.text) {
yield { type: "text", text: chunk.text }
hasYieldedContent = true
}
if (chunk.usageMetadata) {
@ -131,6 +134,19 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
}
}
// Check if we got an empty response
if (!hasYieldedContent) {
// Log the issue for debugging
console.warn("Gemini API returned empty response, no text content was generated")
// Throw a specific error that can be caught and retried
throw new Error(
t("common:errors.gemini.empty_response", {
error: "The Gemini API did not return any text content. This may be a temporary issue.",
}),
)
}
if (pendingGroundingMetadata) {
const citations = this.extractCitationsOnly(pendingGroundingMetadata)
if (citations) {

View file

@ -90,7 +90,8 @@
"gemini": {
"generate_stream": "Gemini generate context stream error: {{error}}",
"generate_complete_prompt": "Gemini completion error: {{error}}",
"sources": "Sources:"
"sources": "Sources:",
"empty_response": "Gemini API returned empty response: {{error}}"
},
"cerebras": {
"authenticationFailed": "Cerebras API authentication failed. Please check your API key is valid and not expired.",

View file

@ -95,7 +95,8 @@
"gemini": {
"generate_stream": "Gemini 生成上下文流错误:{{error}}",
"generate_complete_prompt": "Gemini 完成错误:{{error}}",
"sources": "来源:"
"sources": "来源:",
"empty_response": "Gemini API 返回空响应:{{error}}"
},
"cerebras": {
"authenticationFailed": "Cerebras API 身份验证失败。请检查你的 API 密钥是否有效且未过期。",