feat(google): enable Gemini inline image output#355
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
8069565 to
b106701
Compare
This comment was marked as outdated.
This comment was marked as outdated.
230ba5f to
d665a3b
Compare
d665a3b to
1700fa3
Compare
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
Wibias
left a comment
There was a problem hiding this comment.
Follow-up re-review @ d3c876e6
Verdict: request changes. This corrects and supersedes my previous pass. I re-read the newest Codex review on this exact tip and verified the remaining findings against current code.
Most earlier blockers are fixed: pathToFileURL, CCA success-envelope validation, OAuth login-vs-refresh mapping, registry destination pinning, strict artifact handling, retention, docs updates, and CCA fallback coverage are present. I found no remaining OAuth token/project leak in the current implementation.
Remaining code blockers
-
Client cancellation during CCA body read is reported as 504 instead of 499 —
src/server/images.tssignalWithTimeoutpropagates a parent abort intolinkedSignal.signal, so both signals are aborted.- The body-read catch checks
linkedSignal.signal.abortedfirst, making the later parent-signal 499 branch unreachable for client cancellation. - Check the parent request signal first, or distinguish abort reasons. Add a stalled-body regression that aborts the client request and asserts 499 rather than 504.
-
The image deadline does not cover CCA OAuth preflight —
src/server/images.tsand OAuth refresh plumbinggetValidAccessToken("google-antigravity")runs before the linked image deadline is created and receives no request signal.- Token refresh and project discovery can therefore outlive
images.timeoutMsand client cancellation. - Create the linked deadline before credential resolution and propagate its signal through token refresh/project discovery. Preserve 401 for
OAuthLoginRequiredError, 499 for client abort, 504 for deadline expiry, and sanitized 502 for genuine refresh/network failures.
-
Proxy-local
file:artifact URLs do not work for remote/container clients —src/adapters/google.tspathToFileURLfixes URI syntax, but the URI still points to the proxy host's filesystem.- Remote binding is supported, so a client connected to another machine/container cannot open the emitted artifact.
- Serve artifacts through an authenticated, traversal-safe HTTP route using opaque artifact identifiers, or explicitly restrict and enforce inline artifact output as local-only. Add local and remote-client regressions.
-
PNG validation accepts only the first four bytes of the eight-byte PNG signature —
src/images/artifacts.tssig.startsWith("\x89PNG")accepts malformed data with a truncated/fake signature and persists it as PNG.- Validate the complete
89 50 4E 47 0D 0A 1A 0Asignature and add a regression.
-
Inline Gemini payload limits are enforced after large allocations already occurred — adapter response parsing /
src/images/artifacts.ts- Direct non-stream responses are fully buffered and JSON-parsed before checking
inlineData.data. - Streaming accumulates the complete SSE frame, then parsing and
base64Data.replace(...)create additional full-sized strings before the decoded-size guard. - Enforce encoded response/frame limits before JSON parsing and reject oversized raw base64 strings before normalization copies them. Cover both streaming and non-streaming responses.
- Direct non-stream responses are fully buffered and JSON-parsed before checking
Process gates
- Rebase onto current
devbefore merge. - Resolve or answer the remaining review threads after the code matches the tip.
- Run focused image/server tests, typecheck, privacy scan, and full cross-platform CI on the rebased commit.
- Obtain the explicit maintainer security review required for the OAuth credential boundary.
Keep the already-fixed registry pin, token-leak protections, private permissions, retention policy, strict base64/magic validation, CCA error redaction, and fallback semantics unchanged.
d3c876e to
9602aef
Compare
Replace lidge-jun#355's standalone artifacts.ts with lidge-jun#424's fuller version (adds downloadImageToArtifact, SSRF protection, HTTPS enforcement). Also bring in destination-policy.ts exports needed by the module. Removes lidge-jun#355's duplicate guessExtFromMagic — lidge-jun#424's version is the canonical one.
- Rewrite CCA tests to intercept registry host (not local URL) + sink-host regression - Strip home directory from artifact paths in google adapter markdown output - Add try/catch around materializeInlineImage to prevent stream abort on bad parts - Reject empty/whitespace prompts with 400 before OAuth token refresh - Document CCA /v1/images/generations fallback in codex-integration guide (+ locales) - Replace hardcoded artifact path in adapters docs with platform-agnostic wording - Add exclusive create (flag: wx) to artifact writes - guessExtFromMagic throws on unrecognized format (no silent png fallback)
- Emit file: URI instead of ~/ for artifact paths (resolvable + privacy) - Remove imagen-4.0-generate-001 from IMAGE_CAPABLE_MODELS (wrong API) - Try CCA fallback when OpenAI candidate exists but auth fails - Return 502 on OAuth refresh failure (not misleading 400 'none configured') - Reuse ANTIGRAVITY_REQUEST_UA instead of hard-coded UA - Wrap fetch+body-read in try/finally for linkedSignal.cleanup() - Catch body-read timeout/abort → 504/499 - Preserve CCA 4xx statuses instead of collapsing to 502 - Add UUID collision retry on exclusive-create (flag: wx)
- Use pathToFileURL for standard file: URIs (Windows + RFC8089 compliant) - Validate CCA envelope: Array.isArray(parts) + per-part object check - Map OAuthLoginRequiredError → 401 (login required), not 502 (refresh failed) - Fix body-read timeout: check linkedSignal.signal.aborted before parent signal - Remove Imagen from adapters.md docs - Document CCA fallback also fires after OpenAI auth failure (+ locales) - Document artifacts emitted as file: URIs
R4 findings fixed: 1. Body-read client cancellation now reports 499 (not 504): signalWithTimeout propagates parent abort into the linked signal, so both are aborted on client cancel. Check parent signal first in the body-read catch block. 2. OAuth preflight now runs inside the image deadline: Move signalWithTimeout creation before getValidAccessToken so token refresh and project discovery are bounded by timeoutMs and can be cancelled by client abort. Add 499/504 checks after OAuth preflight. 4. PNG magic bytes: validate complete 8-byte signature (89 50 4E 47 0D 0A 1A 0A) instead of just the first 4 bytes. 5. Inline payload size pre-check: reject oversized base64 strings in the adapter before normalization copies them, preventing large allocations before the decoded-size guard runs. Finding 3 (file: URL for remote clients) is a design discussion, not a code bug — addressed in PR review response.
R4 follow-up — all code blockers addressed (
|
Wibias
left a comment
There was a problem hiding this comment.
Follow-up re-review @ 9602aef0
Verdict: request changes. Tip closes 1 and 4 cleanly, and improves 2 and 5, but it is still not merge-ready. Finding 2 is only half-done (deadline timer exists, but OAuth still ignores the signal), finding 3 remains open as a deferred product gap, finding 5 still buffers full payloads before the size gate, process gates are unmet, and new tip Codex P2s are real.
Disposition of prior remaining blockers
| # | Prior finding | Status on 9602aef0 |
|---|---|---|
| 1 | Body-read client cancel → 499 not 504 | Fixed — parent signal.aborted checked before linked; regression CCA body-read client cancellation returns 499, not 504 |
| 2 | Image deadline must cover CCA OAuth preflight | Partially fixed — signalWithTimeout now starts before getValidAccessToken, with post-OAuth 499/504 checks, but getValidAccessToken("google-antigravity") still takes no signal. Refresh/project discovery can ignore client abort and images.timeoutMs until their own internal timeouts finish. Propagate linkedSignal.signal through token refresh and project discovery (or an abortable wrapper), and add a regression that aborts mid-refresh. |
| 3 | Proxy-local file: URLs fail for remote/container clients |
Still open — pathToFileURL is correct for local clients; remote binding still cannot open host filesystem paths. Author deferred to a follow-up. Either ship an authenticated opaque HTTP artifact route, or explicitly restrict/enforce local-only binding for this feature and document + test that remote clients get a clear non-file: failure/fallback. |
| 4 | PNG magic only checked 4 bytes | Fixed — full 89 50 4E 47 0D 0A 1A 0A + truncated-signature regression |
| 5 | Inline payload limits after large allocations | Partially fixed — encoded-length check before materializeInlineImage on stream and non-stream paths. Direct Google responses are still fully buffered/JSON-parsed (and SSE frames fully accumulated) before that check. Bound encoded response/frame size before parse, and reject oversized raw base64 before normalize/copy. |
Process gates (still open)
- Tip is 7 ahead / 1 behind
dev— rebase again before merge. - No Cross-platform CI on this tip (only
enforce-target+label+ CodeRabbit). - Many review threads still unresolved/outdated — resolve or answer after the tip matches.
- Explicit maintainer security review for the OAuth/CCA credential boundary is still required.
New tip issues (Codex on 9602aef0, still valid)
- Malformed CCA
inlineData.datacan still succeed —src/server/images.ts: non-string / invalid base64 should be sanitized 502, not forwarded as successb64_json. - Locale docs duplicate/break the “neither” bullet — JA/KO/RU/ZH
codex-integration.md. gemini-3.1-flash-imageis filtered out of the routed catalog —shouldExposeRoutedModeldrops delimitedimageIDs; add an exception +gatherRoutedModelsregression.- CCA safety blocks mapped as malformed 502 — content-policy / empty-candidate refusals should be non-retryable filter errors, not retried 502s.
Keep the already-fixed registry pin, sink-host regression, prompt-before-OAuth, retention, strict magic validation, and 499/504 parent-first body-read handling unchanged.
Rebuilt on current dev (terminal-truth parser rework). - Parse inlineData parts in both streaming and non-streaming Google adapter paths, materializing images to OPENCODEX_HOME/artifacts with async writes, per-image (50 MiB) and per-response (100 MiB) decoded byte budgets, 0o700/0o600 permissions, and full-UUID filenames. - Restrict responseModalities=["TEXT","IMAGE"] to explicit image-capable models (gemini-3.1-flash-image class) — non-image Gemini, Vertex, Claude/GPT-on-Antigravity, and thinking models are unaffected. - Whitelist responseModalities in compileGenerationConfig so the setting survives CCA/Vertex wire compilation. - Add gemini-3.1-flash-image to the Antigravity model catalog (wire IDs, picker, context windows). - CCA /v1/images/generations fallback: gated to generations only, uses signalWithTimeout, caps response via arrayBuffer (IMAGES_RESPONSE_MAX_BYTES), redacts errors via safeAntigravityHttpErrorMessage. - Markdown paths escape spaces/parentheses; tests cover special-char dirs. - Update model-count pins in provider-registry-parity and antigravity-wire.
Replace lidge-jun#355's standalone artifacts.ts with lidge-jun#424's fuller version (adds downloadImageToArtifact, SSRF protection, HTTPS enforcement). Also bring in destination-policy.ts exports needed by the module. Removes lidge-jun#355's duplicate guessExtFromMagic — lidge-jun#424's version is the canonical one.
- Rewrite CCA tests to intercept registry host (not local URL) + sink-host regression - Strip home directory from artifact paths in google adapter markdown output - Add try/catch around materializeInlineImage to prevent stream abort on bad parts - Reject empty/whitespace prompts with 400 before OAuth token refresh - Document CCA /v1/images/generations fallback in codex-integration guide (+ locales) - Replace hardcoded artifact path in adapters docs with platform-agnostic wording - Add exclusive create (flag: wx) to artifact writes - guessExtFromMagic throws on unrecognized format (no silent png fallback)
- Emit file: URI instead of ~/ for artifact paths (resolvable + privacy) - Remove imagen-4.0-generate-001 from IMAGE_CAPABLE_MODELS (wrong API) - Try CCA fallback when OpenAI candidate exists but auth fails - Return 502 on OAuth refresh failure (not misleading 400 'none configured') - Reuse ANTIGRAVITY_REQUEST_UA instead of hard-coded UA - Wrap fetch+body-read in try/finally for linkedSignal.cleanup() - Catch body-read timeout/abort → 504/499 - Preserve CCA 4xx statuses instead of collapsing to 502 - Add UUID collision retry on exclusive-create (flag: wx)
- pruneOldArtifacts() caps artifacts/ to 200 files, deleting oldest by mtime - Runs after each successful write in materializeInlineImage + downloadImageToArtifact - Best-effort: errors caught and warned, never fails the write
- Use pathToFileURL for standard file: URIs (Windows + RFC8089 compliant) - Validate CCA envelope: Array.isArray(parts) + per-part object check - Map OAuthLoginRequiredError → 401 (login required), not 502 (refresh failed) - Fix body-read timeout: check linkedSignal.signal.aborted before parent signal - Remove Imagen from adapters.md docs - Document CCA fallback also fires after OpenAI auth failure (+ locales) - Document artifacts emitted as file: URIs
R4 findings fixed: 1. Body-read client cancellation now reports 499 (not 504): signalWithTimeout propagates parent abort into the linked signal, so both are aborted on client cancel. Check parent signal first in the body-read catch block. 2. OAuth preflight now runs inside the image deadline: Move signalWithTimeout creation before getValidAccessToken so token refresh and project discovery are bounded by timeoutMs and can be cancelled by client abort. Add 499/504 checks after OAuth preflight. 4. PNG magic bytes: validate complete 8-byte signature (89 50 4E 47 0D 0A 1A 0A) instead of just the first 4 bytes. 5. Inline payload size pre-check: reject oversized base64 strings in the adapter before normalization copies them, preventing large allocations before the decoded-size guard runs. Finding 3 (file: URL for remote clients) is a design discussion, not a code bug — addressed in PR review response.
R5 findings fixed: 2. OAuth preflight signal propagation: wrap getValidAccessToken in abortableRace against linkedSignal.signal so client abort / timeout during token refresh surfaces as 499/504 instead of hanging. 5. HTTP-layer payload limit: add Content-Length pre-check in parseResponse before response.json() buffers the body. Codex P2-1: Reject malformed CCA inlineData.data (non-string types were forwarded as fake b64_json). Codex P2-2: Fix duplicate 'neither' bullet in JA/KO/RU/ZH locale docs. Codex P2-3: Exempt gemini-*-image chat models from routed catalog media-generation filter (shouldExposeRoutedModel). Codex P2-4: Map CCA safety blocks (finishReason SAFETY/BLOCKLIST/etc) to non-retryable 400 instead of retried 502. Finding 3 (file: URL local-only) addressed in review response as a design positioning decision, not a code bug.
9602aef to
df6a745
Compare
R5 follow-up — all blockers addressed (
|
Wibias
left a comment
There was a problem hiding this comment.
Re-review @ df6a745
Verdict: request changes. Tip closes most earlier Wibias/R5 code blockers and the 14:07 Codex batch. It is still not merge-ready. Remaining work below must ship in this PR before merge; optional follow-ups are not accepted.
Fixed on tip (do not reopen)
| Area | Status |
|---|---|
| Registry destination pin + sink-host regression | Fixed |
| CCA Images fallback documented (EN + locales) + CCA-after-OpenAI-auth-fail | Fixed |
| Empty/whitespace prompt → 400 before OAuth | Fixed |
materializeInlineImage try/catch → structured error |
Fixed |
Exclusive wx create + UUID collision retry |
Fixed |
| Full PNG 8-byte magic; throw on unrecognized | Fixed |
| Artifact retention/prune | Fixed |
pathToFileURL for local file: syntax |
Fixed (local only — remote still open below) |
ANTIGRAVITY_REQUEST_UA, linked timeout finally, body-read 499/504 parent-first |
Fixed |
OAuth login → 401 vs refresh → 502; deadline + abortableRace around preflight |
Fixed |
CCA envelope Array.isArray(parts) + non-object skip |
Fixed |
Safety finish reasons → non-retryable 400 (partial — missing RECITATION) |
Mostly fixed |
Non-string inlineData.data not forwarded as b64_json |
Partial — type check only; base64/magic still missing |
| Locale duplicate “neither” bullets | Fixed |
| Keep Gemini image chat in catalog | Partial — exemption too broad |
Rebase vs dev |
Fixed — ahead 8 / behind 0 |
| Draft cleared | Fixed |
Keep registry pin, OAuth redaction, timeouts, private artifact permissions, and retention unchanged while fixing the items below.
Remaining blockers (required in this PR)
-
Admission bearer must allow CCA-only Images requests —
src/server/images.ts
validateForwardAdmissionCredential()runs before the CCA-only branch and rejects validAuthorization: Bearer <OPENCODEX_API_AUTH_TOKEN>for non-loopback clients. CCA never forwards that bearer; it uses stored Google OAuth. Bypass/strip the admission check when selecting CCA. Never send the proxy admission bearer upstream. -
Reject
n !== 1for CCA unless real multi-image support exists —src/server/images.ts
CCA envelope ignoresnand can succeed with fewer images than requested. Reject unsupportednbefore OAuth or generation (or implement proper multi-image and enforce returned count). -
Treat
RECITATIONas a permanent CCA content-filter —src/server/images.ts
AddRECITATIONtoCCA_BLOCKING_FINISH_REASONS. Google adapter already maps it tocontent_filter; omitting it here turns paid refusals into retried 502s. -
Strictly validate every CCA
inlineData.databeforeb64_json—src/server/images.ts
Require bounded base64 and a supported image magic signature before returning success. Non-string was tightened; invalid base64 / fake bytes can still become a “successful” Images payload that fails in Codex. -
Replace broad
gemini+imageheuristics with explicit allowlists —src/codex/catalog/parsing.ts+src/adapters/google.ts
Current/gemini/i && /image/iresurrects standalone media-gen IDs (e.g.gemini-3-pro-image) into the picker /responseModalitiespath. Allowlist known chat-capable provider/model pairs only; keep media-generation IDs filtered. -
Replace proxy-local
file:links with an authenticated artifact HTTP route —src/adapters/google.ts(+ server route)
pathToFileURLis fine for same-host clients; remote/container clients cannot open host filesystem paths and paths should not leak. Serve artifacts via an authenticated, traversal-safe HTTP route using opaque IDs for both local and remote clients. -
Enforce Gemini response / SSE-frame size limits before full buffering or JSON parse —
src/adapters/google.ts
Encoded per-image check and non-streamContent-Lengthhelp, but missing/lyingContent-Lengthstill fully buffers viaresponse.json(), and a single oversized SSE frame is still parsed first. Cap response/frame size before buffering/parsing.
Focused regressions (keep minimal)
- Admission-bearer CCA-only request succeeds; bearer never appears upstream
n > 1rejected (or multi-image correct) before OAuthRECITATION→ non-retryable content-filter, not 502- Malformed base64 + fake image bytes → sanitized failure, not
b64_json - Negative catalog cases: media-gen IDs stay filtered; allowlisted chat image models stay exposed
- Remote client can fetch artifact via opaque authenticated route (no server path in markdown)
- Oversized non-stream / chunked SSE frame rejected before full parse
Do not expand into a large test matrix beyond these behaviors.
Process gates
| Gate | Status |
|---|---|
| Base branch | dev (correct) |
Divergence vs dev |
OK — ahead 8 / behind 0 |
| Cross-platform CI on exact tip | Still open — only enforce-target / label / CodeRabbit |
| Maintainer security review (OAuth/CCA + new artifact route) | Still required |
| Unresolved review threads | Still open — resolve/reply after tip matches |
After fixes
Land blockers 1–7 + the focused regressions above, run exact-head Cross-platform CI green, then ping for maintainer security review.
Summary
/v1/images/generationsfallback (primary): When no OpenAI upstream is configured, Codex's built-inimage_genskill now falls back to CCA's dedicated image model (gemini-3.1-flash-image). This is the standard Codex workflow — the model calls the tool, codex-rs POSTs to the Images API, and the proxy serves it via CCA.inlineDataparsing: For users who selectgemini-3.1-flash-imagedirectly as their chat model,inlineDataparts in the response are materialized to~/.opencodex/artifacts/and emitted as markdown.responseModalities: ["TEXT", "IMAGE"]togenerationConfigfor non-thinking Gemini models.gemini-3.1-flash-imageto the CCA model catalog (fromFetchAvailableModels.imageGenerationModelIds).responseModalitieswhen a thinking level is active (thinking models reject IMAGE modality).How it works (Codex workflow)
No model switching required. Works with just a CCA login (
ocx login google-antigravity).Verification
bun run typecheck— cleanbun test tests/images/gemini-inline.test.ts— 8/8 passbun test tests/google-adapter.test.ts tests/google-vertex-stream.test.ts— 17/17 passbun run privacy:scan— passedPOST /v1/images/generations {"prompt":"A cute green frog on a lily pad"}→ 2MB JPEG returned in standard{created, data:[{b64_json}]}formatPOST /v1/chat/completionswith modelgemini-3.1-flash-image→markdown with materialized 515KB JPEGNote:
bun run test(full suite) has 24 pre-existing failures ondevin catalog/discovery tests, unrelated to this change.Checklist
Summary by CodeRabbit
gemini-3.1-flash-imagemodel.