@@ -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 = "(Thinking complete, but no output was generated.)"
295+ if ( finishReason === "MAX_TOKENS" ) {
296+ message = "(Thinking complete, but output was truncated due to token limit.)"
297+ } else if ( finishReason === "SAFETY" ) {
298+ message = "(Thinking complete, but output was blocked due to safety settings.)"
299+ } else if ( finishReason === "RECITATION" ) {
300+ message = "(Thinking complete, but output was blocked due to recitation check.)"
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.
0 commit comments