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

This commit is contained in:
Hannes Rudolph 2025-12-11 09:55:47 -07:00 committed by GitHub
parent f9cfc66803
commit 47320dca62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 140 additions and 26 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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é.",

View file

@ -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 कुंजी वैध है और समाप्त नहीं हुई है।",

View file

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

View file

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

View file

@ -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キーが有効で期限切れではないことを確認してください。",

View file

@ -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 키가 유효하고 만료되지 않았는지 확인하세요.",

View file

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

View file

@ -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ł.",

View file

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

View file

@ -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-ключ действителен и не истек.",

View file

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

View file

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

View file

@ -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 密钥是否有效且未过期。",

View file

@ -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 金鑰是否有效且未過期。",