{"record":{"id":"54e472b21773e76a","repo":"odysseus-dev/odysseus","slug":"openai-edit-failed","errorCode":null,"errorMessage":"OpenAI edit failed","messagePattern":"OpenAI edit failed","errorType":"http","errorClass":"HTTPException","httpStatus":null,"severity":"error","filePath":"routes/gallery/gallery_routes.py","lineNumber":1380,"sourceCode":"            }\n            # Honor explicit model selection from the editor; fall back to gpt-image-1.\n            # dall-e-3 has no edit endpoint — refuse it loudly so the user picks again.\n            oa_model = chosen_model or \"gpt-image-1\"\n            if \"dall-e-3\" in oa_model:\n                raise HTTPException(400, \"dall-e-3 doesn't support image edits — pick gpt-image-1 or dall-e-2\")\n            data = {\n                \"model\": oa_model,\n                \"prompt\": body.get(\"prompt\", \"\"),\n                \"size\": size,\n                \"n\": \"1\",\n            }\n            headers = {\"Authorization\": f\"Bearer {api_key}\"}\n            try:\n                async with httpx.AsyncClient(timeout=120) as client:\n                    r = await client.post(_join_checked_gallery_endpoint(base, \"/images/edits\"), headers=headers, data=data, files=files)\n                    if r.status_code != 200:\n                        logger.error(\"inpaint_proxy OpenAI edit: status %s\", r.status_code)\n                        raise HTTPException(r.status_code, \"OpenAI edit failed\")\n                    result = r.json()\n                    raw_b64 = None\n                    if result.get(\"data\"):\n                        item = result[\"data\"][0]\n                        # gpt-image-1 returns b64_json by default; dall-e-2 may return url\n                        if item.get(\"b64_json\"):\n                            raw_b64 = item[\"b64_json\"]\n                        elif item.get(\"url\"):\n                            raw_b64 = await _fetch_result_image_b64(item[\"url\"])\n                    if not raw_b64:\n                        raise HTTPException(502, \"OpenAI returned no image\")\n\n                    # OpenAI's edits API doesn't truly preserve unmasked\n                    # pixels — gpt-image-1 regenerates the whole image,\n                    # so even areas the user didn't mask come back\n                    # slightly different. Composite the model output onto\n                    # the ORIGINAL source using the user's mask, so only\n                    # the masked region actually changes.","sourceCodeStart":1362,"sourceCodeEnd":1398,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/gallery/gallery_routes.py#L1362-L1398","documentation":"Raised inside a FastAPI proxy route when the upstream OpenAI-compatible /v1/images/edits endpoint answers with a non-200 status. The route forwards the upstream status code verbatim as the HTTPException status, with the generic detail 'OpenAI edit failed'. It means the multipart edit request reached the server but was rejected or failed server-side.","triggerScenarios":"POST to the gallery inpaint proxy (inpaint_proxy) that takes the OpenAI path: a valid api_key endpoint base ending in /v1, image+mask files assembled, and the POST to {base}/images/edits returning e.g. 401 (bad key), 400 (malformed image/mask or bad model name), 429 (rate limit), or 5xx.","commonSituations":"Expired or revoked OpenAI API key stored in the endpoint DB row; model name not available to the account (e.g. gpt-image-1 not entitled); mask PNG not the same dimensions as the image; quota exhaustion returning 429; pointing the endpoint at a non-OpenAI server that returns errors on /images/edits.","solutions":["Check the server log line 'inpaint_proxy OpenAI edit: status %s' to get the exact upstream code, then fix accordingly (401 → rotate API key, 429 → quota, 400 → payload/model).","Re-verify the configured image endpoint's base_url and api_key in the gallery endpoint settings (must be a valid OpenAI-compatible base ending before /v1).","Confirm the model chosen (oa_model) is one the upstream account can use for image edits.","If upstream is self-hosted (not OpenAI), ensure it actually implements /v1/images/edits or let the route take the diffusion-server path instead."],"exampleFix":"// before\nif r.status_code != 200:\n    logger.error(\"inpaint_proxy OpenAI edit: status %s\", r.status_code)\n    raise HTTPException(r.status_code, \"OpenAI edit failed\")\n\n// after — surface the upstream detail so the client sees the real cause\nif r.status_code != 200:\n    detail = \"OpenAI edit failed\"\n    try:\n        detail = r.json().get(\"error\", {}).get(\"message\", detail)\n    except Exception:\n        pass\n    logger.error(\"inpaint_proxy OpenAI edit: status %s %s\", r.status_code, detail)\n    raise HTTPException(r.status_code, detail)","handlingStrategy":"try-catch","validationCode":"import httpx\n# Smoke-test credentials/model before launching the proxy call\nasync def endpoint_ready(base: str, api_key: str, model: str) -> bool:\n    try:\n        async with httpx.AsyncClient(timeout=15) as c:\n            r = await c.get(f\"{base}/models\", headers={\"Authorization\": f\"Bearer {api_key}\"})\n            if r.status_code != 200:\n                return False\n            return any(m.get(\"id\") == model for m in r.json().get(\"data\", []))\n    except httpx.HTTPError:\n        return False","typeGuard":null,"tryCatchPattern":"try:\n    resp = await client.post(\"/api/image/inpaint\", json=payload)\n    resp.raise_for_status()\nexcept httpx.HTTPStatusError as e:\n    detail = e.response.json().get(\"detail\", \"\")\n    if \"OpenAI edit failed\" in str(detail):\n        # inspect server log for the upstream status; 401/429 need user action\n        show_error(f\"Upstream image edit rejected ({e.response.status_code}): {detail}\")\n    else:\n        raise","preventionTips":["Keep the image endpoint's api_key and base_url current in the endpoint settings.","Log and surface upstream error bodies instead of the generic message when debugging.","Prefer b64_json response_format so no second URL fetch is needed."],"tags":["openai","httpx","proxy","image-edit","upstream-error"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}