diff --git a/src/api/providers/__tests__/gemini.spec.ts b/src/api/providers/__tests__/gemini.spec.ts index 812c1ae1a64..d6b3e9006d0 100644 --- a/src/api/providers/__tests__/gemini.spec.ts +++ b/src/api/providers/__tests__/gemini.spec.ts @@ -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) diff --git a/src/api/providers/gemini.ts b/src/api/providers/gemini.ts index 5e547edbdc6..a10f53f3ae0 100644 --- a/src/api/providers/gemini.ts +++ b/src/api/providers/gemini.ts @@ -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) { diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 05d039a495d..a654f334357 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -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.", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 808d5345725..dffbfe37553 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -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 密钥是否有效且未过期。",