{"record":{"id":"5b8a0f3c89db5368","repo":"nexu-io/open-design","slug":"nano-banana-image-response-missing-candidates-co","errorCode":null,"errorMessage":"nano-banana image response missing candidates[].content.parts[].inlineData.data","messagePattern":"nano-banana image response missing candidates\\[\\]\\.content\\.parts\\[\\]\\.inlineData\\.data","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/media/index.ts","lineNumber":1769,"sourceCode":"    || aspect === '3:4'\n  ) {\n    return aspect;\n  }\n  return '1:1';\n}\n\nfunction inlineImageBytesFromGenerateContent(data: any): Buffer {\n  const candidates = Array.isArray(data?.candidates) ? data.candidates : [];\n  for (const candidate of candidates) {\n    const parts = Array.isArray(candidate?.content?.parts) ? candidate.content.parts : [];\n    for (const part of parts) {\n      const inline = part?.inlineData;\n      if (typeof inline?.data === 'string' && inline.data) {\n        return Buffer.from(inline.data, 'base64');\n      }\n    }\n  }\n  throw new Error('nano-banana image response missing candidates[].content.parts[].inlineData.data');\n}\n\nfunction sniffImageExt(bytes: Buffer): string {\n  if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {\n    return '.jpg';\n  }\n  if (\n    bytes.length >= 8\n    && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47\n  ) {\n    return '.png';\n  }\n  if (\n    bytes.length >= 12\n    && bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46\n    && bytes[8] === 0x57 && bytes[9] === 0x45 && bytes[10] === 0x42 && bytes[11] === 0x50\n  ) {\n    return '.webp';","sourceCodeStart":1751,"sourceCodeEnd":1787,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/media/index.ts#L1751-L1787","documentation":"Thrown by inlineImageBytesFromGenerateContent when the parsed Gemini response has no candidate whose content.parts contain an `inlineData.data` base64 string. The helper walks candidates→content→parts→inlineData.data and only succeeds if it finds a non-empty string; an empty candidates array, a finishReason-filtered response, or a prompt-blocked response all reach this throw.","triggerScenarios":"Gemini returned `candidates:[]` (nothing generated), `promptFeedback.blockReason` set (safety block with no candidate), candidates whose parts contain only text (a textual refusal), or an empty `inlineData.data` field.","commonSituations":"Prompt tripped Gemini safety filters; the model variant selected is the text-only flash and not the image flash; the key lacks the image generation scope; the response was truncated by maxOutputTokens before the image landed.","solutions":["Inspect the full Gemini response — look at promptFeedback.blockReason and candidates[].finishReason first.","If blocked, reword the prompt to avoid flagged categories and retry.","Confirm ctx.wireModel resolves to the image-capable Gemini slug (gemini-2.5-flash-image family), not the text variant.","If finishReason is MAX_TOKENS, ensure the request body does not cap outputTokens below image-payload size."],"exampleFix":"// before\nthrow new Error('nano-banana image response missing candidates[].content.parts[].inlineData.data');\n\n// after — surface why nothing came back\nconst block = data?.promptFeedback?.blockReason;\nconst reasons = (data?.candidates || []).map(c => c?.finishReason).join(',');\nthrow new Error(\n  `nano-banana image produced no inlineData (block=${block || 'none'}, finishReasons=[${reasons || 'none'}])`,\n);","handlingStrategy":"type-guard","validationCode":"// Inspect block/finish reasons before declaring 'missing inlineData'\nfunction extractGeminiImageBytes(data: any): Buffer {\n  const candidates = Array.isArray(data?.candidates) ? data.candidates : [];\n  for (const c of candidates) {\n    for (const p of c?.content?.parts || []) {\n      if (typeof p?.inlineData?.data === 'string' && p.inlineData.data) {\n        return Buffer.from(p.inlineData.data, 'base64');\n      }\n    }\n  }\n  const block = data?.promptFeedback?.blockReason;\n  const finish = candidates.map((c: any) => c?.finishReason).join(',') || 'none';\n  throw new Error(`nano-banana produced no image (block=${block || 'none'}, finish=[${finish}])`);\n}","typeGuard":"interface GeminiImagePart { inlineData?: { data?: string } }\nfunction hasGeminiInlineImage(data: any): boolean {\n  return Array.isArray(data?.candidates) && data.candidates.some((c: any) =>\n    Array.isArray(c?.content?.parts) && c.content.parts.some((p: GeminiImagePart) =>\n      typeof p?.inlineData?.data === 'string' && p.inlineData.data.length > 0));\n}","tryCatchPattern":"try {\n  bytes = extractGeminiImageBytes(data);\n} catch (e) {\n  if (/block=/.test(String(e))) { /* content filter — surface to user as 'prompt blocked' */ }\n  throw e;\n}","preventionTips":["Check promptFeedback.blockReason and candidate finishReason before declaring missing image.","Confirm the model slug is the image-capable Gemini variant.","Ensure request maxOutputTokens is high enough to fit the image payload.","Reword prompts that trip safety filters instead of retrying unchanged."],"tags":["nano-banana","gemini","image-generation","response-shape","content-moderation","provider-api"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}