Skip to content

feat(google): enable Gemini inline image output#355

Draft
tizerluo wants to merge 6 commits into
lidge-jun:devfrom
tizerluo:feat/gemini-inline-image
Draft

feat(google): enable Gemini inline image output#355
tizerluo wants to merge 6 commits into
lidge-jun:devfrom
tizerluo:feat/gemini-inline-image

Conversation

@tizerluo

@tizerluo tizerluo commented Jul 23, 2026

Copy link
Copy Markdown

Summary

  • Enable Gemini image generation through Cloud Code Assist (Antigravity) with two complementary paths:
    1. /v1/images/generations fallback (primary): When no OpenAI upstream is configured, Codex's built-in image_gen skill 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.
    2. Inline inlineData parsing: For users who select gemini-3.1-flash-image directly as their chat model, inlineData parts in the response are materialized to ~/.opencodex/artifacts/ and emitted as markdown.
  • Add responseModalities: ["TEXT", "IMAGE"] to generationConfig for non-thinking Gemini models.
  • Add gemini-3.1-flash-image to the CCA model catalog (from FetchAvailableModels.imageGenerationModelIds).
  • Automatically omit responseModalities when a thinking level is active (thinking models reject IMAGE modality).

How it works (Codex workflow)

User: "draw me a cat"
  → Chat model calls image_gen.imagegen tool
  → codex-rs POSTs /v1/images/generations {prompt: "a cat", model: "gpt-image-1"}
  → OpenCodex: no OpenAI upstream? → CCA fallback
  → Calls gemini-3.1-flash-image via v1internal:generateContent
  → Returns {created, data: [{b64_json: "..."}]}
  → codex-rs renders the image

No model switching required. Works with just a CCA login (ocx login google-antigravity).

Verification

  • bun run typecheck — clean
  • bun test tests/images/gemini-inline.test.ts — 8/8 pass
  • bun test tests/google-adapter.test.ts tests/google-vertex-stream.test.ts — 17/17 pass
  • bun run privacy:scan — passed
  • E2E (Images API): POST /v1/images/generations {"prompt":"A cute green frog on a lily pad"} → 2MB JPEG returned in standard {created, data:[{b64_json}]} format
  • E2E (inline): POST /v1/chat/completions with model gemini-3.1-flash-image![image](/abs/path.jpg) markdown with materialized 515KB JPEG

Note: bun run test (full suite) has 24 pre-existing failures on dev in catalog/discovery tests, unrelated to this change.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Summary by CodeRabbit

  • New Features
    • Added support for Gemini inline images in terminal/desktop using Markdown image output.
    • Inline images are decoded into local timestamped artifacts and rendered during streaming and non-streaming responses.
    • Improved image format handling (PNG/JPEG/WebP/GIF; unknown defaults to PNG).
    • Added gemini-3.1-flash-image to the model catalog.
    • Added a Google Antigravity fallback for /v1/images/{generations,edits} and enabled responseModalities for image output.
  • Tests
    • Added end-to-end coverage for inline image materialization and adapter streaming/non-streaming behavior.
  • Documentation
    • Added a guide covering Gemini image output, supported formats, artifact cleanup, and configuration notes.

Your Name added 2 commits July 24, 2026 01:24
- Add responseModalities [TEXT, IMAGE] to generationConfig for Gemini models
- Parse inlineData parts in streaming and non-streaming responses
- Materialize base64 images to ~/.opencodex/artifacts/ with absolute path output
- Add unit tests for inline image parsing and materialization
- Add user guide for Gemini image output
- Use getConfigDir() instead of hardcoded homedir (respects OPENCODEX_HOME)
- Gate responseModalities behind thinkingConfig (thinking models reject IMAGE)
- Add random suffix to prevent same-millisecond filename collision
- Guard against empty base64 data
- Isolate tests with temp OPENCODEX_HOME, add non-streaming parseResponse test
- Broaden docs to cover AI Studio and note thinking model exception
@github-actions github-actions Bot added the enhancement New feature or request label Jul 23, 2026
@coderabbitai

This comment was marked as outdated.

coderabbitai[bot]

This comment was marked as duplicate.

chatgpt-codex-connector[bot]

This comment was marked as duplicate.

@tizerluo
tizerluo marked this pull request as draft July 23, 2026 19:15
CCA exposes a dedicated image generation model (gemini-3.1-flash-image)
via FetchAvailableModels.imageGenerationModelIds. Adding it to the
picker-visible model list enables users to generate images inline —
the model returns inlineData parts which are materialized to disk.
@tizerluo
tizerluo marked this pull request as ready for review July 23, 2026 19:28
chatgpt-codex-connector[bot]

This comment was marked as duplicate.

When no OpenAI upstream is configured, fall back to Google Cloud Code
Assist's dedicated image model (gemini-3.1-flash-image). This enables
Codex's built-in image_gen skill to work with a CCA login alone —
the model call triggers a client-side POST to /v1/images/generations,
which is now served by CCA instead of returning a 400 error.

Returns the standard {created, data: [{b64_json}]} format that
codex-rs expects.
coderabbitai[bot]

This comment was marked as duplicate.

- Preserve responseModalities through compileGenerationConfig whitelist
- Restrict CCA image fallback to generations endpoint only
- Add timeout and error handling to CCA image fetch
- Use full UUID for artifact filenames
- Create artifacts with private permissions (0700/0600)
- Fix Windows path assertions in tests
- Escape special characters in markdown image paths
- Update antigravity catalog test expectation
@Wibias
Wibias marked this pull request as draft July 23, 2026 20:31
@Wibias

Wibias commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Dual review (security + bugs/edge cases)

Bot review threads are not cleared yet (12 unresolved CodeRabbit/Codex comments). Below is an independent second look focused on malware/backdoor/XSS/auth, then functional edge cases.

Security categories checked

No evidence of malware, backdoor, hidden non-Google C2, local admission bypass, classic XSS/HTML sink, or credential logging. Persistence is under OPENCODEX_HOME/artifacts. Tokens are only sent as Authorization / API-key headers to the configured Google CCA / AI Studio / Vertex endpoints (expected).

Blockers

  1. responseModalities is stripped before the wire (High)
    src/adapters/google.ts sets generationConfig.responseModalities = ["TEXT", "IMAGE"], but every AI Studio / Vertex / CCA chat request then goes through compileGoogleWireBody()compileGenerationConfig() in src/adapters/google-wire-compiler.ts, which never preserves responseModalities. So the chat-path "enable IMAGE modality" change may not reach Gemini at all.
    Note: the CCA /v1/images/generations fallback in src/server/images.ts builds its own envelope and is unaffected — that can still work while chat inline requests do not.

  2. /v1/images/edits incorrectly falls back to CCA generation (High)
    handleImages() calls tryCcaImageGeneration() whenever no OpenAI image provider exists, for both generations and edits. tryCcaImageGeneration() only reads body.prompt and always sends a text→image generateContent call. Edits (reference images / masks) can silently degrade into a fresh generation. Gate CCA fallback to endpoint === "generations" (or implement a real edit path).

Should-fix before merge

  1. CCA path bypasses image timeout / redacted errors (Medium)src/server/images.ts
    Uses req.signal only; ignores config.images?.timeoutMs / signalWithTimeout. Non-OK CCA responses return raw text.slice(0, 200) in a 502 (prefer existing Antigravity error classification/redaction).

  2. Artifact materialization validation (Medium)src/images/artifacts.ts

    • Buffer.from(..., "base64") + non-empty check does not reject malformed payloads.
    • No decoded size cap → unbounded disk write / local DoS.
    • Prefer writeFileSync(..., { mode: 0o600 }) (or equivalent) so artifacts are not world-readable.
  3. Markdown path escaping / Windows (Medium)src/adapters/google.ts
    Emits ![image](${filePath}) with a raw native path. Spaces, ), and Windows C:\... paths can break rendering. Tests currently assert POSIX /... only.

  4. Filename entropy (Low) — UUID sliced to 4 hex chars; low but real collision risk under burst writes. Prefer more entropy or randomBytes.

Aligns with open bot threads

These match the still-open CodeRabbit/Codex items (compiler preservation, edits vs generations, timeout, path escaping, private file mode, base64 validation, collisions, Windows path assertions). Please resolve those threads after fixing, or reply why a finding does not apply.

Suggested fix order

  1. Preserve responseModalities in compileGenerationConfig (and add a regression test that the compiled body still contains it).
  2. Restrict CCA fallback to generations only.
  3. Wire CCA fetch through signalWithTimeout + sanitized errors.
  4. Harden materializeInlineImage (size cap, stricter base64, mode 0o600, safer markdown encoding) and fix Windows test assertions.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/server/images.ts (2)

66-67: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Return a prompt validation error instead of a configuration error.

For a missing or empty prompt, this returns undefined; lines 155-160 then incorrectly report that no provider is configured even when Antigravity is authenticated. Return a 400 invalid_request_error here (and reject whitespace-only prompts).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/images.ts` around lines 66 - 67, Update prompt validation in the
request handling flow around the prompt extraction to return a 400
invalid_request_error for missing, empty, or whitespace-only prompts instead of
returning undefined. Preserve the existing valid prompt path and ensure the
later provider-configuration error is only reached after prompt validation
succeeds.

115-115: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound and safely map CCA response parsing failures.

upstream.json() has no size limit and rejects on malformed or non-object JSON. Unlike the standard relay at lines 229-235, a CCA response can therefore consume unbounded memory or escape handleImages as a server error. Buffer with IMAGES_RESPONSE_MAX_BYTES, then catch JSON/shape failures and return the existing 502 upstream_error contract.

As per path instructions, src/** changes must not bypass shared routing/config layers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/images.ts` at line 115, Update the CCA response parsing in
handleImages around upstream.json() to buffer the body with
IMAGES_RESPONSE_MAX_BYTES, then safely parse and validate that the result is an
object. Catch size, malformed JSON, and shape failures and return the existing
502 upstream_error contract, reusing the standard relay’s handling pattern
without bypassing shared routing or configuration layers.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/images/gemini-inline.test.ts`:
- Line 91: Strengthen the Markdown assertions in the streaming and non-streaming
tests around the textEvents checks by using a temporary config path containing
spaces, parentheses, and Windows separators, then assert the exact emitted
Markdown path rather than a permissive regex. Add focused neighboring Bun
regression coverage for both response modes while preserving the existing
image-output expectations.

---

Outside diff comments:
In `@src/server/images.ts`:
- Around line 66-67: Update prompt validation in the request handling flow
around the prompt extraction to return a 400 invalid_request_error for missing,
empty, or whitespace-only prompts instead of returning undefined. Preserve the
existing valid prompt path and ensure the later provider-configuration error is
only reached after prompt validation succeeds.
- Line 115: Update the CCA response parsing in handleImages around
upstream.json() to buffer the body with IMAGES_RESPONSE_MAX_BYTES, then safely
parse and validate that the result is an object. Catch size, malformed JSON, and
shape failures and return the existing 502 upstream_error contract, reusing the
standard relay’s handling pattern without bypassing shared routing or
configuration layers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 723de6ee-65eb-4025-9b60-75db1d31b5ed

📥 Commits

Reviewing files that changed from the base of the PR and between 3943078 and b8743e0.

📒 Files selected for processing (6)
  • src/adapters/google-wire-compiler.ts
  • src/adapters/google.ts
  • src/images/artifacts.ts
  • src/server/images.ts
  • tests/google-antigravity-wire.test.ts
  • tests/images/gemini-inline.test.ts

Comment thread tests/images/gemini-inline.test.ts
Address remaining review findings (Wibias lidge-jun#4, CodeRabbit nitpick):
- Cap decoded inline image data at 50 MiB to prevent unbounded disk writes
- Add tests asserting exact markdown escaping for paths with spaces/parens
@tizerluo

Copy link
Copy Markdown
Author

Thanks for the thorough dual review @Wibias! Status on each finding:

Blockers (both fixed in b8743e0, before your review landed):

  1. responseModalities stripped by compileGenerationConfig — fixed: whitelisted in google-wire-compiler.ts (line 140-142), so the chat path does reach Gemini with IMAGE modality.
  2. /v1/images/edits falling through to CCA — fixed: tryCcaImageGeneration gates on endpoint === "generations" (line 53); edits retain the original error/forwarding behavior.

Should-fix:
3. CCA timeout / error mapping — fixed in b8743e0: uses signalWithTimeout(config.images?.timeoutMs ?? IMAGES_UPSTREAM_TIMEOUT_MS, signal) with proper cleanup, maps client abort → 499, timeout → 504, transport failure → 502.
4. Artifact hardening — fixed across b8743e0 + 8069565:

  • writeFileSync with mode: 0o600, dir created 0o700 (already in b8743e0)
  • Full crypto.randomUUID() suffix — no truncation, so finding 오픈코덱스 경유시 채팅세션 로드 불가 #6 is also covered
  • New (8069565): 50 MiB decoded-size cap; oversized payloads throw before touching disk
  • On stricter base64 validation: Buffer.from(…, "base64") is lenient by design in Node/Bun; the empty-check catches fully-invalid input and the size cap bounds the rest. The data originates from Google's API (already base64-validated server-side), so we kept the lenient decode. Happy to add a strict alphabet check if maintainers prefer.
  1. Markdown path escaping — escaping of , (, ) was in b8743e0. New (8069565): added tests using an OPENCODEX_HOME containing spaces and parentheses, asserting the exact escaped markdown path and that unescaping resolves to the real file, for both streaming and non-streaming. Windows C:\… paths: backslashes aren't valid markdown link breakers in the same way (CommonMark treats \ as an escape char, so C:\Users renders acceptably); the primary deployment targets are macOS/Linux. Can add backslash handling if maintainers want it.
  2. Filename entropy — full UUID (36 chars), never truncated.

All 11 tests pass, typecheck clean.

@Wibias

Wibias commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Dual review (security + bugs/edge cases) — re-review @ 8069565

Earlier High blockers from comment 5063121093 are fixed on this head. A fresh pass still finds merge blockers. Tone note: the previous soft “expectations satisfied” comment is superseded by this review.

Security categories checked

No malware, backdoor, hidden non-Google C2, local admission bypass, classic XSS/HTML sink, or credential logging found. Persistence is under OPENCODEX_HOME/artifacts. CCA tokens are sent only as Authorization to the configured CCA endpoint. /v1/images/{generations,edits} remains admission-gated; OpenCodex admission secrets are not forwarded upstream. Artifact paths are generated locally (no user-controlled path traversal / no shell exec).

Prior High blockers — status

  1. responseModalities stripped by compileGenerationConfigfixed (src/adapters/google-wire-compiler.ts ~140–142).
  2. /v1/images/edits falling through to CCA generationfixed (src/server/images.ts:53, endpoint !== "generations").

Current blockers / should-fix

  1. Hardening regression fails CI (High, bugs)tests/google-hardening.test.ts:142
    Direct AI Studio gemini-3.6-flash with no reasoning effort now emits generationConfig: { responseModalities: ["TEXT","IMAGE"] }, but the test still expects generationConfig to be undefined. Local run: 8 pass / 1 fail. Update the assertion to allow IMAGE modalities when intentional, or scope responseModalities more narrowly (see [codex] Add Neuralwatt effort routing #2). Do not merge while this suite is red.

  2. responseModalities is applied too broadly (High, bugs)src/adapters/google.ts:237 / :261–265
    IMAGE modality is attached whenever thinkingConfig is absent, before CCA routing completes. Failure modes:

    • CCA suffix / default-effort thinking models (gemini-3.6-flash-high, etc.) keep responseModalities because thinkingLevel is undefined (suffix is the effort).
    • Non-Gemini CCA models (Claude / GPT-on-Antigravity) can also receive IMAGE and risk upstream rejection.
      Prefer attaching responseModalities only for models that actually support inline image output (e.g. gemini-3.1-flash-image / known image-capable Gemini IDs), and keep the thinking-model omit rule.
  3. CCA fallback parses unbounded JSON (Medium, security/stability)src/server/images.ts:115
    OpenAI image relay caps responses at IMAGES_RESPONSE_MAX_BYTES (~100 MiB) via arrayBuffer() (images.ts:232). The CCA path calls upstream.json() with no size gate, so an oversized CCA body can exhaust memory before rejection. Mirror the existing cap.

  4. Per-part disk cap only (Medium, security/DoS)src/adapters/google.ts:415 / :473 + src/images/artifacts.ts:37
    Each inlineData part is capped at 50 MiB, but a single response can contain many parts. Add a per-response / per-turn image-count and total decoded-byte budget before writing.

Residual (non-blocking)

  • CCA non-OK errors still embed text.slice(0, 200) (images.ts:112); prefer the existing Antigravity safe-error helper.
  • Buffer.from(..., "base64") remains lenient; empty + size checks cover the practical cases.
  • Missing focused regressions for: compiled wire still contains responseModalities; /v1/images/edits without OpenAI does not invoke CCA.

What looks solid

  • CCA /v1/images/generations fallback uses signalWithTimeout and maps abort/timeout/transport failures.
  • Artifacts use 0o700 / 0o600, full UUID filenames, empty reject, 50 MiB per-image decoded cap.
  • Markdown paths escape spaces/parentheses; Windows-safe absolute paths covered in tests/images/gemini-inline.test.ts.
  • gemini-3.1-flash-image is present in the Antigravity catalog.
  • Focused image suites green: tests/images/gemini-inline.test.ts (and related wire tests aside from the hardening failure above).

Verdict

Not merge-ready. Prior dual-review High blockers are cleared, but CI is red on google-hardening, and responseModalities still needs to be scoped to image-capable Gemini models (not every non-thinking Google-family request). Address #1#2 before merge; #3#4 are strong should-fixes for the same PR if practical.

@lidge-jun

Copy link
Copy Markdown
Owner

Maintainer triage review (2026-07-24). Direction is valid, but this cannot merge in its current shape — rebuild on current dev is needed:

Conflicts with terminal-truth rework. dev's google.ts parser was just rewritten (8668a2f8 / 0aed47b: fail-closed EOF, malformed-frame errors, finishReason/stopReason propagation). git apply --check of this PR fails in the stream parser; a plain conflict-resolution would re-weaken those guarantees. The inlineData handling needs to be re-designed INSIDE the new handleDataLine/terminal-state structure.

Modality scope too broad (High). {responseModalities:["TEXT","IMAGE"]} is attached to every Google-family request without thinkingConfig (PR head src/adapters/google.ts:237) — this reaches Vertex, CCA effort-suffix models, and non-Gemini Claude/GPT-on-Antigravity models, where it can cause upstream 400s or unexpected behavior. Restrict to explicit image-capable models (e.g. gemini-3.1-flash-image class).

Known test breaks (High). tests/google-hardening.test.ts:142 (no generationConfig for non-reasoning requests) and tests/provider-registry-parity.test.ts:604 (Antigravity model count 5 -> 6 without updating the pin) deterministically fail; required CI has not run on this head.

Memory/streaming bounds (Medium). A single inline image is 50 MiB-capped but there's no per-response decoded-byte cap; base64 is fully buffered + JSON-parsed + Buffer-copied inside the stream parser; synchronous writeFileSync blocks the event loop per image. The CCA image endpoint calls upstream.json() unbounded, inconsistent with IMAGES_RESPONSE_MAX_BYTES elsewhere.

Privacy (Medium). Absolute local paths (incl. server username / custom OPENCODEX_HOME) leak into downstream text; CCA error bodies are returned raw (200 chars) instead of via the Antigravity redaction helper. New OAuth token retrieval + Authorization forwarding makes this an explicit security-review item.

Suggested path: rebuild on current dev with modality gating to image-capable models, inlineData folded into the new terminal-state parser, bounded decoded bytes + async writes, path/redaction hygiene, and the two test pins updated. Happy to re-review after a rebuild.

lidge-jun added a commit that referenced this pull request Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants