{"record":{"id":"6fbb8f991a3b8f8e","repo":"nexu-io/open-design","slug":"leonardo-ai-submit-submitresp-status-truncat","errorCode":null,"errorMessage":"leonardo.ai submit ${submitResp.status}: ${truncate(submitText, 240)}","messagePattern":"leonardo\\.ai submit (.+?): (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/media/index.ts","lineNumber":2234,"sourceCode":"    modelId: platformModelId,\n    width: size.width,\n    height: size.height,\n    num_images: 1,\n    ...(requiresContrast ? { contrast: 3.5 } : {}),\n  };\n  \n  const submitResp = await fetch(`${baseUrl}/generations`, withMediaRequestInit(ctx, {\n    method: 'POST',\n    headers: {\n      'authorization': `Bearer ${credentials.apiKey}`,\n      'content-type': 'application/json',\n    },\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();","sourceCodeStart":2216,"sourceCodeEnd":2252,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/media/index.ts#L2216-L2252","documentation":"Thrown when the POST to {baseUrl}/generations on Leonardo.ai returned a non-2xx status. The message includes the HTTP status and the first 240 chars of the response body (truncated via truncate()) so the operator can see Leonardo's error text. This is the upstream-rejection guard for the submit step, distinct from JSON-parse and shape errors that follow.","triggerScenarios":"Leonardo.ai rejects the generation request: invalid/expired API key (401), quota/plan exhausted (402/429), malformed body or unsupported model/dimension combination (400), or Leonardo-side outage (5xx).","commonSituations":"Free-tier plan out of credits; API key revoked; combination of model + dimensions Leonardo rejects (e.g. requesting a size the chosen model doesn't support); transient 5xx during Leonardo maintenance; clock/auth skew after rotating keys.","solutions":["Read the truncated body in the error message — Leonardo usually states the exact reason (out_of_credits, invalid_model, unauthorized).","If 401/403: rotate the LEONARDO_API_KEY (Settings or env) and retry.","If 402/429: check your Leonardo plan/credit balance at cloud.leonardo.ai and top up or wait for quota reset.","If 400: verify the model+aspect combination is supported — some models restrict dimensions; try 1:1 first to isolate.","If 5xx: retry after a short backoff; check status.leonardo.ai."],"exampleFix":"// before — model+aspect combo Leonardo rejects\nbody = { modelId: '...', width: 1344, height: 768 } // → HTTP 400\n\n// after\nbody = { modelId: '...', width: 1024, height: 1024 } // supported 1:1","handlingStrategy":"try-catch","validationCode":"// Pre-validate the request shape against Leonardo's known constraints before\n// spending a network round-trip on a guaranteed 400.\nfunction validateLeonardoSubmitBody(body: Record<string, unknown>, model: string): string[] {\n  const errors: string[] = [];\n  if (!body.modelId) errors.push('modelId required');\n  if (typeof body.width !== 'number' || typeof body.height !== 'number') errors.push('width/height required');\n  if ((body.width as number) > 1024 && model === 'leonardo-flux-schnell') errors.push('flux-schnell max 1024 on long edge');\n  return errors;\n}","typeGuard":null,"tryCatchPattern":"// Classify the submit failure so callers can retry only idempotent classes.\ntry {\n  await renderLeonardoImage(ctx, creds);\n} catch (e) {\n  const msg = String((e as Error).message || e);\n  const m = msg.match(/leonardo\\.ai submit (\\d+)/);\n  if (m) {\n    const status = Number(m[1]);\n    if (status === 429 || status >= 500) { /* transient — safe to retry */ await renderLeonardoImage(ctx, creds); return; }\n    if (status === 401 || status === 403) throw new Error('Leonardo API key invalid — reconfigure in Settings');\n    if (status === 402) throw new Error('Leonardo credits exhausted — top up at cloud.leonardo.ai');\n  }\n  throw e;\n}","preventionTips":["Test new model+aspect combinations in the Leonardo playground before wiring them into production prompts.","Monitor Leonardo credit balance via their API on a schedule and alert before exhaustion.","Keep the API key in a rotation pipeline so 401s surface during planned rotation, not mid-job."],"tags":["leonardo","http-status","upstream-error","api-rejection"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}