{"record":{"id":"b452bb68f3ec35dc","repo":"nexu-io/open-design","slug":"leonardo-ai-response-missing-generationid","errorCode":null,"errorMessage":"leonardo.ai response missing generationId","messagePattern":"leonardo\\.ai response missing generationId","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/media/index.ts","lineNumber":2246,"sourceCode":"    },\n    body: JSON.stringify(body),\n  }));\n  \n  const submitText = await submitResp.text();\n  if (!submitResp.ok) {\n    throw new Error(`leonardo.ai submit ${submitResp.status}: ${truncate(submitText, 240)}`);\n  }\n  \n  let submitData: any;\n  try {\n    submitData = JSON.parse(submitText);\n  } catch {\n    throw new Error(`leonardo.ai non-JSON: ${truncate(submitText, 200)}`);\n  }\n  \n  const generationId = submitData?.sdGenerationJob?.generationId;\n  if (!generationId) {\n    throw new Error('leonardo.ai response missing generationId');\n  }\n  \n  // Poll for completion\n  const maxPollMs = 120000; // 2 minutes\n  const pollIntervalMs = 2000; // 2 seconds\n  const startedAt = Date.now();\n  let imageUrl: string | null = null;\n  \n  while (Date.now() - startedAt < maxPollMs) {\n    await new Promise(resolve => setTimeout(resolve, pollIntervalMs));\n    \n    const pollResp = await fetch(`${baseUrl}/generations/${generationId}`, withMediaRequestInit(ctx, {\n      headers: {\n        'authorization': `Bearer ${credentials.apiKey}`,\n      },\n    }));\n    \n    if (!pollResp.ok) {","sourceCodeStart":2228,"sourceCodeEnd":2264,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/media/index.ts#L2228-L2264","documentation":"Thrown when the Leonardo submit response parsed as JSON but the expected field submitData.sdGenerationJob.generationId is missing/falsy. Leonardo's documented contract is { sdGenerationJob: { generationId } }; a 2xx response without that path means the API shape changed or an unexpected success envelope was returned. The message is a fixed string (no interpolated values), so debugging requires capturing the raw response.","triggerScenarios":"Leonardo returns 2xx JSON without sdGenerationJob.generationId — e.g. a different envelope ({ generationId } at root, or { sdGenerationJob: { generationId: null } }), or an empty success object. The current code can't surface the body in the message.","commonSituations":"Leonardo ships an API version bump that changes the response shape; the configured baseUrl points at a different Leonardo API version; Leonardo returns a soft-success that is actually an error (e.g. validation passed-through as 200).","solutions":["Reproduce the call with curl against {baseUrl}/generations to inspect the actual response envelope and compare to Leonardo's current API docs.","If Leonardo changed the shape, update the extraction path in apps/daemon/src/media/index.ts (line ~2246) to match the new contract.","Confirm the configured baseUrl matches the Leonardo REST API version documented for your account; an older/newer version can return a different envelope.","If the response is genuinely malformed, file a Leonardo support ticket with the request body."],"exampleFix":"// before\nconst generationId = submitData?.sdGenerationJob?.generationId;\nif (!generationId) {\n  throw new Error('leonardo.ai response missing generationId');\n}\n\n// after — surface body for diagnosis + accept alternate shapes\nconst generationId =\n  submitData?.sdGenerationJob?.generationId\n  ?? submitData?.generationId\n  ?? submitData?.id;\nif (!generationId) {\n  throw new Error(`leonardo.ai response missing generationId: ${truncate(JSON.stringify(submitData), 240)}`);\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"// Narrow the parsed Leonardo submit response so missing generationId is caught\n// at the boundary with a typed error instead of a stringy runtime throw.\ninterface LeonardoSubmitOk { sdGenerationJob: { generationId: string } }\nfunction isLeonardoSubmitOk(d: unknown): d is LeonardoSubmitOk {\n  return !!d\n    && typeof d === 'object'\n    && typeof (d as any)?.sdGenerationJob?.generationId === 'string'\n    && (d as any).sdGenerationJob.generationId.length > 0;\n}","tryCatchPattern":"let submitData: any;\ntry { submitData = JSON.parse(submitText); } catch { throw new Error(`leonardo.ai non-JSON: ${truncate(submitText, 200)}`); }\nif (!isLeonardoSubmitOk(submitData)) {\n  // Surface the body so the next dev can see what shape Leonardo actually returned.\n  throw new Error(`leonardo.ai response missing generationId: ${truncate(JSON.stringify(submitData), 240)}`);\n}\nconst generationId = submitData.sdGenerationJob.generationId;","preventionTips":["Pin the Leonardo REST API version in baseUrl and document which version the parser was written against.","When Leonardo ships API changes, update the type guard and parser together; never let one drift.","Log the raw submit body on shape mismatch (truncated) so future regressions are debuggable from one occurrence."],"tags":["leonardo","api-contract","response-shape","parse-error"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}