Skip to content

fix(providers): route Codex GPT Image models to dedicated Images API - #14976

Merged
diegosouzapw merged 5 commits into
cheaper-inference:release/v3.8.52from
xiaoyaner0201:fix/codex-images-dedicated-endpoint
Oct 8, 2026
Merged

diegosouzapw merged 5 commits into
cheaper-inference:release/v3.8.52from
xiaoyaner0201:fix/codex-images-dedicated-endpoint

Conversation

@xiaoyaner0201

@xiaoyaner0201 xiaoyaner0201 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Refs #14617.

Codex GPT Image models now use the dedicated Codex Images API routes instead of the Responses hosted image_generation tool:

  • gpt-image-* generation → /backend-api/codex/images/generations
  • reference-image edit → /backend-api/codex/images/edits
  • requested image model is forwarded unchanged
  • upstream usage is preserved on the internal result for the follow-up usage-accounting slice
  • existing GPT-5.6 *-image catalog entries remain on the hosted Responses tool path

This removes the hosted-tool failure mode where the backend pins the image model to gpt-image-2-codex and where the chat model can decline to call the tool. It does not claim that the backend's output alone proves which image model served a request; the fix establishes the correct dedicated route and request model contract.

The implementation keeps the existing Codex free-plan guard, sibling-account retry classification, error sanitization, response defaults, reference-image handling, and n>1 fan-out. Fan-out is capped at 10 per request, and response-body read failures become a controlled 502 instead of escaping as an unhandled exception.

Tests

Focused suite on the exact release/v3.8.51-rebased candidate:

34 passed, 0 failed

Also passed:

  • ESLint on changed production/test files
  • Prettier check
  • file-size gate
  • diff check
  • changelog fragment changelog.d/fixes/14976-codex-dedicated-images.md

Focused coverage includes generation, n>1, response formatting, free-plan/access errors, sanitization, unreadable response bodies, multipart reference edits, route dispatch, and preservation of the GPT-5.6 hosted-tool path.

Boundary

size remains forwarded but is not asserted as a backend guarantee: the Codex Images backend chooses the actual output dimensions, as documented in the linked issue evidence.

Codex image generation went through /backend-api/codex/responses with the
hosted image_generation tool. The backend pins that tool to gpt-image-2-codex
regardless of the requested model, so GPT Image 2.5 was unreachable, and the
chat model that must call the tool can decline, which surfaced as
"502 Codex completed without producing an image_generation_call".

