Bläddra i källkod

fix: handle empty Gemini responses and reasoning loops (#10007)

Hannes Rudolph 2 veckor sedan
förälder
incheckning
47320dca62

+ 23 - 0
src/api/providers/gemini.ts

@@ -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.

+ 27 - 8
src/api/transform/gemini-format.ts

@@ -14,6 +14,11 @@ type ReasoningContentBlock = {
 type ExtendedContentBlockParam = Anthropic.ContentBlockParam | ThoughtSignatureContentBlock | ReasoningContentBlock
 type ExtendedAnthropicContent = string | ExtendedContentBlockParam[]
 
+// Extension type to safely add thoughtSignature to Part
+type PartWithThoughtSignature = Part & {
+	thoughtSignature?: string
+}
+
 function isThoughtSignatureContentBlock(block: ExtendedContentBlockParam): block is ThoughtSignatureContentBlock {
 	return block.type === "thoughtSignature"
 }
@@ -47,16 +52,11 @@ export function convertAnthropicContentToGemini(
 		return [{ text: content }]
 	}
 
-	return content.flatMap((block): Part | Part[] => {
+	const parts = content.flatMap((block): Part | Part[] => {
 		// Handle thoughtSignature blocks first
 		if (isThoughtSignatureContentBlock(block)) {
-			if (includeThoughtSignatures && typeof block.thoughtSignature === "string") {
-				// The Google GenAI SDK currently exposes thoughtSignature as an
-				// extension field on Part; model it structurally without widening
-				// the upstream type.
-				return { thoughtSignature: block.thoughtSignature } as Part
-			}
-			// Explicitly omit thoughtSignature when not including it.
+			// We process thought signatures globally and attach them to the relevant parts
+			// or create a placeholder part if no other content exists.
 			return []
 		}
 
@@ -135,6 +135,25 @@ export function convertAnthropicContentToGemini(
 				return []
 		}
 	})
+
+	// Post-processing: Ensure thought signature is attached if required
+	if (includeThoughtSignatures && activeThoughtSignature) {
+		const hasSignature = parts.some((p) => "thoughtSignature" in p)
+
+		if (!hasSignature) {
+			if (parts.length > 0) {
+				// Attach to the first part (usually text)
+				// We use the intersection type to allow adding the property safely
+				;(parts[0] as PartWithThoughtSignature).thoughtSignature = activeThoughtSignature
+			} else {
+				// Create a placeholder part if no other content exists
+				const placeholder: PartWithThoughtSignature = { text: "", thoughtSignature: activeThoughtSignature }
+				parts.push(placeholder)
+			}
+		}
+	}
+
+	return parts
 }
 
 export function convertAnthropicMessageToGemini(

+ 5 - 1
src/i18n/locales/ca/common.json

@@ -109,7 +109,11 @@
 		"gemini": {
 			"generate_stream": "Error del flux de context de generació de Gemini: {{error}}",
 			"generate_complete_prompt": "Error de finalització de Gemini: {{error}}",
-			"sources": "Fonts:"
+			"sources": "Fonts:",
+			"thinking_complete_no_output": "(Pensament completat, però no s'ha generat cap sortida.)",
+			"thinking_complete_truncated": "(Pensament completat, però la sortida s'ha truncat a causa del límit de fitxes.)",
+			"thinking_complete_safety": "(Pensament completat, però la sortida s'ha bloquejat a causa de la configuració de seguretat.)",
+			"thinking_complete_recitation": "(Pensament completat, però la sortida s'ha bloquejat a causa de la comprovació de recitació.)"
 		},
 		"cerebras": {
 			"authenticationFailed": "Ha fallat l'autenticació de l'API de Cerebras. Comproveu que la vostra clau d'API sigui vàlida i no hagi caducat.",

+ 5 - 1
src/i18n/locales/de/common.json

@@ -106,7 +106,11 @@
 		"gemini": {
 			"generate_stream": "Fehler beim Generieren des Kontext-Streams von Gemini: {{error}}",
 			"generate_complete_prompt": "Fehler bei der Vervollständigung durch Gemini: {{error}}",
-			"sources": "Quellen:"
+			"sources": "Quellen:",
+			"thinking_complete_no_output": "(Denken abgeschlossen, aber es wurde keine Ausgabe generiert.)",
+			"thinking_complete_truncated": "(Denken abgeschlossen, aber die Ausgabe wurde aufgrund des Token-Limits gekürzt.)",
+			"thinking_complete_safety": "(Denken abgeschlossen, aber die Ausgabe wurde aufgrund von Sicherheitseinstellungen blockiert.)",
+			"thinking_complete_recitation": "(Denken abgeschlossen, aber die Ausgabe wurde aufgrund der Rezitationsprüfung blockiert.)"
 		},
 		"cerebras": {
 			"authenticationFailed": "Cerebras API-Authentifizierung fehlgeschlagen. Bitte überprüfe, ob dein API-Schlüssel gültig und nicht abgelaufen ist.",

+ 5 - 1
src/i18n/locales/en/common.json

@@ -106,7 +106,11 @@
 		"gemini": {
 			"generate_stream": "Gemini generate context stream error: {{error}}",
 			"generate_complete_prompt": "Gemini completion error: {{error}}",
-			"sources": "Sources:"
+			"sources": "Sources:",
+			"thinking_complete_no_output": "(Thinking complete, but no output was generated.)",
+			"thinking_complete_truncated": "(Thinking complete, but output was truncated due to token limit.)",
+			"thinking_complete_safety": "(Thinking complete, but output was blocked due to safety settings.)",
+			"thinking_complete_recitation": "(Thinking complete, but output was blocked due to recitation check.)"
 		},
 		"cerebras": {
 			"authenticationFailed": "Cerebras API authentication failed. Please check your API key is valid and not expired.",

+ 5 - 1
src/i18n/locales/es/common.json

@@ -106,7 +106,11 @@
 		"gemini": {
 			"generate_stream": "Error del stream de contexto de generación de Gemini: {{error}}",
 			"generate_complete_prompt": "Error de finalización de Gemini: {{error}}",
-			"sources": "Fuentes:"
+			"sources": "Fuentes:",
+			"thinking_complete_no_output": "(Pensamiento completado, pero no se generó salida.)",
+			"thinking_complete_truncated": "(Pensamiento completado, pero la salida fue truncada debido al límite de tokens.)",
+			"thinking_complete_safety": "(Pensamiento completado, pero la salida fue bloqueada debido a la configuración de seguridad.)",
+			"thinking_complete_recitation": "(Pensamiento completado, pero la salida fue bloqueada debido a la comprobación de recitación.)"
 		},
 		"cerebras": {
 			"authenticationFailed": "Falló la autenticación de la API de Cerebras. Verifica que tu clave de API sea válida y no haya expirado.",

+ 5 - 1
src/i18n/locales/fr/common.json

@@ -106,7 +106,11 @@
 		"gemini": {
 			"generate_stream": "Erreur du flux de contexte de génération Gemini : {{error}}",
 			"generate_complete_prompt": "Erreur d'achèvement de Gemini : {{error}}",
-			"sources": "Sources :"
+			"sources": "Sources :",
+			"thinking_complete_no_output": "(Réflexion terminée, mais aucune sortie générée.)",
+			"thinking_complete_truncated": "(Réflexion terminée, mais la sortie a été tronquée en raison de la limite de jetons.)",
+			"thinking_complete_safety": "(Réflexion terminée, mais la sortie a été bloquée en raison des paramètres de sécurité.)",
+			"thinking_complete_recitation": "(Réflexion terminée, mais la sortie a été bloquée en raison de la vérification de récitation.)"
 		},
 		"cerebras": {
 			"authenticationFailed": "Échec de l'authentification de l'API Cerebras. Vérifiez que votre clé API est valide et n'a pas expiré.",

+ 5 - 1
src/i18n/locales/hi/common.json

@@ -106,7 +106,11 @@
 		"gemini": {
 			"generate_stream": "जेमिनी जनरेट कॉन्टेक्स्ट स्ट्रीम त्रुटि: {{error}}",
 			"generate_complete_prompt": "जेमिनी समापन त्रुटि: {{error}}",
-			"sources": "स्रोत:"
+			"sources": "स्रोत:",
+			"thinking_complete_no_output": "(सोचना पूरा हुआ, लेकिन कोई आउटपुट नहीं बना।)",
+			"thinking_complete_truncated": "(सोचना पूरा हुआ, लेकिन टोकन सीमा के कारण आउटपुट काट दिया गया।)",
+			"thinking_complete_safety": "(सोचना पूरा हुआ, लेकिन सुरक्षा सेटिंग्स के कारण आउटपुट अवरुद्ध कर दिया गया।)",
+			"thinking_complete_recitation": "(सोचना पूरा हुआ, लेकिन पाठ जाँच के कारण आउटपुट अवरुद्ध कर दिया गया।)"
 		},
 		"cerebras": {
 			"authenticationFailed": "Cerebras API प्रमाणीकरण विफल हुआ। कृपया जांचें कि आपकी API कुंजी वैध है और समाप्त नहीं हुई है।",

+ 5 - 1
src/i18n/locales/id/common.json

@@ -106,7 +106,11 @@
 		"gemini": {
 			"generate_stream": "Kesalahan aliran konteks pembuatan Gemini: {{error}}",
 			"generate_complete_prompt": "Kesalahan penyelesaian Gemini: {{error}}",
-			"sources": "Sumber:"
+			"sources": "Sumber:",
+			"thinking_complete_no_output": "(Berpikir selesai, tetapi tidak ada output yang dihasilkan.)",
+			"thinking_complete_truncated": "(Berpikir selesai, tetapi output terpotong karena batas token.)",
+			"thinking_complete_safety": "(Berpikir selesai, tetapi output diblokir karena pengaturan keamanan.)",
+			"thinking_complete_recitation": "(Berpikir selesai, tetapi output diblokir karena pemeriksaan resitasi.)"
 		},
 		"cerebras": {
 			"authenticationFailed": "Autentikasi API Cerebras gagal. Silakan periksa apakah kunci API Anda valid dan belum kedaluwarsa.",

+ 5 - 1
src/i18n/locales/it/common.json

@@ -106,7 +106,11 @@
 		"gemini": {
 			"generate_stream": "Errore del flusso di contesto di generazione Gemini: {{error}}",
 			"generate_complete_prompt": "Errore di completamento Gemini: {{error}}",
-			"sources": "Fonti:"
+			"sources": "Fonti:",
+			"thinking_complete_no_output": "(Pensiero completato, ma non è stato generato alcun output.)",
+			"thinking_complete_truncated": "(Pensiero completato, ma l'output è stato troncato a causa del limite di token.)",
+			"thinking_complete_safety": "(Pensiero completato, ma l'output è stato bloccato a causa delle impostazioni di sicurezza.)",
+			"thinking_complete_recitation": "(Pensiero completato, ma l'output è stato bloccato a causa del controllo di recitazione.)"
 		},
 		"cerebras": {
 			"authenticationFailed": "Autenticazione API Cerebras fallita. Verifica che la tua chiave API sia valida e non scaduta.",

+ 5 - 1
src/i18n/locales/ja/common.json

@@ -106,7 +106,11 @@
 		"gemini": {
 			"generate_stream": "Gemini 生成コンテキスト ストリーム エラー: {{error}}",
 			"generate_complete_prompt": "Gemini 完了エラー: {{error}}",
-			"sources": "ソース:"
+			"sources": "ソース:",
+			"thinking_complete_no_output": "(思考完了、出力なし)",
+			"thinking_complete_truncated": "(思考完了、トークン制限により出力切り捨て)",
+			"thinking_complete_safety": "(思考完了、安全設定により出力ブロック)",
+			"thinking_complete_recitation": "(思考完了、引用チェックにより出力ブロック)"
 		},
 		"cerebras": {
 			"authenticationFailed": "Cerebras API認証が失敗しました。APIキーが有効で期限切れではないことを確認してください。",

+ 5 - 1
src/i18n/locales/ko/common.json

@@ -106,7 +106,11 @@
 		"gemini": {
 			"generate_stream": "Gemini 생성 컨텍스트 스트림 오류: {{error}}",
 			"generate_complete_prompt": "Gemini 완료 오류: {{error}}",
-			"sources": "출처:"
+			"sources": "출처:",
+			"thinking_complete_no_output": "(생각 완료, 출력 없음)",
+			"thinking_complete_truncated": "(생각 완료, 토큰 제한으로 출력 잘림)",
+			"thinking_complete_safety": "(생각 완료, 안전 설정으로 출력 차단됨)",
+			"thinking_complete_recitation": "(생각 완료, 암송 확인으로 출력 차단됨)"
 		},
 		"cerebras": {
 			"authenticationFailed": "Cerebras API 인증에 실패했습니다. API 키가 유효하고 만료되지 않았는지 확인하세요.",

+ 5 - 1
src/i18n/locales/nl/common.json

@@ -106,7 +106,11 @@
 		"gemini": {
 			"generate_stream": "Fout bij het genereren van contextstream door Gemini: {{error}}",
 			"generate_complete_prompt": "Fout bij het voltooien door Gemini: {{error}}",
-			"sources": "Bronnen:"
+			"sources": "Bronnen:",
+			"thinking_complete_no_output": "(Nadenken voltooid, maar er is geen uitvoer gegenereerd.)",
+			"thinking_complete_truncated": "(Nadenken voltooid, maar uitvoer is afgekapt vanwege tokenlimiet.)",
+			"thinking_complete_safety": "(Nadenken voltooid, maar uitvoer is geblokkeerd vanwege veiligheidsinstellingen.)",
+			"thinking_complete_recitation": "(Nadenken voltooid, maar uitvoer is geblokkeerd vanwege recitatiecontrole.)"
 		},
 		"cerebras": {
 			"authenticationFailed": "Cerebras API-authenticatie mislukt. Controleer of je API-sleutel geldig is en niet verlopen.",

+ 5 - 1
src/i18n/locales/pl/common.json

@@ -106,7 +106,11 @@
 		"gemini": {
 			"generate_stream": "Błąd strumienia kontekstu generowania Gemini: {{error}}",
 			"generate_complete_prompt": "Błąd uzupełniania Gemini: {{error}}",
-			"sources": "Źródła:"
+			"sources": "Źródła:",
+			"thinking_complete_no_output": "(Myślenie zakończone, ale nie wygenerowano żadnych danych wyjściowych.)",
+			"thinking_complete_truncated": "(Myślenie zakończone, ale dane wyjściowe zostały obcięte z powodu limitu tokenów.)",
+			"thinking_complete_safety": "(Myślenie zakończone, ale dane wyjściowe zostały zablokowane przez ustawienia bezpieczeństwa.)",
+			"thinking_complete_recitation": "(Myślenie zakończone, ale dane wyjściowe zostały zablokowane przez kontrolę recytacji.)"
 		},
 		"cerebras": {
 			"authenticationFailed": "Uwierzytelnianie API Cerebras nie powiodło się. Sprawdź, czy twój klucz API jest ważny i nie wygasł.",

+ 5 - 1
src/i18n/locales/pt-BR/common.json

@@ -110,7 +110,11 @@
 		"gemini": {
 			"generate_stream": "Erro de fluxo de contexto de geração do Gemini: {{error}}",
 			"generate_complete_prompt": "Erro de conclusão do Gemini: {{error}}",
-			"sources": "Fontes:"
+			"sources": "Fontes:",
+			"thinking_complete_no_output": "(Pensamento concluído, mas nenhuma saída foi gerada.)",
+			"thinking_complete_truncated": "(Pensamento concluído, mas a saída foi truncada devido ao limite de tokens.)",
+			"thinking_complete_safety": "(Pensamento concluído, mas a saída foi bloqueada devido às configurações de segurança.)",
+			"thinking_complete_recitation": "(Pensamento concluído, mas a saída foi bloqueada devido à verificação de recitação.)"
 		},
 		"cerebras": {
 			"authenticationFailed": "Falha na autenticação da API Cerebras. Verifique se sua chave de API é válida e não expirou.",

+ 5 - 1
src/i18n/locales/ru/common.json

@@ -106,7 +106,11 @@
 		"gemini": {
 			"generate_stream": "Ошибка потока контекста генерации Gemini: {{error}}",
 			"generate_complete_prompt": "Ошибка завершения Gemini: {{error}}",
-			"sources": "Источники:"
+			"sources": "Источники:",
+			"thinking_complete_no_output": "(Размышление завершено, но вывод не сгенерирован.)",
+			"thinking_complete_truncated": "(Размышление завершено, но вывод урезан из-за лимита токенов.)",
+			"thinking_complete_safety": "(Размышление завершено, но вывод заблокирован настройками безопасности.)",
+			"thinking_complete_recitation": "(Размышление завершено, но вывод заблокирован проверкой цитирования.)"
 		},
 		"cerebras": {
 			"authenticationFailed": "Ошибка аутентификации Cerebras API. Убедитесь, что ваш API-ключ действителен и не истек.",

+ 5 - 1
src/i18n/locales/tr/common.json

@@ -106,7 +106,11 @@
 		"gemini": {
 			"generate_stream": "Gemini oluşturma bağlam akışı hatası: {{error}}",
 			"generate_complete_prompt": "Gemini tamamlama hatası: {{error}}",
-			"sources": "Kaynaklar:"
+			"sources": "Kaynaklar:",
+			"thinking_complete_no_output": "(Düşünme tamamlandı, ancak çıktı oluşturulmadı.)",
+			"thinking_complete_truncated": "(Düşünme tamamlandı, ancak çıktı jeton sınırı nedeniyle kesildi.)",
+			"thinking_complete_safety": "(Düşünme tamamlandı, ancak çıktı güvenlik ayarları nedeniyle engellendi.)",
+			"thinking_complete_recitation": "(Düşünme tamamlandı, ancak çıktı okuma kontrolü nedeniyle engellendi.)"
 		},
 		"cerebras": {
 			"authenticationFailed": "Cerebras API kimlik doğrulama başarısız oldu. API anahtarınızın geçerli olduğunu ve süresi dolmadığını kontrol edin.",

+ 5 - 1
src/i18n/locales/vi/common.json

@@ -106,7 +106,11 @@
 		"gemini": {
 			"generate_stream": "Lỗi luồng ngữ cảnh tạo Gemini: {{error}}",
 			"generate_complete_prompt": "Lỗi hoàn thành Gemini: {{error}}",
-			"sources": "Nguồn:"
+			"sources": "Nguồn:",
+			"thinking_complete_no_output": "(Đã suy nghĩ xong nhưng không có kết quả đầu ra.)",
+			"thinking_complete_truncated": "(Đã suy nghĩ xong nhưng kết quả bị cắt ngắn do giới hạn token.)",
+			"thinking_complete_safety": "(Đã suy nghĩ xong nhưng kết quả bị chặn do cài đặt an toàn.)",
+			"thinking_complete_recitation": "(Đã suy nghĩ xong nhưng kết quả bị chặn do kiểm tra trích dẫn.)"
 		},
 		"cerebras": {
 			"authenticationFailed": "Xác thực API Cerebras thất bại. Vui lòng kiểm tra khóa API của bạn có hợp lệ và chưa hết hạn.",

+ 5 - 1
src/i18n/locales/zh-CN/common.json

@@ -111,7 +111,11 @@
 		"gemini": {
 			"generate_stream": "Gemini 生成上下文流错误:{{error}}",
 			"generate_complete_prompt": "Gemini 完成错误:{{error}}",
-			"sources": "来源:"
+			"sources": "来源:",
+			"thinking_complete_no_output": "(思考完成,但未生成输出。)",
+			"thinking_complete_truncated": "(思考完成,但由于 Token 限制输出被截断。)",
+			"thinking_complete_safety": "(思考完成,但由于安全设置输出被阻止。)",
+			"thinking_complete_recitation": "(思考完成,但由于引用检查输出被阻止。)"
 		},
 		"cerebras": {
 			"authenticationFailed": "Cerebras API 身份验证失败。请检查你的 API 密钥是否有效且未过期。",

+ 5 - 1
src/i18n/locales/zh-TW/common.json

@@ -105,7 +105,11 @@
 		"gemini": {
 			"generate_stream": "Gemini 產生內容串流錯誤:{{error}}",
 			"generate_complete_prompt": "Gemini 完成錯誤:{{error}}",
-			"sources": "來源:"
+			"sources": "來源:",
+			"thinking_complete_no_output": "(思考完成,但未產生輸出。)",
+			"thinking_complete_truncated": "(思考完成,但由於 Token 限制輸出被截斷。)",
+			"thinking_complete_safety": "(思考完成,但由於安全設定輸出被阻止。)",
+			"thinking_complete_recitation": "(思考完成,但由於引用檢查輸出被阻止。)"
 		},
 		"cerebras": {
 			"authenticationFailed": "Cerebras API 驗證失敗。請檢查您的 API 金鑰是否有效且未過期。",