{"record":{"id":"affcf916737e5bbe","repo":"invoke-ai/InvokeAI","slug":"gemini-response-payload-missing-candidates","errorCode":null,"errorMessage":"Gemini response payload missing candidates","messagePattern":"Gemini response payload missing candidates","errorType":"exception","errorClass":"ExternalProviderRequestError","httpStatus":null,"severity":"error","filePath":"invokeai/app/services/external_generation/providers/gemini.py","lineNumber":132,"sourceCode":"            if response.status_code == 429:\n                retry_after = _parse_retry_after(response.headers.get(\"retry-after\"))\n                raise ExternalProviderRateLimitError(\n                    f\"Gemini rate limit exceeded. {f'Retry after {retry_after:.0f}s.' if retry_after else 'Please try again later.'}\",\n                    retry_after=retry_after,\n                )\n            raise ExternalProviderRequestError(\n                f\"Gemini request failed with status {response.status_code} for model '{model_id}': {response.text}\"\n            )\n\n        data = response.json()\n        if not isinstance(data, dict):\n            raise ExternalProviderRequestError(\"Gemini response payload was not a JSON object\")\n        images: list[ExternalGeneratedImage] = []\n        text_parts: list[str] = []\n        finish_messages: list[str] = []\n        candidates = data.get(\"candidates\")\n        if not isinstance(candidates, list):\n            raise ExternalProviderRequestError(\"Gemini response payload missing candidates\")\n        for candidate in candidates:\n            if not isinstance(candidate, dict):\n                continue\n            finish_message = candidate.get(\"finishMessage\")\n            finish_reason = candidate.get(\"finishReason\")\n            if isinstance(finish_message, str):\n                finish_messages.append(finish_message)\n            elif isinstance(finish_reason, str):\n                finish_messages.append(f\"Finish reason: {finish_reason}\")\n            for part in _iter_response_parts(candidate):\n                inline_data = part.get(\"inline_data\") or part.get(\"inlineData\")\n                if isinstance(inline_data, dict):\n                    encoded = inline_data.get(\"data\")\n                    if encoded:\n                        image = decode_image_base64(encoded)\n                        images.append(ExternalGeneratedImage(image=image, seed=request.seed))\n                        continue\n                file_data = part.get(\"fileData\") or part.get(\"file_data\")","sourceCodeStart":114,"sourceCodeEnd":150,"githubUrl":"https://github.com/invoke-ai/InvokeAI/blob/0b6a024f2ff6a86bfb953dcdb9cc504ef7397a06/invokeai/app/services/external_generation/providers/gemini.py#L114-L150","documentation":"ExternalProviderRequestError raised when the parsed Gemini JSON object has no 'candidates' key or its value is not a list. A successful generateContent response must contain a candidates array; its absence means the request was rejected at the model level (e.g. safety blocking with only promptFeedback present) or the response shape is wrong. The provider cannot extract any images without candidates.","triggerScenarios":"data.get('candidates') is missing or not a list despite HTTP 200 — typically when Google returns {\"promptFeedback\": {\"blockReason\": \"SAFETY\"}} for a blocked prompt, an empty object from a broken proxy, or a MODEL_NOT_FOUND-style JSON error body returned with a 200 status by a gateway.","commonSituations":"Prompt (or init/reference images) tripping Gemini's safety filters; API key lacking access to the requested image model; content-filtered prompts in regions/policies; proxy stripping the real response.","solutions":["Inspect the full response logged by the provider (it logs data on the no-images path) — look for promptFeedback.blockReason.","Rephrase the prompt to avoid content that triggers safety filters; remove problematic reference images.","Verify the API key has access to the requested Gemini image model and billing is enabled.","If a proxy is in play, bypass it or fix its passthrough of the Gemini response."],"exampleFix":"// before: prompt likely to be blocked\nprompt = \"...violent/NSFW wording...\"\n// after: sanitised prompt less likely to be safety-blocked\nprompt = sanitize(prompt)  # strip flagged terms\nresult = provider.generate(request)","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"def has_candidates(data: object) -> bool:\n    return isinstance(data, dict) and isinstance(data.get(\"candidates\"), list) and bool(data[\"candidates\"])\n# a blocked prompt yields {\"promptFeedback\": {\"blockReason\": ...}} with no candidates","tryCatchPattern":"try:\n    result = provider.generate(request)\nexcept ExternalProviderRequestError as e:\n    if \"missing candidates\" in str(e):\n        notify_user(\"Prompt or images were likely blocked by Gemini safety filters; rephrase and retry.\")\n    else:\n        raise","preventionTips":["Pre-screen prompts and reference images for content likely to trip safety filters.","Treat missing candidates as a safety/permission signal, not a transient failure — don't blind-retry.","Ensure the API key has access (and billing) for the requested image model.","Log the full response (as the provider does) to read promptFeedback.blockReason."],"tags":["gemini","response-shape","safety-filter","empty-response"],"backgroundTag":"empty-or-blocked-response","analyzedSha":"0b6a024f2ff6a86bfb953dcdb9cc504ef7397a06","analyzedAt":"2026-08-29T04:46:49.967Z","schemaVersion":2},"datasetVersion":"2026-08-29T07:17:48.351Z"}