Repository navigation
feat(api): combo/alias resolution + OpenAI-compatible edits for image routes (#3214, #3215) - #3219
Conversation
… routes (#3214, #3215) Image routes now resolve a requested model the same way across /v1/images/generations and /v1/images/edits, via a shared resolver: built-in id -> custom provider prefix -> bare combo/alias name (e.g. "image" -> its single image target). Previously a bare combo name fell through to "Invalid image model". /v1/images/edits gains two capabilities for custom OpenAI-compatible providers: - multipart edit forwarding to the node's {base_url}/images/edits (was hard-rejected unless chatgpt-web); - JSON/data-URL edit input (images:[{image_url:"data:..."}]), converted to the same fields the multipart reader produces (was "Invalid multipart body"). The chatgpt-web conversation-continuation edit flow is unchanged.
There was a problem hiding this comment.
Code Review
This pull request introduces shared model resolution for image routes and adds support for OpenAI-compatible image edits via multipart or JSON payloads, along with corresponding unit tests. Feedback points out a TypeScript compilation error in resolveImageModelPrefix due to a missing id property on the typed node parameter, and suggests improving the robustness of extractImageEditInputFromJson to handle nested image_url objects in standard OpenAI vision formats.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const nodes = await getProviderNodes({ type: "openai-compatible" }); | ||
| // node.id (internal UUID) is already a valid internal id; only rewrite when a | ||
| // user-defined prefix differs from the node id. | ||
| const matched = nodes.find((node: { prefix?: unknown }) => node.prefix === prefixPart); |
There was a problem hiding this comment.
The parameter node is explicitly typed as { prefix?: unknown }. Consequently, TypeScript infers the type of matched as { prefix?: unknown } | undefined. Accessing matched.id on line 42 will trigger a compilation error because id is not a property of { prefix?: unknown }. Typing node with id?: unknown resolves this type-checking issue.
| const matched = nodes.find((node: { prefix?: unknown }) => node.prefix === prefixPart); | |
| const matched = nodes.find((node: { prefix?: unknown; id?: unknown }) => node.prefix === prefixPart); |
| else if (entry && typeof entry === "object") { | ||
| const e = entry as Record<string, unknown>; | ||
| candidates.push(e.image_url ?? e.url ?? e.b64_json); | ||
| } |
There was a problem hiding this comment.
If a client sends the standard OpenAI vision format where image_url is an object containing a nested url property (e.g., "image_url": { "url": "data:..." }), e.image_url will resolve to an object. Pushing this object directly to candidates will cause parseDataUrl to fail since it expects a string. Handling nested url or b64_json properties makes the parser significantly more robust.
else if (entry && typeof entry === "object") {
const e = entry as Record<string, unknown>;
const imgUrl = e.image_url ?? e.url ?? e.b64_json;
if (typeof imgUrl === "string") {
candidates.push(imgUrl);
} else if (imgUrl && typeof imgUrl === "object") {
const nested = imgUrl as Record<string, unknown>;
candidates.push(nested.url ?? nested.b64_json);
}
}| if (typeof entry === "string") candidates.push(entry); | ||
| else if (entry && typeof entry === "object") { | ||
| const e = entry as Record<string, unknown>; | ||
| candidates.push(e.image_url ?? e.url ?? e.b64_json); |
There was a problem hiding this comment.
WARNING: b64_json field is accepted but silently dropped — parseDataUrl requires a data: prefix, so raw base64 values (e.g. "iVBORw0...") return null and the image is never extracted. The request then falls through to the route-level "Missing required field: image" error, which is confusing for clients that sent b64_json expecting it to work.
If b64_json support is intended, wrap it with a data-URL prefix before passing to parseDataUrl, or document that only data: URIs are accepted.
… routes (diegosouzapw#3214, diegosouzapw#3215) (diegosouzapw#3219) Image routes now resolve a requested model the same way across /v1/images/generations and /v1/images/edits, via a shared resolver: built-in id -> custom provider prefix -> bare combo/alias name (e.g. "image" -> its single image target). Previously a bare combo name fell through to "Invalid image model". /v1/images/edits gains two capabilities for custom OpenAI-compatible providers: - multipart edit forwarding to the node's {base_url}/images/edits (was hard-rejected unless chatgpt-web); - JSON/data-URL edit input (images:[{image_url:"data:..."}]), converted to the same fields the multipart reader produces (was "Invalid multipart body"). The chatgpt-web conversation-continuation edit flow is unchanged.
… routes (diegosouzapw#3214, diegosouzapw#3215) (diegosouzapw#3219) Image routes now resolve a requested model the same way across /v1/images/generations and /v1/images/edits, via a shared resolver: built-in id -> custom provider prefix -> bare combo/alias name (e.g. "image" -> its single image target). Previously a bare combo name fell through to "Invalid image model". /v1/images/edits gains two capabilities for custom OpenAI-compatible providers: - multipart edit forwarding to the node's {base_url}/images/edits (was hard-rejected unless chatgpt-web); - JSON/data-URL edit input (images:[{image_url:"data:..."}]), converted to the same fields the multipart reader produces (was "Invalid multipart body"). The chatgpt-web conversation-continuation edit flow is unchanged.
… routes (diegosouzapw#3214, diegosouzapw#3215) (diegosouzapw#3219) Image routes now resolve a requested model the same way across /v1/images/generations and /v1/images/edits, via a shared resolver: built-in id -> custom provider prefix -> bare combo/alias name (e.g. "image" -> its single image target). Previously a bare combo name fell through to "Invalid image model". /v1/images/edits gains two capabilities for custom OpenAI-compatible providers: - multipart edit forwarding to the node's {base_url}/images/edits (was hard-rejected unless chatgpt-web); - JSON/data-URL edit input (images:[{image_url:"data:..."}]), converted to the same fields the multipart reader produces (was "Invalid multipart body"). The chatgpt-web conversation-continuation edit flow is unchanged.
Closes #3214
Closes #3215
Follow-up to #3205/#3208 (which fixed custom-provider base URL + prefix resolution for generations). Implements the two remaining image-route gaps the reporter (@ngocquynh85) documented.
What changed
Shared model resolver (
src/lib/images/imageRouteModel.ts) used by both/v1/images/generationsand/v1/images/edits, resolving in order:myImg/gpt-image-2→ internal<nodeId>/<model>)image→ the combo's single image target) — was "Invalid image model" (Image routes should resolve combo aliases and support OpenAI-compatible image edits #3215 part 1)/v1/images/edits{base_url}/images/edits(handleOpenAIImageEdit) instead of being hard-rejected as non-chatgpt-web (OpenAI-compatible custom image providers are not supported by /v1/images/edits #3214).images:[{image_url:"data:..."}]orimage:"data:..."), converted to the same fields the multipart reader produces — previously "Invalid multipart body" (Image routes should resolve combo aliases and support OpenAI-compatible image edits #3215 part 3).resolveImageBaseUrlgained anendpoint: "generations" | "edits"param (defaults to generations; rewrites a base URL that already points at the other image endpoint).Tests (TDD)
tests/unit/image-routes-combo-edits-3214-3215.test.ts(isolated DATA_DIR):resolveImageBaseUrledits-endpoint append + cross-endpoint rewrite + fallbackparseDataUrl/extractImageEditInputFromJson(data-URL decode + JSON edit shapes)All additive to currently-failing paths (bare-combo, non-chatgpt-web edit, JSON edit) → no change to working generation/chatgpt-web behavior. Existing image suites stay green (route 7, handler 43, baseurl-3205 6). typecheck + any-budget pass.