Codex CLI (openai/codex#23989) uses dedicated unary routes instead:
POST {codex}/images/generations and POST {codex}/images/edits, which take
the image model as a request field. Route `gpt-image-*` Codex ids there:

- add codex/gpt-image-2.5-flare and codex/gpt-image-2 to the Codex image
  catalog; the codex/gpt-5.6-*-image ids stay on the hosted tool unchanged
- new codexImages.ts builds the typed request (model, n=1, size, quality,
  background, images[] for edits), fans out n>1, keeps the free-plan skip,
  the retryable model-access 400, error sanitization and the b64_json/url
  defaults (cheaper-inference#12268), and passes the upstream usage block through
- edits reuse the same path via handleCodexImageEdit

Tests drive the handler, /v1/images/generations and /v1/images/edits with a
mocked upstream; disabling the new dispatch fails 5 of the 6 new tests.
@xiaoyaner0201

Copy link
Copy Markdown
Contributor Author

I reviewed the exact Draft head 876f2eca7ecb62063030af05b373f2a157e47009 against #14617. The dedicated route shape and hosted-tool compatibility look right, but two guards need closing before this goes Ready:

  1. Bound upstream response bodies before decoding. open-sse/handlers/imageGeneration/providers/codexImages.ts:217-219 calls response.text() for successful and error responses without a byte limit. A large or chunked upstream body is buffered in full; n can fan out up to 10 such responses. Check Content-Length where present and count actual streamed bytes (including chunked transfers), cancelling on excess. Keep separate success/error caps. This is an upstream-response limit, distinct from the edit route's ingress-image limit.
  2. Reject malformed n before fan-out. The handler only guards requestedCount > 10 at :142-151; the outer handler normalizes n=0, negative, and fractional values to one call, so those requests succeed rather than receiving a 400. Validate a finite safe integer in the supported range at the new-path boundary; the hosted-tool path can remain unchanged.

I ran a three-case regression against that exact head in an isolated detached worktree with a mocked upstream: 0/3 pass. The n=0 case returned success; a response advertising a 1 GB Content-Length and a 41 MiB chunked response both reached the generic no b64_json image result rather than being stopped as oversized. These tests make no live provider request and touch no production instance. This is feedback on the current Draft head, not a claim that the backend serves a particular image model or that the separate pricing/accounting issue is solved.

@diegosouzapw diegosouzapw changed the title fix(providers): route Codex GPT Image models to dedicated Images API [defer] fix(providers): route Codex GPT Image models to dedicated Images API Sep 29, 2026
@diegosouzapw diegosouzapw added the deferred-v3.8.52 Grande demais / suspeito para o lote atual; precisa de sessão dedicada no ciclo v3.8.52 label Sep 29, 2026
@diegosouzapw
diegosouzapw changed the base branch from release/v3.8.51 to release/v3.8.52 September 29, 2026 11:18
@diegosouzapw

Copy link
Copy Markdown
Collaborator

Re-homed to release/v3.8.52: v3.8.51 entered its release freeze, so the branch now belongs to the release captain and development continues on the next cycle. Nothing is wrong with this PR — it just needed a live base. No action needed from you; CI will re-run against the new base.

@diegosouzapw diegosouzapw removed the deferred-v3.8.52 Grande demais / suspeito para o lote atual; precisa de sessão dedicada no ciclo v3.8.52 label Oct 1, 2026
@diegosouzapw diegosouzapw changed the title [defer] fix(providers): route Codex GPT Image models to dedicated Images API fix(providers): route Codex GPT Image models to dedicated Images API Oct 1, 2026
diegosouzapw and others added 2 commits October 8, 2026 05:19
# Conflicts:
#	open-sse/handlers/imageGeneration.ts
…spatch (cheaper-inference#14976)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
@diegosouzapw
diegosouzapw marked this pull request as ready for review October 8, 2026 09:35
@diegosouzapw
diegosouzapw self-requested a review as a code owner October 8, 2026 09:35
@diegosouzapw
diegosouzapw merged commit 25196cb into cheaper-inference:release/v3.8.52 Oct 8, 2026
6 of 9 checks passed
diegosouzapw added a commit that referenced this pull request Oct 8, 2026
* test(translator): expect boolean is_error on tool_result blocks (#15754)

#15754 made the OpenAI-to-Messages translator always emit a boolean is_error on
every tool_result block (strict upstreams such as the Zed hosted proxy
reject requests without it). The older multimodal translation test still
pinned the block shape without the field.

* test(images): include Codex GPT Image models in the image catalog listing (#14976)

#14976 registered gpt-image-2.5-flare and gpt-image-2 under the codex image
provider (routed to the dedicated Codex Images API) and updated the registry
test to the five-id list, but the image catalog route test still pinned the
three GPT-5.6 hosted-tool ids.

* docs(flags): keep RATE_LIMIT_AUTO_ENABLE catalog default in sync with its definition

#15736 changed the catalog Default cell to _(unset)_, but the catalog
documents FEATURE_FLAG_DEFINITIONS 1:1 and the definition (what the
feature-flags API/UI resolves when nothing is set) is still "false".
Restore the code value and keep the corrected runtime explanation in the
description: rateLimitManager reads only the env var and falls back to the
dashboard setting (default on) when it is unset.

* test(combos): pin on-demand per-candidate reads on the tables #15378 left unmemoized

#15353 added a contrast test asserting the on-demand capability path re-reads
the synced catalog per candidate (>50 reads). #15378, merged just before it,
memoized the synced vision verdict behind the catalog version, so that path
now issues 7 synced reads and the contrast went red on the tip.

Keep the contrast on the capability and override tables (still thousands of
per-candidate reads on demand vs <=2 / bounded on the snapshot route) and pin
the memo itself: synced reads stay within the per-provider bound. With the
pre-#15378 module the new assertion fails (336 synced reads).

* test(chatcore): end the prompt-cache fixture on a user turn (#15830)

#15830 re-landed #13572: the first-party Messages provider now strips a trailing
text-only assistant turn (upstream rejects assistant prefill). The prompt-cache
metadata fixture ended on an assistant text block carrying a cache_control
breakpoint, so the strip dropped it and the recorded totalBreakpoints fell from
3 to 2. Append a user turn so the fixture still exercises system, user and
assistant breakpoints; the assertions are unchanged.

* fix(providers): keep discovered Codex effort variants in the exclusive dashboard listing

#13224 lists one <model>-<tier> row per reasoning level the Codex account
advertises (appendSyncedEffortVariants, non-exclusive branch). #15132 then made
Codex an exclusive synced-listing provider, and the exclusive branch returns
before that step, so the provider dashboard / Test All listing lost the
discovered tier rows while /v1/models still advertised them (cx/<model>-max).

Apply the same variant step on the exclusive branch for codex. The variants
come from the live inventory, so #15132's contract (no static aliases the
account does not advertise) still holds; its own tests stay green. The one
assertion in codex-discovered-reasoning that expected a static registry-only
row (gpt-5.6-sol-ultra) next to the live inventory is flipped to #15132's
contract.

* test(providers): keep the custom gpt-6 max_tokens case on an unregistered id (#15164)

The #14869 custom-model case used gpt-6-luna because it was not in the openai
registry, so it stayed on Chat Completions and had to be renamed to
max_completion_tokens. #15164 registered gpt-6-luna with targetFormat
openai-responses, so the request now goes through the Responses translator
(max_output_tokens) and the Chat Completions assertion read undefined.

Use an unregistered gpt-6 id for the custom-model case and add a case for the
registered gpt-6-luna Responses path (max_output_tokens, no max_tokens).

* chore(changelog): add fragment for the v3.8.52 tip unit-red drain
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.

2 participants