{"record":{"id":"0b5c72ab65bc2234","repo":"nexu-io/open-design","slug":"nano-banana-image-resp-status-truncate-text","errorCode":null,"errorMessage":"nano-banana image ${resp.status}: ${truncate(text, 240)}","messagePattern":"nano-banana image (.+?): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/media/index.ts","lineNumber":1708,"sourceCode":"        imageSize: NANOBANANA_DEFAULT_IMAGE_SIZE,\n      },\n    },\n  };\n\n  const resp = await fetchImageGenerationWithResponseRetry(\n    () => fetch(`${baseUrl}/v1beta/models/${encodeURIComponent(wireModel)}:generateContent`, withMediaRequestInit(ctx, {\n      method: 'POST',\n      headers: nanoBananaHeaders(baseUrl, apiKey),\n      body: JSON.stringify(body),\n    })),\n    (summary) => ctx.onProviderRequestSettled?.({\n      providerId: 'nanobanana',\n      ...summary,\n    }),\n  );\n  const text = await resp.text();\n  if (!resp.ok) {\n    throw new Error(`nano-banana image ${resp.status}: ${truncate(text, 240)}`);\n  }\n  let data: any;\n  try {\n    data = JSON.parse(text);\n  } catch {\n    throw new Error(`nano-banana image non-JSON: ${truncate(text, 200)}`);\n  }\n  const bytes = inlineImageBytesFromGenerateContent(data);\n  return {\n    bytes,\n    providerNote: `nano-banana/${wireModel} · ${nanoBananaAspectFor(ctx.aspect)} · ${NANOBANANA_DEFAULT_IMAGE_SIZE} · ${bytes.length} bytes`,\n    suggestedExt: sniffImageExt(bytes),\n  };\n}\n\nfunction nanoBananaHeaders(baseUrl: string, apiKey: string): Record<string, string> {\n  const headers: Record<string, string> = {\n    'content-type': 'application/json',","sourceCodeStart":1690,"sourceCodeEnd":1726,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/media/index.ts#L1690-L1726","documentation":"Thrown by renderNanoBananaImage when the POST to `${baseUrl}/v1beta/models/<model>:generateContent` returns a non-2xx status. The message embeds the HTTP status and the first 240 chars of the body so the operator can see xAI/Google's error detail inline. This is the generic 'the API rejected the request' path.","triggerScenarios":"Invalid/expired API key (401/403), quota exhaustion (429), malformed model id (404), unsupported aspect/size for the model (400), or upstream 5xx. The truncate(text,240) helper caps long HTML error pages so the log stays readable.","commonSituations":"Free-tier AI Studio key hitting rate limits, model name typo (e.g. missing 'flash-image' suffix), key revoked, or Google API outage.","solutions":["Read the embedded status+body in the error: 401/403 → rotate/regenerate the key; 429 → wait and/or upgrade quota; 404 → fix the model id.","Verify the key is valid at aistudio.google.com by issuing a trivial generateContent request.","Confirm credentials.model or ctx.wireModel is a current Gemini image model slug.","If 5xx, retry with backoff or check the Google AI status dashboard."],"exampleFix":"// before\nif (!resp.ok) {\n  throw new Error(`nano-banana image ${resp.status}: ${truncate(text, 240)}`);\n}\n\n// after — classify common failures for clearer operator action\nif (!resp.ok) {\n  const hint = resp.status === 401 || resp.status === 403 ? ' (check API key)'\n    : resp.status === 429 ? ' (quota/rate limit)'\n    : resp.status === 404 ? ' (unknown model slug)'\n    : '';\n  throw new Error(`nano-banana image ${resp.status}${hint}: ${truncate(text, 240)}`);\n}","handlingStrategy":"try-catch","validationCode":"// Map common HTTP statuses to actionable hints before throwing\nfunction describeNanoBananaHttpError(status: number, body: string): string {\n  const hint = status === 401 || status === 403 ? 'check API key'\n    : status === 429 ? 'quota/rate limit reached'\n    : status === 404 ? 'unknown model slug'\n    : status >= 500 ? 'upstream Google AI outage — retry'\n    : 'see body for detail';\n  return `nano-banana image ${status} (${hint}): ${truncate(body, 240)}`;\n}","typeGuard":"function isRetryableNanoBananaStatus(status: number): boolean {\n  return status === 429 || status >= 500;\n}","tryCatchPattern":"let resp: Response;\ntry {\n  resp = await nanoBananaFetch(ctx, credentials);\n} catch (e) {\n  // network error — single retry with backoff\n  await sleep(2000);\n  resp = await nanoBananaFetch(ctx, credentials);\n}\nif (!resp.ok) {\n  if (isRetryableNanoBananaStatus(resp.status)) { /* one retry */ }\n  throw new Error(describeNanoBananaHttpError(resp.status, await resp.text()));\n}","preventionTips":["Classify HTTP statuses into retryable (429, 5xx) vs terminal (401/403/404) and retry accordingly.","Embed status-specific hints in the error so the operator can act without reading the body.","Verify the key works at aistudio.google.com before relying on it in production.","Keep the model slug in credentials.model validated against Google's published list."],"tags":["nano-banana","gemini","image-generation","http-status","credentials","provider-api"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}