|
|
@@ -199,13 +199,17 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|
|
let lastUsageMetadata: GenerateContentResponseUsageMetadata | undefined
|
|
|
let pendingGroundingMetadata: GroundingMetadata | undefined
|
|
|
let finalResponse: { responseId?: string } | undefined
|
|
|
+ let finishReason: string | undefined
|
|
|
|
|
|
let toolCallCounter = 0
|
|
|
+ let hasContent = false
|
|
|
+ let hasReasoning = false
|
|
|
|
|
|
for await (const chunk of result) {
|
|
|
// Track the final structured response (per SDK pattern: candidate.finishReason)
|
|
|
if (chunk.candidates && chunk.candidates[0]?.finishReason) {
|
|
|
finalResponse = chunk as { responseId?: string }
|
|
|
+ finishReason = chunk.candidates[0].finishReason
|
|
|
}
|
|
|
// Process candidates and their parts to separate thoughts from content
|
|
|
if (chunk.candidates && chunk.candidates.length > 0) {
|
|
|
@@ -233,9 +237,11 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|
|
if (part.thought) {
|
|
|
// This is a thinking/reasoning part
|
|
|
if (part.text) {
|
|
|
+ hasReasoning = true
|
|
|
yield { type: "reasoning", text: part.text }
|
|
|
}
|
|
|
} else if (part.functionCall) {
|
|
|
+ hasContent = true
|
|
|
// Gemini sends complete function calls in a single chunk
|
|
|
// Emit as partial chunks for consistent handling with NativeToolCallParser
|
|
|
const callId = `${part.functionCall.name}-${toolCallCounter}`
|
|
|
@@ -263,6 +269,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|
|
} else {
|
|
|
// This is regular content
|
|
|
if (part.text) {
|
|
|
+ hasContent = true
|
|
|
yield { type: "text", text: part.text }
|
|
|
}
|
|
|
}
|
|
|
@@ -272,6 +279,7 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|
|
|
|
|
// Fallback to the original text property if no candidates structure
|
|
|
else if (chunk.text) {
|
|
|
+ hasContent = true
|
|
|
yield { type: "text", text: chunk.text }
|
|
|
}
|
|
|
|
|
|
@@ -280,6 +288,21 @@ export class GeminiHandler extends BaseProvider implements SingleCompletionHandl
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // If we had reasoning but no content, emit a placeholder text to prevent "Empty assistant response" errors.
|
|
|
+ // This typically happens when the model hits max output tokens while reasoning.
|
|
|
+ if (hasReasoning && !hasContent) {
|
|
|
+ let message = t("common:errors.gemini.thinking_complete_no_output")
|
|
|
+ if (finishReason === "MAX_TOKENS") {
|
|
|
+ message = t("common:errors.gemini.thinking_complete_truncated")
|
|
|
+ } else if (finishReason === "SAFETY") {
|
|
|
+ message = t("common:errors.gemini.thinking_complete_safety")
|
|
|
+ } else if (finishReason === "RECITATION") {
|
|
|
+ message = t("common:errors.gemini.thinking_complete_recitation")
|
|
|
+ }
|
|
|
+
|
|
|
+ yield { type: "text", text: message }
|
|
|
+ }
|
|
|
+
|
|
|
if (finalResponse?.responseId) {
|
|
|
// Capture responseId so Task.addToApiConversationHistory can store it
|
|
|
// alongside the assistant message in api_history.json.
|