Skip to content

Commit 47320dc

Browse files
fix: handle empty Gemini responses and reasoning loops (#10007)
1 parent f9cfc66 commit 47320dc

File tree

20 files changed

+140
-26
lines changed

20 files changed

+140
-26
lines changed

src/api/providers/gemini.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,17 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
199199
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
200200
let pendingGroundingMetadata: GroundingMetadata | undefined
201201
let finalResponse: { responseId?: string } | undefined
202+
let finishReason: string | undefined
202203

203204
let toolCallCounter = 0
205+
let hasContent = false
206+
let hasReasoning = false
204207

205208
for await (const chunk of result) {
206209
// Track the final structured response (per SDK pattern: candidate.finishReason)
207210
if (chunk.candidates && chunk.candidates[0]?.finishReason) {
208211
finalResponse = chunk as { responseId?: string }
212+
finishReason = chunk.candidates[0].finishReason
209213
}
210214
// Process candidates and their parts to separate thoughts from content
211215
if (chunk.candidates && chunk.candidates.length > 0) {
@@ -233,9 +237,11 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
233237
if (part.thought) {
234238
// This is a thinking/reasoning part
235239
if (part.text) {
240+
hasReasoning = true
236241
yield { type: "reasoning", text: part.text }
237242
}
238243
} else if (part.functionCall) {
244+
hasContent = true
239245
// Gemini sends complete function calls in a single chunk
240246
// Emit as partial chunks for consistent handling with NativeToolCallParser
241247
const callId = `${part.functionCall.name}-${toolCallCounter}`
@@ -263,6 +269,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
263269
} else {
264270
// This is regular content
265271
if (part.text) {
272+
hasContent = true
266273
yield { type: "text", text: part.text }
267274
}
268275
}
@@ -272,6 +279,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
272279

273280
// Fallback to the original text property if no candidates structure
274281
else if (chunk.text) {
282+
hasContent = true
275283
yield { type: "text", text: chunk.text }
276284
}
277285

@@ -280,6 +288,21 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
280288
}
281289
}
282290

291+
// If we had reasoning but no content, emit a placeholder text to prevent "Empty assistant response" errors.
292+
// This typically happens when the model hits max output tokens while reasoning.
293+
if (hasReasoning && !hasContent) {
294+
let message = t("common:errors.gemini.thinking_complete_no_output")
295+
if (finishReason === "MAX_TOKENS") {
296+
message = t("common:errors.gemini.thinking_complete_truncated")
297+
} else if (finishReason === "SAFETY") {
298+
message = t("common:errors.gemini.thinking_complete_safety")
299+
} else if (finishReason === "RECITATION") {
300+
message = t("common:errors.gemini.thinking_complete_recitation")
301+
}
302+
303+
yield { type: "text", text: message }
304+
}
305+
283306
if (finalResponse?.responseId) {
284307
// Capture responseId so Task.addToApiConversationHistory can store it
285308
// alongside the assistant message in api_history.json.

src/api/transform/gemini-format.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ type ReasoningContentBlock = {
1414
type ExtendedContentBlockParam = Anthropic.ContentBlockParam | ThoughtSignatureContentBlock | ReasoningContentBlock
1515
type ExtendedAnthropicContent = string | ExtendedContentBlockParam[]
1616

17+
// Extension type to safely add thoughtSignature to Part
18+
type PartWithThoughtSignature = Part & {
19+
thoughtSignature?: string
20+
}
21+
1722
function isThoughtSignatureContentBlock(block: ExtendedContentBlockParam): block is ThoughtSignatureContentBlock {
1823
return block.type === "thoughtSignature"
1924
}
@@ -47,16 +52,11 @@ export function convertAnthropicContentToGemini(
4752
return [{ text: content }]
4853
}
4954

50-
return content.flatMap((block): Part | Part[] => {
55+
const parts = content.flatMap((block): Part | Part[] => {
5156
// Handle thoughtSignature blocks first
5257
if (isThoughtSignatureContentBlock(block)) {
53-
if (includeThoughtSignatures && typeof block.thoughtSignature === "string") {
54-
// The Google GenAI SDK currently exposes thoughtSignature as an
55-
// extension field on Part; model it structurally without widening
56-
// the upstream type.
57-
return { thoughtSignature: block.thoughtSignature } as Part
58-
}
59-
// Explicitly omit thoughtSignature when not including it.
58+
// We process thought signatures globally and attach them to the relevant parts
59+
// or create a placeholder part if no other content exists.
6060
return []
6161
}
6262

@@ -135,6 +135,25 @@ export function convertAnthropicContentToGemini(
135135
return []
136136
}
137137
})
138+
139+
// Post-processing: Ensure thought signature is attached if required
140+
if (includeThoughtSignatures && activeThoughtSignature) {
141+
const hasSignature = parts.some((p) => "thoughtSignature" in p)
142+
143+
if (!hasSignature) {
144+
if (parts.length > 0) {
145+
// Attach to the first part (usually text)
146+
// We use the intersection type to allow adding the property safely
147+
;(parts[0] as PartWithThoughtSignature).thoughtSignature = activeThoughtSignature
148+
} else {
149+
// Create a placeholder part if no other content exists
150+
const placeholder: PartWithThoughtSignature = { text: "", thoughtSignature: activeThoughtSignature }
151+
parts.push(placeholder)
152+
}
153+
}
154+
}
155+
156+
return parts
138157
}
139158

140159
export function convertAnthropicMessageToGemini(

src/i18n/locales/ca/common.json

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/locales/de/common.json

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/locales/en/common.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,11 @@
106106
"gemini": {
107107
"generate_stream": "Gemini generate context stream error: {{error}}",
108108
"generate_complete_prompt": "Gemini completion error: {{error}}",
109-
"sources": "Sources:"
109+
"sources": "Sources:",
110+
"thinking_complete_no_output": "(Thinking complete, but no output was generated.)",
111+
"thinking_complete_truncated": "(Thinking complete, but output was truncated due to token limit.)",
112+
"thinking_complete_safety": "(Thinking complete, but output was blocked due to safety settings.)",
113+
"thinking_complete_recitation": "(Thinking complete, but output was blocked due to recitation check.)"
110114
},
111115
"cerebras": {
112116
"authenticationFailed": "Cerebras API authentication failed. Please check your API key is valid and not expired.",

src/i18n/locales/es/common.json

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/locales/fr/common.json

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/locales/hi/common.json

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/locales/id/common.json

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n/locales/it/common.json

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)