Skip to content

fix(images): keep edits URL on retry for openai/azure - #2131

Merged
steebchen merged 1 commit into
mainfrom
openai-image-fix
May 1, 2026
Merged

steebchen merged 1 commit into
mainfrom
openai-image-fix

Conversation

@steebchen

@steebchen steebchen commented May 1, 2026 •

Copy link
Copy Markdown
Member

Summary

OpenAI gpt-image-2 image-edit requests (FormData / multipart) were failing with:

Unsupported content type: 'multipart/form-data'. This API method only accepts 'application/json' requests…

…on retries (e.g. alternate-key fallback, post-error retry). Azure happened to mask the same bug because we tested it less frequently against this code path.

Root cause

For OpenAI/Azure gpt-image-* edits, prepareRequestBody returns a multipart FormData, and chat.ts swaps the upstream URL from /images/generations to /images/edits to match. That swap was only applied on the initial pass. applyResolvedProviderContext then overwrote url from resolveProviderContext, which only knows the base /images/generations endpoint, and never re-ran the swap. Any retry (same-provider alternate key or HTTP-error fallback) shipped the multipart body to /images/generations, and OpenAI 400'd.

Fix

Mirror the same OpenAI / Azure / xAI URL-to-/edits swap inside applyResolvedProviderContext, so retries land on the same endpoint as the initial attempt.

Test plan

  • OpenAI gpt-image-2 edits with input image, force a same-provider retry → upstream still hits /v1/images/edits
  • Azure gpt-image-2 edits regression (deployment-based + ai-foundry URLs)
  • OpenAI / Azure pure generations (no input image, JSON body) still hit /v1/images/generations
  • xAI image edits retry still hits /v1/images/edits

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Enhanced retry and fallback handling for image generation and editing requests to ensure proper endpoint targeting across OpenAI, Azure, and xAI providers.

OpenAI and Azure gpt-image-* edits return a multipart FormData body, and
chat.ts swaps the upstream URL from /images/generations to /images/edits
to match. That swap only ran on the initial path; applyResolvedProviderContext
overwrote url from resolveProviderContext (which only knows the base
/images/generations endpoint) without re-running the swap, so any retry
fallback (alternate key, HTTP error retry, etc.) sent the multipart body
to /images/generations and OpenAI rejected it with "Unsupported content
type: 'multipart/form-data'".

Mirror the swap inside applyResolvedProviderContext so retries hit the
same endpoint as the initial attempt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 1, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

The PR extends URL rewriting for image generation endpoints to the retry/fallback path. It rewrites upstream URLs from /images/generations to /images/edits during retries for OpenAI, Azure, and xAI when the provider base endpoint is resolved, mirroring existing non-retry logic.

Changes

Cohort / File(s) Summary
Image Generation Retry Logic
apps/gateway/src/chat/chat.ts
Extends URL rewriting to the retry/fallback path, rewriting /images/generations to /images/edits for OpenAI (multipart FormData), Azure (multipart FormData), and xAI (non-FormData bodies containing image/images) when resolveProviderContext returns a base endpoint.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main fix: ensuring the edits URL is maintained on retry for OpenAI/Azure image requests, which is the core issue described in the PR objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch openai-image-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
apps/gateway/src/chat/chat.ts (1)

4046-4073: ⚡ Quick win

Consolidate /images/edits URL rewrite logic into one helper

This retry block now duplicates the same rewrite rules already present around Line 3913. That duplication caused this bug once, and keeping both copies in sync is brittle.

♻️ Suggested refactor
+function rewriteImageEditsUrl(
+	url: string | undefined,
+	isImageGeneration: boolean,
+	usedProvider: string,
+	requestBody: ProviderRequestBody | FormData,
+): string | undefined {
+	if (!isImageGeneration || !url) {
+		return url;
+	}
+	if (usedProvider === "openai" && requestBody instanceof FormData) {
+		return url.replace("/v1/images/generations", "/v1/images/edits");
+	}
+	if (usedProvider === "azure" && requestBody instanceof FormData) {
+		return url.replace("/images/generations", "/images/edits");
+	}
+	if (
+		usedProvider === "xai" &&
+		!(requestBody instanceof FormData) &&
+		("image" in requestBody || "images" in requestBody)
+	) {
+		return url.replace("/v1/images/generations", "/v1/images/edits");
+	}
+	return url;
+}

Then replace both rewrite blocks with:

-// provider-specific rewrite block...
+url = rewriteImageEditsUrl(url, isImageGeneration, usedProvider, requestBody);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/gateway/src/chat/chat.ts` around lines 4046 - 4073, Duplicate
URL-rewrite logic for mapping generation->edits should be consolidated: add a
helper function (e.g., rewriteImageEditsUrl(url: string | undefined,
usedProvider: string, requestBody: any, isImageGeneration: boolean): string |
undefined) that encapsulates the three branch rules (openai FormData replace
"/v1/images/generations"->"/v1/images/edits", azure FormData replace
"/images/generations"->"/images/edits", xai non-FormData with "image"/"images"
keys replace "/v1/images/generations"->"/v1/images/edits") and returns the
possibly-modified url; then replace both places in chat.ts where the same logic
appears (the retry block around this diff and the earlier block around line
~3913) by calling rewriteImageEditsUrl(url, usedProvider, requestBody,
isImageGeneration) so the behavior is centralized and future changes apply in
one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/gateway/src/chat/chat.ts`:
- Around line 4046-4073: Duplicate URL-rewrite logic for mapping
generation->edits should be consolidated: add a helper function (e.g.,
rewriteImageEditsUrl(url: string | undefined, usedProvider: string, requestBody:
any, isImageGeneration: boolean): string | undefined) that encapsulates the
three branch rules (openai FormData replace
"/v1/images/generations"->"/v1/images/edits", azure FormData replace
"/images/generations"->"/images/edits", xai non-FormData with "image"/"images"
keys replace "/v1/images/generations"->"/v1/images/edits") and returns the
possibly-modified url; then replace both places in chat.ts where the same logic
appears (the retry block around this diff and the earlier block around line
~3913) by calling rewriteImageEditsUrl(url, usedProvider, requestBody,
isImageGeneration) so the behavior is centralized and future changes apply in
one place.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7a0ce855-c204-4ce4-b59f-9844aef0c946

📥 Commits

Reviewing files that changed from the base of the PR and between bb072da and c6c7c6d.

📒 Files selected for processing (1)
  • apps/gateway/src/chat/chat.ts

@steebchen
steebchen added this pull request to the merge queue May 1, 2026
Merged via the queue into main with commit a755461 May 1, 2026
17 of 18 checks passed
@steebchen
steebchen deleted the openai-image-fix branch May 1, 2026 11:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant