Skip to content

feat(images): add Grok image bridge for non-OpenAI models#424

Open
tizerluo wants to merge 1 commit into
lidge-jun:devfrom
tizerluo:feat/image-bridge
Open

feat(images): add Grok image bridge for non-OpenAI models#424
tizerluo wants to merge 1 commit into
lidge-jun:devfrom
tizerluo:feat/image-bridge

Conversation

@tizerluo

@tizerluo tizerluo commented Jul 24, 2026

Copy link
Copy Markdown

Summary

When Codex routes to a non-OpenAI model (Claude, Gemini, Grok, etc.), the image_generation hosted tool stops working because it requires OpenAI's server-side execution. This PR adds an Image Bridge that:

  1. Detects image_generation in the request's tools array
  2. Replaces it with a synthetic function tool (image_gen) the routed model can call
  3. Intercepts the model's tool call and routes it to xAI Grok Imagine (/images/generations)
  4. Materializes generated images to ~/.opencodex/artifacts/ with magic-byte format detection
  5. Injects the local file path back into the conversation as a tool result
  6. Re-calls the model so it can reference the image in its final response

The architecture mirrors the proven src/web-search/ sidecar pattern — same async-generator loop, bridgeToResponsesSSE, heartbeat, and abort-signal linkage.

New Files

File Purpose Lines
src/images/types.ts Shared types (ImageBridgePlan, ImageCallResult) 19
src/images/synthetic-tool.ts Tool injection + name detection + extraction 69
src/images/xai-client.ts xAI /images/generations and /images/edits client 72
src/images/fulfill.ts Execute one image call, xAI API, materialize to disk 69
src/images/plan.ts Decide whether to activate bridge (provider + token check) 52
src/images/loop.ts Core agentic loop (max 3 rounds, SSE bridge) 372
src/images/index.ts Barrel export 4
tests/images/*.test.ts 5 test files, 53 tests covering all modules
docs-site/.../image-bridge.md User guide 63

Modified Files

  • src/types.ts_imageGeneration stash field, OcxTool.imageGeneration flag, OcxImagesConfig bridge fields
  • src/responses/parser.tsextractHostedImageGeneration() + stash into parsed result
  • src/server/responses/core.ts — image bridge trigger (after web-search block, before standard path)
  • src/images/artifacts.ts — extended with downloadImageToArtifact() + guessExtFromMagic() (shared helpers extracted)

Verification

  • bun x tsc --noEmit passes (0 errors)
  • bun test tests/images/ passes (53 tests, 0 failures)
  • bun run privacy:scan passes
  • Full test suite: 4097 pass / 26 fail (all 26 failures are pre-existing, unrelated)
  • Manual integration test: sent "draw a simple cat" with grok-4.5 + image_generation tool. Image generated via xAI Grok Imagine, materialized to disk (864x1152 JPEG, 108KB), model produced markdown response with local path.

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 an image-bridge for image_generation on non-OpenAI providers using an interception/synthetic tool and bounded multi-round tool-call looping.
    • Materializes images into local artifacts from both base64 and URLs, returning a primary Markdown image link and allowing partial successes.
    • Added configurable bridging (enablement, bridge model, max rounds) with xAI Grok Imagine support.
  • Documentation
    • Published an “Image Bridge” guide with setup, config locations, and limitations (streaming-only).
  • Bug Fixes
    • Enforced streaming-only behavior and improved validation/error handling for image fulfillment and looping.
  • Tests
    • Added Bun test coverage for planning, tool matching, fulfillment, artifact materialization, and streamed loop behavior.

@github-actions github-actions Bot added the enhancement New feature or request label Jul 24, 2026
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an Image Bridge for routed image-generation requests. It detects hosted image tools, plans xAI execution, stores generated images as local artifacts, fulfills intercepted calls, continues the model conversation through bounded SSE iterations, and documents configuration and limitations.

Changes

Image Bridge

Layer / File(s) Summary
Image tool contracts and provider planning
src/types.ts, src/images/synthetic-tool.ts, src/responses/parser.ts, src/images/plan.ts, src/images/types.ts, src/images/index.ts, tests/images/synthetic-tool.test.ts, tests/images/plan.test.ts, tests/responses-parser.test.ts
Adds image bridge configuration and types, extracts hosted image-generation tools, resolves xAI credentials, and creates plans with a synthetic callable tool.
xAI calls and artifact fulfillment
src/images/xai-client.ts, src/images/artifacts.ts, src/images/fulfill.ts, tests/images/fulfill.test.ts, tests/images/xai-client.test.ts
Calls xAI generation or edit endpoints, validates and stores base64 or downloaded images, enforces size budgets, and returns artifact paths with partial-success handling.
Bounded image interception loop
src/images/loop.ts, tests/images/loop.test.ts
Intercepts image-only streamed tool calls, fulfills them, injects tool results, handles cancellation and iteration limits, and produces SSE output.
Request-handler integration and guide
src/server/responses/core.ts, docs-site/src/content/docs/guides/image-bridge.md
Routes eligible non-OpenAI requests through the image bridge and documents prerequisites, configuration, flow, and limitations.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ResponsesCore
  participant runWithImageBridge
  participant xAI
  participant Artifacts
  Client->>ResponsesCore: image_generation request
  ResponsesCore->>runWithImageBridge: execute routed image turn
  runWithImageBridge->>xAI: generate or edit image
  xAI-->>runWithImageBridge: image data or URL
  runWithImageBridge->>Artifacts: save image artifact
  Artifacts-->>runWithImageBridge: local path
  runWithImageBridge-->>Client: continued SSE response
Loading

Possibly related PRs

Suggested reviewers: ingwannu, lidge-jun

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% 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 is concise and accurately summarizes the main change: adding a Grok-based image bridge for non-OpenAI models.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

coderabbitai[bot]

This comment was marked as outdated.

chatgpt-codex-connector[bot]

This comment was marked as outdated.

@tizerluo
tizerluo force-pushed the feat/image-bridge branch from fb94938 to 976da38 Compare July 24, 2026 21:18

@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: 2

♻️ Duplicate comments (3)
docs-site/src/content/docs/guides/image-bridge.md (1)

15-18: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Correct the xAI authentication guidance.

Document the xai provider preset with https://api.x.ai/v1 and the openai-chat adapter. Explain that authMode: "oauth" uses the stored/auto-refreshed bearer token, while authMode: "key" uses the configured API key. Do not claim OAuth is less reliable without evidence; forward only relays the curated ChatGPT header allowlist and is not a substitute for xAI bridge credentials.

Also applies to: 60-63

🤖 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 `@docs-site/src/content/docs/guides/image-bridge.md` around lines 15 - 18,
Update the xAI authentication guidance in the image-bridge guide to document the
xAI preset using https://api.x.ai/v1 with the openai-chat adapter, and
distinguish authMode: "oauth" (stored/auto-refreshed bearer token) from
authMode: "key" (configured API key). Remove unsupported reliability claims and
clarify that forward only relays the curated ChatGPT header allowlist, not xAI
bridge credentials.

Source: Path instructions

src/server/responses/core.ts (2)

1452-1473: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add focused handler-level regression coverage.

Cover a non-OpenAI routed request that injects image_gen and calls the bridge, OpenAI passthrough bypass, web-search precedence, and the non-streaming behavior above.

As per path instructions, “A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem.”

🤖 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/responses/core.ts` around lines 1452 - 1473, Add focused
regression tests near the existing response-handler tests covering a non-OpenAI
routed request that injects image_gen and invokes runWithImageBridge, OpenAI
passthrough bypass, web-search taking precedence, and non-streaming responses
returning the bridged body with preserved status and headers. Use the handler
symbols planImageBridge, runWithImageBridge, and the surrounding response flow
to verify each behavior without broad refactoring.

Source: Path instructions


1457-1471: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not return SSE for non-streaming image-bridge requests.

This branch ignores parsed.stream, while runWithImageBridge forces every provider iteration to stream: true and returns the bridged stream. A request with "stream": false can therefore receive text/event-stream instead of a normal Responses JSON object.

Collect/serialize a non-streaming result, or reject it explicitly before invoking the bridge.

Proposed guard
   if (imgPlan && !wsPlan) {
+    if (!parsed.stream) {
+      return formatErrorResponse(
+        400,
+        "invalid_request_error",
+        "Image bridge requires stream=true",
+      );
+    }
     parsed.context.tools = [...(parsed.context.tools ?? []), buildImageTool()];
🤖 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/responses/core.ts` around lines 1457 - 1471, Update the
image-bridge branch around runWithImageBridge to honor parsed.stream: reject
non-streaming requests before invoking the bridge, or collect and serialize the
bridged result as a normal Responses JSON object instead of returning its SSE
body. Preserve the existing trackStreamLifetime Response path only for streaming
requests.
🤖 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 `@src/images/loop.ts`:
- Around line 147-150: Update the image bridge dependency construction in the
responses core flow to forward config.images.maxRounds whenever it is defined,
including 0, instead of using a truthy check. Preserve omission when the value
is undefined so runWithImageBridge can apply its default only when no
configuration was provided.

In `@src/images/synthetic-tool.ts`:
- Around line 21-44: Update the function-tool branch in
extractHostedImageGeneration to read the tool name from the flat obj.name field
instead of nested obj.function.name, while retaining the isImageGenName check
and existing tool tracking. Update the corresponding synthetic-tool test fixture
to use the flat Responses shape with type "function" and name.

---

Duplicate comments:
In `@docs-site/src/content/docs/guides/image-bridge.md`:
- Around line 15-18: Update the xAI authentication guidance in the image-bridge
guide to document the xAI preset using https://api.x.ai/v1 with the openai-chat
adapter, and distinguish authMode: "oauth" (stored/auto-refreshed bearer token)
from authMode: "key" (configured API key). Remove unsupported reliability claims
and clarify that forward only relays the curated ChatGPT header allowlist, not
xAI bridge credentials.

In `@src/server/responses/core.ts`:
- Around line 1452-1473: Add focused regression tests near the existing
response-handler tests covering a non-OpenAI routed request that injects
image_gen and invokes runWithImageBridge, OpenAI passthrough bypass, web-search
taking precedence, and non-streaming responses returning the bridged body with
preserved status and headers. Use the handler symbols planImageBridge,
runWithImageBridge, and the surrounding response flow to verify each behavior
without broad refactoring.
- Around line 1457-1471: Update the image-bridge branch around
runWithImageBridge to honor parsed.stream: reject non-streaming requests before
invoking the bridge, or collect and serialize the bridged result as a normal
Responses JSON object instead of returning its SSE body. Preserve the existing
trackStreamLifetime Response path only for streaming requests.
🪄 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: 1c432e5a-32fd-4e62-aa6d-2b080fad526f

📥 Commits

Reviewing files that changed from the base of the PR and between fb94938 and 976da38.

📒 Files selected for processing (17)
  • docs-site/src/content/docs/guides/image-bridge.md
  • src/images/artifacts.ts
  • src/images/fulfill.ts
  • src/images/index.ts
  • src/images/loop.ts
  • src/images/plan.ts
  • src/images/synthetic-tool.ts
  • src/images/types.ts
  • src/images/xai-client.ts
  • src/responses/parser.ts
  • src/server/responses/core.ts
  • src/types.ts
  • tests/images/fulfill.test.ts
  • tests/images/loop.test.ts
  • tests/images/plan.test.ts
  • tests/images/synthetic-tool.test.ts
  • tests/images/xai-client.test.ts

Comment thread src/images/synthetic-tool.ts
@tizerluo
tizerluo force-pushed the feat/image-bridge branch from 976da38 to 38d4112 Compare July 24, 2026 22:45
coderabbitai[bot]

This comment was marked as outdated.

@tizerluo
tizerluo force-pushed the feat/image-bridge branch from 38d4112 to d4b9ac5 Compare July 24, 2026 23:25
coderabbitai[bot]

This comment was marked as outdated.

@tizerluo
tizerluo force-pushed the feat/image-bridge branch from d4b9ac5 to 79da2f9 Compare July 25, 2026 07:10
coderabbitai[bot]

This comment was marked as outdated.

@tizerluo
tizerluo force-pushed the feat/image-bridge branch from 79da2f9 to db48e7c Compare July 25, 2026 08:08
@lidge-jun

Copy link
Copy Markdown
Owner

🔒 Under maintainer review — detailed feedback incoming

@lidge-jun (maintainer) has this PR in an active review pass. Please do not merge or close
it
until the detailed review lands; a full comment with specific file:line findings,
failure modes, and suggested fixes is being prepared.

This PR is not part of the current integration batch. It is held back deliberately —
either because it touches a security boundary that MAINTAINERS.md requires an explicit
security review for, because it needs to be split into reviewable units, or because it needs
a rebase onto changes landing in dev right now.

Heads-up on rebasing: dev is receiving a coordinated integration batch today. If this PR
touches the same files, expect to rebase after that batch lands. The upcoming review comment
will name the exact overlapping paths so you only have to do it once.

No action needed from you until then. Thanks for the contribution and for your patience.

Review tracker: devlog/_plan/260725_pr_issue_rework · marker posted by the maintainer review pass

@lidge-jun

Copy link
Copy Markdown
Owner

Thank you for building out the Grok image bridge. There are two merge-blocking issues in the current implementation.

First, src/images/artifacts.ts:83 fetches a provider-returned URL without validating its protocol, hostname, resolved addresses, or redirects. A malicious or compromised upstream can therefore make the local proxy request loopback, link-local, RFC1918, or other internal services (SSRF). Please require the intended HTTPS origin(s), run the existing destination/private-network policy before every request, revalidate every redirect target, and add negative tests for loopback/private/link-local hosts plus a redirect into a private address. Do not log the returned URL if it can contain credentials.

Second, src/server/responses/core.ts:1308 and src/server/responses/core.ts:1449 return through adapter.runTurn before the new image-plan branch executes. Cursor and any other runTurn adapter therefore never reach this bridge even when the request contains the advertised image-generation plan. Please integrate the bridge into the runTurn path or move the plan decision ahead of the early return, then add an end-to-end regression using a runTurn adapter that proves the bridge executes and its artifact is returned.

The outbound fetch is a network security boundary, so this PR also needs explicit maintainer security review after the fixes. I appreciate the substantial work here; these two paths need to be closed before we can safely expose it.

Please also rebase onto current dev, which now includes #436's validated Responses parser contract in src/responses/parser.ts and #439's context-pressure types in src/types.ts; preserve both while re-cutting the image bridge.


Reviewed as part of a maintainer integration pass. dev moved to ebc62d1f today (7 PRs integrated); Cross-platform CI is green on ubuntu, macOS, and Windows.

@tizerluo
tizerluo force-pushed the feat/image-bridge branch from db48e7c to 99fa843 Compare July 25, 2026 19:45
@lidge-jun

Copy link
Copy Markdown
Owner

Follow-up review at head 99fa843f, against dev after today's integration batch. git merge-tree is clean, and you've addressed the earlier findings — the missing additional_tools detection, the runTurn bypass, OAuth refresh, and the basic SSRF checks are all handled now. Two release blockers remain.

The paid bridge defaults to on. src/images/plan.ts:39 reads:

if (config.images?.bridgeEnabled === false) return undefined;

so merely having xAI configured is enough to route prompts into a paid generation path. A cross-provider feature that spends money on the operator's account should require them to say yes:

if (config.images?.bridgeEnabled !== true) return undefined;

Update src/types.ts to document the default as false, and correct docs-site/src/content/docs/guides/image-bridge.md, which currently describes the opposite. Existing positive plan tests will need { bridgeEnabled: true } passed explicitly, and a new negative test should assert that a fully configured xAI provider still yields no plan without the opt-in.

The credential destination is not pinned. plan.ts returns auth: { baseUrl: xai.baseUrl.replace(/\/+$/, ""), token } straight from persisted config, bypassing registry routing. A raw providers.xai.baseUrl override can therefore receive the built-in OAuth token. Separately, a custom-named provider pointing at api.x.ai falls through to getValidAccessToken("xai") and borrows the built-in account's token, which it should not.

Resolve the provider through routeModel() and make token resolution name-aware: an explicit apiKey on any provider is fine, but the built-in OAuth token should only be handed to the provider actually named xai. A regression should assert that with providers.xai.baseUrl set to a sink host, plan.auth.baseUrl is still https://api.x.ai/v1.

Status: held for explicit security review under MAINTAINERS.md — this consumes xAI OAuth or API keys and initiates paid, non-idempotent generation. Both fixes above should land before that review.

One coordination note: #355 also adds src/images/artifacts.ts, and pairwise merge shows a direct add/add conflict. Your version is the functional superset, but whichever lands second should extend the reviewed module rather than replace it wholesale.


Reviewed as part of a maintainer PABCD pass. Adds findings not present in the earlier pass.

lidge-jun added a commit that referenced this pull request Jul 26, 2026
통합(dev push 완료): #437 803807a, #460 74ddd96, #431 82a47db,
#405 be16c1d, #468 후속 테스트 bcaf029.

close: 통합 4건 + 통합 없이 1건(#459, 설계 충돌). #457은 OPEN 유지.
이슈 close 0건 — dev에서 고쳐졌는데 안 닫힌 이슈가 없었다.

무효화: WP4(#466)와 WP6(#467)은 동료가 직접 머지했고, 우리 분석이
지적한 결함을 동료 커밋이 같은 방향으로 해소했다.

보안 보류 5건에 신규 발견 게시: #429 재분류, #355 OAuth 토큰 목적지,
#424 유료 브리지 기본 ON, #408 설치 락 해제, #403 4분할 요청.

A-gate는 사이클마다 3~5라운드를 돌았다. 반복된 원인은 계획서에
테스트나 코드를 적으면서 실물 소스를 확인하지 않은 것이었다.
When a non-OpenAI model receives an image_generation hosted tool from Codex,
OpenAI's server-side execution is unavailable. This intercepts the tool call,
routes it to xAI Grok Imagine, materializes the image to ~/.opencodex/artifacts/,
and feeds the result back to the model — all inside a streaming SSE loop.

Architecture mirrors the proven web-search sidecar pattern:
- parser.ts: extract hosted image_generation tool, stash into parsed._imageGeneration
- plan.ts: decide whether to activate (xAI provider + token available, non-OpenAI route)
- loop.ts: agentic loop (max 3 rounds) — intercept image tool calls, fulfill via xAI,
  inject results, re-call model, bridge to SSE
- synthetic-tool.ts: buildImageTool() injection + isImageGenName() detection
- xai-client.ts: xAI /images/generations and /images/edits API client
- fulfill.ts: execute single image call, materialize to disk (never throws)
- artifacts.ts: extended with downloadImageToArtifact() + magic byte format detection

New files:
  src/images/{types,xai-client,synthetic-tool,fulfill,plan,loop,index}.ts
  tests/images/{xai-client,plan,fulfill,synthetic-tool,loop}.test.ts
  docs-site/src/content/docs/guides/image-bridge.md

Modified files:
  src/types.ts: +_imageGeneration stash, +OcxTool.imageGeneration flag, +OcxImagesConfig fields
  src/responses/parser.ts: extractHostedImageGeneration extraction
  src/server/responses/core.ts: image bridge trigger point
  src/images/artifacts.ts: +downloadImageToArtifact, +guessExtFromMagic, shared helpers
@tizerluo
tizerluo force-pushed the feat/image-bridge branch from 99fa843 to b3f2b49 Compare July 26, 2026 09:49
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.

2 participants