Skip to content

feat(videos): add Grok video bridge for non-OpenAI models - #582

Merged
Wibias merged 10 commits into
lidge-jun:devfrom
tizerluo:feat/video-bridge-v2
Jul 29, 2026
Merged

feat(videos): add Grok video bridge for non-OpenAI models#582
Wibias merged 10 commits into
lidge-jun:devfrom
tizerluo:feat/video-bridge-v2

Conversation

@tizerluo

@tizerluo tizerluo commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Extends the image bridge (#577) to support asynchronous video generation via xAI Grok Imagine Video. When videoBridgeEnabled: true, a synthetic video_gen tool is injected into the conversation. The model calls it like any function; opencodex intercepts the call, submits a video generation job to xAI, polls until completion, and downloads the result to disk.

Architecture: submit → poll with heartbeats → download to artifact → inject as tool_result → loop (max 2 rounds).

Changes

New files:

  • src/images/xai-video-client.ts — async xAI video client (submitVideoJob + pollVideoJob)
  • src/images/fulfill-video.ts — arg parsing, heartbeat polling generator, result builder
  • tests/videos/ — 32 tests across 3 files
  • docs-site/.../guides/video-bridge.md — user guide

Modified files:

  • src/images/loop.ts — unified media bridge loop (image + video dispatch), per-turn call caps (MAX_VIDEO_CALLS_PER_TURN = 3)
  • src/images/types.tsVideoBridgePlan, VideoCallResult
  • src/images/synthetic-tool.tsbuildVideoTool, VIDEO_GEN_TOOL_NAME
  • src/images/plan.tsplanVideoBridge (opt-in, API-key-only, registry-pinned baseUrl)
  • src/images/artifacts.tsdownloadVideoToArtifact (200 MiB cap, streaming, SSRF-protected via pinned HTTPS)
  • src/server/responses/core.ts — media bridge wiring with tool dedup
  • src/types.tsvideoGeneration flag, videoBridgeEnabled/videoBridgeModel/videoMaxRounds/videoTimeoutMs

Security

  • SSRF protection: downloadVideoToArtifact reuses the same assessUrlDestinationresolvePublicAddressespinnedHttpsGet pattern as the hardened image downloader
  • Size caps: 200 MiB hard cap on video downloads, 1 MiB on API responses
  • Per-turn limits: MAX_VIDEO_CALLS_PER_TURN = 3 prevents unbounded paid calls
  • Abort linking: client cancel propagates to in-flight xAI submit/poll/download

Configuration

{
  "images": {
    "bridgeEnabled": true,
    "videoBridgeEnabled": true,
    "videoBridgeModel": "grok-imagine-video",
    "videoMaxRounds": 2,
    "videoTimeoutMs": 300000
  }
}

Video bridge is opt-in (videoBridgeEnabled defaults to false). Coexists with the image bridge.

Test results

  • tsc --noEmit: clean
  • tests/videos/: 32 pass, 0 fail
  • tests/images/ (regression): all pass individually (pre-existing mock.module pollution when run together is unchanged)

Summary by CodeRabbit

  • New Features
    • Added optional Video Bridge to generate videos via xAI Grok Imagine Video, including async progress/heartbeats, job polling, and video artifact downloads.
    • Introduced a synthetic video tool with configurable enablement, model, max rounds, and timeouts (plus prompt/duration/resolution/aspect ratio support).
  • Bug Fixes
    • Improved combined image+video orchestration, including safer stream handling, tool-choice/media routing, and stricter video size/format validation.
  • Documentation
    • Added a Video Bridge guide with prerequisites and operational limits.
  • Tests
    • Added Bun coverage for video planning, tool-arg parsing, job submission/polling, timeouts, and result formatting.

Extends the image bridge to support asynchronous video generation via xAI
Grok Imagine Video. Video generation uses a submit→poll→download pattern
(POST /v1/videos/generations → GET /v1/videos/{id}) with heartbeat forwarding
to keep the SSE stream alive during the 30-180s generation window.

New files:
- src/images/xai-video-client.ts: submitVideoJob + pollVideoJob
- src/images/fulfill-video.ts: arg parsing, async polling generator, result builder
- tests/videos/: 32 tests (xai-video-client, fulfill-video, plan-video)
- docs-site/.../video-bridge.md: user guide

Modified files:
- src/images/loop.ts: unified image+video fulfillment, per-turn call caps
- src/images/types.ts: VideoBridgePlan/VideoCallResult shared types
- src/images/synthetic-tool.ts: buildVideoTool, VIDEO_GEN_TOOL_NAME
- src/images/plan.ts: planVideoBridge (opt-in via videoBridgeEnabled)
- src/images/artifacts.ts: downloadVideoToArtifact (200MB cap, SSRF-protected)
- src/server/responses/core.ts: video bridge wiring + tool dedup
- src/types.ts: videoGeneration flag, video config fields

Video bridge is opt-in (videoBridgeEnabled defaults to false). All upstream
image-bridge hardening preserved (SSRF, size caps, abort linking, pinned HTTPS).
@github-actions github-actions Bot added the enhancement New feature or request label Jul 28, 2026
@coderabbitai

coderabbitai Bot commented Jul 28, 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 xAI Video Bridge for eligible routed requests. It injects a synthetic video_gen tool, submits and polls asynchronous xAI jobs, downloads videos as artifacts, and integrates video fulfillment with the existing image bridge.

Changes

Video Bridge

Layer / File(s) Summary
Video bridge contracts and activation
src/types.ts, src/images/types.ts, src/images/synthetic-tool.ts, src/images/plan.ts, src/images/index.ts, src/server/responses/core.ts, tests/videos/plan-video.test.ts, docs-site/src/content/docs/guides/video-bridge.md
Adds video configuration and result types, defines video_gen, plans eligible xAI requests, wires streaming dispatch, and documents and tests activation behavior.
xAI video transport and artifact storage
src/images/xai-video-client.ts, src/images/artifacts.ts, tests/videos/xai-video-client.test.ts
Adds bounded job submission and polling, status normalization, secure video downloads, format detection, size budgets, cleanup, and client tests.
Video call fulfillment and media loop
src/images/fulfill-video.ts, src/images/loop.ts, tests/videos/fulfill-video.test.ts
Validates calls, polls with heartbeats and timeout handling, builds artifact results, enforces video limits, and combines video and image fulfillment.

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

Sequence Diagram(s)

sequenceDiagram
  participant Model
  participant MediaBridge
  participant xAI
  participant Artifacts
  Model->>MediaBridge: emit video_gen tool call
  MediaBridge->>xAI: submit video generation job
  xAI-->>MediaBridge: return request id
  MediaBridge->>xAI: poll until completion
  xAI-->>MediaBridge: return video URL
  MediaBridge->>Artifacts: download video
  Artifacts-->>MediaBridge: return local artifact path
  MediaBridge-->>Model: return video tool result
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 52.38% 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 clearly summarizes the main change: adding a Grok video 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.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@Wibias
Wibias marked this pull request as draft July 28, 2026 01:51
P1 fixes:
- Non-streaming requests no longer 400 when video bridge is enabled
  (only image bridge requires stream=true; video-only skips bridge for
  non-stream requests)
- Restore globalThis.fetch after video client tests (afterEach cleanup)

P2 fixes:
- Use clamped vidPlan.timeoutMs instead of raw config value
- Honor videoMaxRounds when both image+video bridges active (tighter wins)
- guessVideoExtFromMagic throws on unrecognized magic (was defaulting to mp4)
- Poll 4xx permanent failures (except 429) fail fast instead of retrying
- buildVideoResult uses pathToFileURL for cross-platform markdown links
- Initial poll interval restored to 5s (was 200ms)
- done-without-videoUrl returns error instead of spinning until timeout
- VideoBudget charges every streamed chunk (was only first chunk)
- downloadVideoToArtifact: reader cleanup on file-open failure
- readBoundedText: cancel reader body on size cap breach
- Namespaced video_gen tools preserved in dedup filter
- Fix JSON missing closing brace in docs
- Clarify API key auth requirement in docs prerequisites
@tizerluo

Copy link
Copy Markdown
Contributor Author

Review response (HEAD: 47225638)

All Codex P1/P2 and CodeRabbit findings addressed on tip:

P1 fixes

  • Non-streaming requests: Video bridge no longer turns every stream:false request into a 400. Only the image bridge (which detects a hosted image_generation tool) requires streaming. Video-only non-streaming requests skip the bridge entirely and proceed normally.
  • Test fetch leak: globalThis.fetch is now saved and restored in afterEach for xai-video-client.test.ts.

P2 fixes

Finding Fix
Video timeout not clamped Uses vidPlan.timeoutMs (clamped in plan) instead of raw config
videoMaxRounds ignored when both active Uses Math.min(image, video) when both plans are active
Unknown magic defaults to mp4 guessVideoExtFromMagic now throws for unrecognized formats
4xx poll failures retry forever 4xx (except 429) returns permanent failure immediately
Markdown path not portable Uses pathToFileURL(path).href matching fulfillImageCall
Initial poll interval too fast Restored to 5s (was 200ms)
done-without-URL spins Returns terminal error "no video URL was returned"
VideoBudget partial accounting Every streamed chunk now charged to budget
Reader leak on file-open failure Reader cancelled/released in all error paths
readBoundedText body not cancelled reader.cancel() added to finally
Namespaced video_gen stripped Dedup now checks namespacedToolName for video tools
Docs JSON missing } Fixed
Docs auth prerequisite Clarified API key requirement

Typecheck clean. All 33 video tests + 6 handler-activation tests pass.

…rtion

- Encode requestId in poll URL to prevent path injection (CodeRabbit)
- Move paidVideoCalls++ past arg validation so malformed calls don't burn budget
- Add early guard for undefined plan in image branch, removing non-null assertions
@Wibias
Wibias marked this pull request as ready for review July 28, 2026 15:14

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e09f32675

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/images/loop.ts Outdated
if (signal.aborted) throw new LoopError(499, "client closed request during video-bridge");
if (pollResult.ok) {
const dlPath = await downloadVideoToArtifact(pollResult.videoUrl, vBudget, signal);
pruneArtifacts(videoPlan?.artifactsKeepCount);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prune only after retaining the entire video batch

When one model iteration contains multiple video_gen calls and artifactsKeepCount is smaller than that batch (for example, two calls with a limit of 1), pruning after each download lets the second call delete the first video before either tool result is injected. The first result still reports ok: true with a path that no longer exists; retain and prune the completed batch atomically, sharing the image retention serialization, and add a focused multi-call regression test.

AGENTS.md reference: AGENTS.md:L93-L95

Useful? React with 👍 / 👎.


## Prerequisites

- An xAI account with an API key (set `XAI_API_KEY` or configure the key in your provider config — `ocx login xai` alone is not sufficient for the video bridge, which requires API key auth)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Show the required provider key reference

Setting XAI_API_KEY by itself does not arm this bridge: planVideoBridge only reads found.provider.apiKey, and the normal provider created by ocx login xai remains authMode: "oauth", which resolveXaiImageApiKey rejects even when the environment variable exists. A user following this prerequisite therefore gets no video_gen tool and no diagnostic; document an explicit providers.xai entry using apiKey: "${XAI_API_KEY}" and key auth, as the image-bridge guide does.

AGENTS.md reference: AGENTS.md:L96-L97

Useful? React with 👍 / 👎.

@Wibias Wibias left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maintainer review (bug + security)

Verdict: REQUEST CHANGES

Cross-platform CI is green. Security review found no medium+ issues (SSRF/download path matches the hardened image bridge: destination policy + pinned HTTPS + no redirects; API-key-only; registry-pinned xAI baseUrl; size caps; safe artifact paths).

Two fix commits already landed most of the first Codex/Rabbit pass (47225638, 5e09f326). Remaining blockers are still present on 5e09f326.

Open Codex / CodeRabbit (verified against HEAD)

Item Status
Codex P1: non-streaming 400 when video enabled Fixed — video-only + stream:false skips injection
Codex P1: video lost when web search wins Still open(imgPlan || vidPlan) && (!wsPlan || adapter.runTurn) still drops video on common Codex+web-search turns
Codex P2: videoTimeoutMs covers submit+poll Still open — poll deadline starts after submitVideoJob; submit has its own 60s
Codex P2: pass vidPlan.timeoutMs Fixed
Codex P2: honor videoMaxRounds with both bridges Fixed (Math.min)
Codex P2: reject unknown video magic Fixed
Codex P2: stop retrying permanent poll 4xx Fixed
Codex P2: pathToFileURL markdown Fixed
Codex P2: namespaced video_gen dedup Fixed (filter); aliases still not intercepted
Codex P2: poll starts at 5s Fixed
Codex P2: cancel reader on open failure / oversized body Fixed
Codex P2 / Rabbit: docs JSON braces Fixed
Codex P1: restore globalThis.fetch in tests Fixed
Rabbit critical: OAuth never activates Invalid / by design — API-key-only; docs + test cover this
Rabbit: done without videoUrl spins Fixed
Rabbit: paidVideoCalls before parse Fixed
Rabbit: encode requestId Fixed
Rabbit: errors carry .status Fixed
Rabbit: prune video artifacts Fixed
Rabbit: VideoBudget never enforces a turn ceiling Still openspent is updated, but no aggregate per-turn byte cap like images

Required before merge

  1. Web search coexistence: either compose video with runWithWebSearch, or document the same priority rule as the image bridge in video-bridge.md and surface a clear runtime signal when vidPlan is dropped because of wsPlan (silent loss of a config-enabled capability is the P1).
  2. Timeout budget: start one deadline before submit and bind submit+poll (and ideally download) to the remaining budget, matching VideoBridgePlan.timeoutMs docs — or change the docs/type comment if poll-only is intentional.
  3. Optional but preferred: enforce an aggregate VideoBudget ceiling (parity with image chargeImageBudget), and add tests for done-without-URL + encoded requestId.

Security

No medium+ findings. Same trust model as the image CDN downloader; OAuth-only xAI correctly does not arm the bridge.

- Move pruneArtifacts from per-download to post-batch so multi-video turns
  don't delete earlier videos before tool results are injected (Codex P2)
- Document that video bridge requires providers.xai with apiKey authMode,
  not OAuth from 'ocx login xai' (Codex P2)

@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

Caution

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

⚠️ Outside diff range comments (1)
tests/videos/fulfill-video.test.ts (1)

92-115: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Heartbeat test still sleeps for real — now ~10s per run instead of ~400ms.

The test doesn't mock sleep()/the poll interval; it works around the real delay by bumping the Bun test timeout to 15_000 (line 115). Since INITIAL_POLL_INTERVAL_MS was raised from 200ms to 5000ms in src/images/fulfill-video.ts, this single test now burns ~10 real seconds every CI run for two polls, where a mocked interval would make it instant and less flaky under load.

♻️ Proposed fix — inject/mock the interval instead of waiting for real time
-    const gen = pollVideoWithHeartbeats("r1", { baseUrl: "https://api.x.ai/v1", token: "t" }, ac.signal, 60_000);
+    // Mock the module's sleep so backoff doesn't cost real wall-clock time.
+    mock.module("../../src/images/fulfill-video", () => ({
+      ...actualModule,
+      sleep: () => Promise.resolve(),
+    }));
+    const gen = pollVideoWithHeartbeats("r1", { baseUrl: "https://api.x.ai/v1", token: "t" }, ac.signal, 60_000);

(or export/inject the poll interval as a parameter for tests)

🤖 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 `@tests/videos/fulfill-video.test.ts` around lines 92 - 115, Update the
heartbeat test around pollVideoWithHeartbeats so it does not wait for the real
INITIAL_POLL_INTERVAL_MS between polls. Mock the sleep/timer dependency used by
pollVideoWithHeartbeats, or inject a test poll interval, while preserving the
two-poll completion and heartbeat assertions; remove the extended 15-second
timeout once the test runs without real delays.
♻️ Duplicate comments (2)
src/images/loop.ts (1)

622-632: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Heartbeat progress text is generated but still discarded before reaching SSE.

pollVideoWithHeartbeats (src/images/fulfill-video.ts) yields { type: "heartbeat", message: "Generating video... ${elapsed}s" }, but line 628 here only re-yields { type: "heartbeat" }value.message is never forwarded. Combined with src/types.ts's heartbeat event shape lacking a message field, the elapsed-time text never reaches the client during the (documented) 30-120+ second async video generation window, leaving only a bare keepalive instead of the intended progress indicator.

🤖 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/images/loop.ts` around lines 622 - 632, Update the
pollVideoWithHeartbeats consumption in the loop around pollGen.next() to
preserve and re-yield the heartbeat message from value, and extend the heartbeat
event type in src/types.ts to include the optional message field so the
elapsed-time progress text reaches SSE clients.
src/server/responses/core.ts (1)

1630-1640: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

allowedTools/toolChoice.name still target image_gen when only the video bridge is active — this reported "addressed" fix is not actually present in the code.

Lines 1633 and 1639 unconditionally rewrite the literals "image_generation"/"image_gen" to IMAGE_GEN_TOOL_NAME, gated only by imgPlan?.toolNames.has(...) for the third disjunct. When only vidPlan is active (image bridge disabled), buildImageTool() is never pushed into bridgeTools (Line 1625 is gated on imgPlan), so a client sending tool_choice: { allowedTools: ["image_generation", "video_gen"] } gets rewritten to ["image_gen", "video_gen"] — but bridgeTools contains no image_gen entry, so the request now names an undeclared tool. Strict OpenAI-shaped backends reject allowed_tools referencing an undeclared tool with a 400 before the model runs.

This is unchanged code (no ~ marker in this update) despite the past review thread showing "✅ Addressed in commits 4722563 to 5e09f32" — that resolution marker appears stale/incorrect for this specific block.

🐛 Proposed fix
     if (tc && typeof tc === "object" && "allowedTools" in tc && Array.isArray(tc.allowedTools)) {
       const mapped = tc.allowedTools.map(name =>
-        name === "image_generation" || name === "image_gen" || (imgPlan?.toolNames.has(name) ?? false)
+        imgPlan && (name === "image_generation" || name === "image_gen" || imgPlan.toolNames.has(name))
           ? IMAGE_GEN_TOOL_NAME
           : name,
       );
       parsed.options.toolChoice = { ...tc, allowedTools: [...new Set(mapped)] };
     } else if (tc && typeof tc === "object" && "name" in tc && typeof tc.name === "string"
-      && (tc.name === "image_generation" || (imgPlan?.toolNames.has(tc.name) ?? false))) {
+      && imgPlan && (tc.name === "image_generation" || imgPlan.toolNames.has(tc.name))) {
       parsed.options.toolChoice = { ...tc, name: IMAGE_GEN_TOOL_NAME };
     }
🤖 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 1630 - 1640, Update the
allowedTools mapping and toolChoice.name handling in the tool-choice
normalization block so image tool aliases are rewritten to IMAGE_GEN_TOOL_NAME
only when the image bridge is active and that tool is declared; preserve aliases
when only the video bridge is active to avoid referencing an undeclared tool.
🤖 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/artifacts.ts`:
- Around line 566-580: The per-turn video budget is accounted for but never
enforced. In src/images/artifacts.ts:566-580, update downloadVideoToArtifact to
define or reuse a MAX_VIDEO_TURN_BYTES cap and check budget.spent after both the
first-chunk and streaming-loop accounting, throwing when the cumulative limit is
exceeded; in src/images/loop.ts:635-635, make no direct change and continue
passing vBudget unchanged.
- Around line 489-497: Update the download flow around guessVideoExtFromMagic
and the initial reader.read() to accumulate network chunks until at least 12
bytes are available before sniffing the video format. Buffer each received chunk
in firstChunks, preserve the existing size budget/cap accounting, then write all
buffered chunks to fh before entering the main read loop; only reject when the
stream ends before enough bytes are collected.

---

Outside diff comments:
In `@tests/videos/fulfill-video.test.ts`:
- Around line 92-115: Update the heartbeat test around pollVideoWithHeartbeats
so it does not wait for the real INITIAL_POLL_INTERVAL_MS between polls. Mock
the sleep/timer dependency used by pollVideoWithHeartbeats, or inject a test
poll interval, while preserving the two-poll completion and heartbeat
assertions; remove the extended 15-second timeout once the test runs without
real delays.

---

Duplicate comments:
In `@src/images/loop.ts`:
- Around line 622-632: Update the pollVideoWithHeartbeats consumption in the
loop around pollGen.next() to preserve and re-yield the heartbeat message from
value, and extend the heartbeat event type in src/types.ts to include the
optional message field so the elapsed-time progress text reaches SSE clients.

In `@src/server/responses/core.ts`:
- Around line 1630-1640: Update the allowedTools mapping and toolChoice.name
handling in the tool-choice normalization block so image tool aliases are
rewritten to IMAGE_GEN_TOOL_NAME only when the image bridge is active and that
tool is declared; preserve aliases when only the video bridge is active to avoid
referencing an undeclared tool.
🪄 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: b6acdc1a-a922-4970-960e-1a81d70cc163

📥 Commits

Reviewing files that changed from the base of the PR and between 78109c2 and 73cab25.

📒 Files selected for processing (8)
  • docs-site/src/content/docs/guides/video-bridge.md
  • src/images/artifacts.ts
  • src/images/fulfill-video.ts
  • src/images/loop.ts
  • src/images/xai-video-client.ts
  • src/server/responses/core.ts
  • tests/videos/fulfill-video.test.ts
  • tests/videos/xai-video-client.test.ts

Comment thread src/images/artifacts.ts
Comment thread src/images/artifacts.ts Outdated
…get ceiling

Address Wibias CHANGES_REQUESTED review:

1. Web search coexistence (P1):
   - When wsPlan is active, media bridge is skipped (existing behavior)
   - Now emits console.warn so the user sees the skip instead of silent loss
   - Documented priority rule in video-bridge.md

2. Timeout budget (P2):
   - Start deadline BEFORE submitVideoJob, not after
   - Poll receives remaining budget (deadline - now), min 5s floor
   - Submit (60s) + poll now share one videoTimeoutMs deadline

3. VideoBudget aggregate ceiling (optional, addressed):
   - VideoBudget now has a cap (600 MiB = 3 × single-download max)
   - chargeVideoBudget() enforces the ceiling per-chunk during streaming
   - Exceeding the budget throws mid-download (partial file is cleaned up)

4. New tests: done-without-videoUrl, permanent 4xx poll stop, requestId encoding
@tizerluo

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review! All three items addressed in bb543844:

1. Web search coexistence (P1)
Documented the priority rule in video-bridge.md and added a console.warn runtime signal when vidPlan is dropped because of wsPlan. The media bridge cannot run alongside runWithWebSearch without a compose layer; making the skip visible is the right tradeoff for now.

2. Timeout budget (P2)
The deadline is now captured before submitVideoJob (line 613). The poll generator receives remainingMs = max(5_000, deadline - Date.now()) so submit (60 s) + polling share one videoTimeoutMs budget. Matches the type comment on VideoBridgePlan.timeoutMs.

3. VideoBudget ceiling (optional, done)
VideoBudget now carries a cap (600 MiB = MAX_VIDEO_DOWNLOAD_BYTES × 3). chargeVideoBudget() is called per-chunk during streaming download; exceeding the ceiling throws mid-download and the partial file is cleaned up in the catch block — same pattern as chargeImageBudget.

New tests (36 total, all passing):

  • done without videoUrl → returns error, not infinite poll
  • Permanent 4xx (401) poll error → stops immediately, no retry storm
  • requestId encoding → special characters encoded in poll URL

@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 (1)
src/server/responses/core.ts (1)

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

videoTimeoutMs is not a shared submit-and-poll deadline.

videoDeadline is computed before submission, but submitVideoJob() receives only signal, so it may consume its separate submit timeout after the configured deadline. Then Math.max(5_000, ...) permits an additional poll window even when the deadline has already expired.

  • src/server/responses/core.ts#L613-L624: create a deadline-bound signal before submission, pass it to submission, and return a timeout result without polling when no budget remains; do not floor an expired budget to five seconds.
  • docs-site/src/content/docs/guides/video-bridge.md#L85-L85: retain this claim only after the runtime deadline is enforced; otherwise document the actual independent submit/poll behavior.
🤖 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 613 - 624, Update the video
request flow around videoDeadline and submitVideoJob in
src/server/responses/core.ts lines 613-624 to create one deadline-bound signal
before submission, pass it to submission, and return a timeout result without
polling when no budget remains; remove the five-second floor for expired
budgets. After enforcing this shared deadline, retain the existing
videoTimeoutMs claim in docs-site/src/content/docs/guides/video-bridge.md line
85; otherwise document the actual independent submit/poll behavior there.

Source: Path instructions

♻️ Duplicate comments (2)
src/server/responses/core.ts (1)

1638-1647: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Regression: gate image tool-choice rewriting on imgPlan.

When only vidPlan is active, this still rewrites image_generation/image_gen to image_gen although no synthetic image tool was injected. Strict upstreams can reject the resulting undeclared allowedTools entry. This is the previously addressed issue, but it remains in the current code.

Proposed fix
-        name === "image_generation" || name === "image_gen" || (imgPlan?.toolNames.has(name) ?? false)
+        imgPlan && (name === "image_generation" || name === "image_gen" || imgPlan.toolNames.has(name))
           ? IMAGE_GEN_TOOL_NAME
           : name,

Apply the same imgPlan gate to the named-tool branch on Lines 1645-1647.

🤖 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 1638 - 1647, Gate the named-tool
rewriting branch in the tool-choice normalization logic on imgPlan, alongside
its existing image-name checks. Update the condition associated with tc.name so
image_generation or planned image tools are rewritten to IMAGE_GEN_TOOL_NAME
only when imgPlan is active; leave the allowedTools branch unchanged.
src/images/artifacts.ts (1)

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

Buffer enough bytes before sniffing the video format.

reader.read() is not guaranteed to return 12 bytes. Calling guessVideoExtFromMagic(first.value) immediately can reject a valid video when its first network chunk is short. Buffer reads until the sniffing minimum is available, then charge and write every buffered chunk through the existing cap/budget path.

🤖 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/images/artifacts.ts` around lines 570 - 586, Update the video download
flow around reader.read and guessVideoExtFromMagic to accumulate chunks until
the minimum sniffing bytes are available, while still handling an empty stream.
Then determine the extension from the buffered bytes and charge, cap-check, and
write every buffered chunk through the existing accounting path, preserving
limits for subsequent reads.
🤖 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 `@docs-site/src/content/docs/guides/video-bridge.md`:
- Line 84: Update the “Web search priority” documentation to scope the warning
and video-bridge skip behavior to the runnable web-search sidecar path,
specifically when web search can run without an adapter runTurn handler. Avoid
claiming that all active web-search turns skip media bridging, since runTurn
adapters follow the media bridge path.

---

Outside diff comments:
In `@src/server/responses/core.ts`:
- Around line 613-624: Update the video request flow around videoDeadline and
submitVideoJob in src/server/responses/core.ts lines 613-624 to create one
deadline-bound signal before submission, pass it to submission, and return a
timeout result without polling when no budget remains; remove the five-second
floor for expired budgets. After enforcing this shared deadline, retain the
existing videoTimeoutMs claim in
docs-site/src/content/docs/guides/video-bridge.md line 85; otherwise document
the actual independent submit/poll behavior there.

---

Duplicate comments:
In `@src/images/artifacts.ts`:
- Around line 570-586: Update the video download flow around reader.read and
guessVideoExtFromMagic to accumulate chunks until the minimum sniffing bytes are
available, while still handling an empty stream. Then determine the extension
from the buffered bytes and charge, cap-check, and write every buffered chunk
through the existing accounting path, preserving limits for subsequent reads.

In `@src/server/responses/core.ts`:
- Around line 1638-1647: Gate the named-tool rewriting branch in the tool-choice
normalization logic on imgPlan, alongside its existing image-name checks. Update
the condition associated with tc.name so image_generation or planned image tools
are rewritten to IMAGE_GEN_TOOL_NAME only when imgPlan is active; leave the
allowedTools branch unchanged.
🪄 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: 841180d8-0ac3-476a-9bf8-745d2cf42cbd

📥 Commits

Reviewing files that changed from the base of the PR and between 73cab25 and bb54384.

📒 Files selected for processing (6)
  • docs-site/src/content/docs/guides/video-bridge.md
  • src/images/artifacts.ts
  • src/images/loop.ts
  • src/server/responses/core.ts
  • tests/videos/fulfill-video.test.ts
  • tests/videos/xai-video-client.test.ts

- **Cost**: Video generation is a paid xAI feature (~$0.05/sec @480p, ~$0.07/sec @720p)
- **One video per call**: Each `video_gen` call produces one video
- **Coexists with Image Bridge**: Both bridges can be enabled simultaneously
- **Web search priority**: When web search is active for a turn, the video bridge is skipped (web search and media bridging cannot run concurrently). A `console.warn` is emitted so you can detect this in logs.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Scope the web-search warning to the runnable sidecar path.

src/server/responses/core.ts Lines 1601 and 1610 skip media only when canRunWebSearch is true (wsPlan && !adapter.runTurn). A runTurn adapter with a web-search plan instead enters the media bridge, so this absolute statement is inaccurate. Rephrase it to cover the runnable web-search-sidecar path, or make runtime behavior universally match the documented priority.

🤖 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/video-bridge.md` at line 84, Update the
“Web search priority” documentation to scope the warning and video-bridge skip
behavior to the runnable web-search sidecar path, specifically when web search
can run without an adapter runTurn handler. Avoid claiming that all active
web-search turns skip media bridging, since runTurn adapters follow the media
bridge path.

Source: Path instructions

@Wibias Wibias left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maintainer re-review (post bb543844)

Verdict: REQUEST CHANGES

Security: still no medium+. Cross-platform CI mostly green (windows-latest still pending at review time). Focused tests/videos locally: xAI client suite passes; fulfill/plan files need bun install in this worktree (zod/v4) — rely on CI for full gate.

Prior required items vs bb543844

Item Status
Web search coexistence (docs + runtime warn) Fixed — warn gated on canRunWebSearch (wsPlan && !adapter.runTurn) at core.ts:1601-1608; docs note priority
Timeout budget submit+poll Partial — deadline clock starts before submit (loop.ts:613), but submit is still only bound to client abort + fixed SUBMIT_TIMEOUT_MS (60s); poll uses Math.max(5_000, remaining) which can extend past the deadline
VideoBudget aggregate ceiling Fixedcap + chargeVideoBudget per chunk

Author response matches intent; timeout claim in docs (video-bridge.md:85) is still stronger than runtime.

New CodeRabbit findings (verified)

Finding Verdict Action
B. Gate image tool_choice rewrite on imgPlan (core.ts:1638-1647) Valid / required Video-only still rewrites image_generation/image_genimage_gen even though buildImageTool() was not injected. Strict upstreams can 400 on undeclared allowedTools. Gate both allowedTools + named branches on imgPlan.
C. Shared videoTimeoutMs incomplete (loop.ts:613-624 + xai-video-client.ts:78-79) Valid / required Create a deadline-bound signal before submit, pass it into submitVideoJob, skip/fail poll when remaining ≤ 0 (no 5s floor on expired budget). Keep docs line 85 only after that, or document independent submit/poll timeouts.
A. Buffer ≥12 bytes before magic sniff (artifacts.ts:570-574) Valid / preferred Real failure mode if first reader.read() is short; not the common path. Buffer until sniff minimum, then charge/write all buffered chunks.
D. Scope web-search docs to sidecar path Optional docs Runtime warn is already correctly scoped. Soften docs “when web search is active” → runnable sidecar / non-runTurn path if you want parity with the code comment at core.ts:1591-1595.

Also noted (non-blocking)

  • Heartbeat message still dropped in the loop (keepalive only) — UX.
  • Heartbeat test still sleeps real 5s intervals — CI cost only.
  • Video tool_choice aliases are not remapped the way image aliases are — follow-up.

Required before merge

  1. Gate image tool-choice rewriting on imgPlan (Rabbit B).
  2. Finish the shared timeout: deadline-bound signal into submit + no poll floor after expiry; align docs (Rabbit C).

Preferred: buffer magic sniff (A). Optional: docs wording (D).

…fer magic sniff

Address Wibias second CHANGES_REQUESTED review:

Required:
B. Gate image tool_choice rewriting on imgPlan — video-only turns no longer
   rewrite image_generation/image_gen aliases to an undeclared tool (core.ts)
C. Shared timeout complete — deadline-bound AbortSignal passed into submitVideoJob;
   if budget expired after submit, poll is skipped with timeout error (no 5s floor)

Preferred:
A. Buffer ≥12 bytes before magic-byte sniff — accumulate chunks until sniff minimum
   so short first reads don't crash guessVideoExtFromMagic (artifacts.ts)

Optional:
D. Docs: scope web-search priority to runnable sidecar / non-runTurn path
@tizerluo

Copy link
Copy Markdown
Contributor Author

All items addressed in a9069236:

Required B — Gate tool_choice on imgPlan
Both the allowedTools map and the named tc.name rewrite are now gated behind if (imgPlan && ...). In a video-only turn, image_generation/image_gen aliases pass through untouched.

Required C — Shared timeout complete

  • AbortSignal.timeout(videoTimeout) is created before submitVideoJob and linked with the client signal via AbortSignal.any(). Submit is bound to the shared deadline, not just SUBMIT_TIMEOUT_MS.
  • After submit returns, remainingMs = videoDeadline - Date.now(). If remainingMs <= 0, poll is skipped entirely with a timeout error — no 5s floor on an expired budget.
  • Poll receives only the actual remaining budget.

Preferred A — Buffer magic sniff
downloadVideoToArtifact now accumulates chunks in a loop until it has ≥12 bytes before calling guessVideoExtFromMagic. All buffered bytes are written + charged in one pass.

Optional D — Docs wording
Clarified: "When a web search sidecar is active for a turn (non-runTurn adapter)..."

36 tests pass, typecheck clean.

@Wibias Wibias left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maintainer re-review (post a906923)

Verdict: REQUEST CHANGES

HEAD: a9069236. Required B / preferred A / optional D from the prior review are fixed. CodeRabbit's pass on a906923 found nothing new; no new Codex review after that commit. Cross-platform CI mostly green (windows-latest pending at review time).

Prior required items vs a906923

Item Status
B. Gate image tool_choice on imgPlan Fixed — both allowedTools and named branches gated
C. Shared videoTimeoutMs Still incomplete — submit is deadline-bound and the post-submit remainingMs <= 0 floor is gone, but poll still ignores the deadline signal
A. Buffer ≥12 bytes before magic sniff Fixed
D. Web-search docs wording Fixed

Required before merge

Finish shared timeout through poll (loop.ts ~633 + fulfill-video.ts / xai-video-client.ts)

linkedDeadline is passed into submitVideoJob, but pollVideoWithHeartbeats(..., signal, remainingMs) still receives only the client abort signal. An in-flight pollVideoJob can run up to POLL_TIMEOUT_MS (30s) and sleep(interval) up to 15s after the wall-clock budget is gone. Docs still claim submit+poll share one deadline.

Fix: pass linkedDeadline (or AbortSignal.any([signal, AbortSignal.timeout(remainingMs)])) into the poll generator so poll fetch + sleep abort when the budget ends. Optionally clamp each poll request timeout to min(POLL_TIMEOUT_MS, remaining).

Bot triage (not blocking this round)

Finding Verdict
Codex P1 web-search drops video Accepted / by design — docs + console.warn
Codex P2 poll interval / body cancel / docs JSON / fetch restore Fixed
Rabbit OAuth never activates Invalid / by design — API-key-only
Video aliases / test authMode: "api_key" / DNS "image URL" noun / heartbeat real sleep Follow-up / trivial

Pass linkedDeadline (not raw client signal) into pollVideoWithHeartbeats so
in-flight pollVideoJob fetches and sleep() calls abort when the wall-clock
budget expires. Abort error messages now distinguish deadline-expiry from
client-cancel based on elapsed time vs timeoutMs threshold.
@tizerluo

Copy link
Copy Markdown
Contributor Author

Fixed in c613b05e:

Shared timeout through poll

  • pollVideoWithHeartbeats now receives linkedDeadline (the deadline-bound signal), not the raw client signal
  • In-flight pollVideoJob fetches abort when the budget expires (was: could run up to POLL_TIMEOUT_MS = 30s past deadline)
  • sleep(interval) aborts on deadline expiry (was: could sleep up to 15s past deadline)
  • Abort error messages now distinguish deadline-expiry from client-cancel using an elapsed-time threshold (95% of timeoutMs)

Submit + poll + sleep all share one videoTimeoutMs deadline. Docs claim matches runtime. 36 tests pass, typecheck clean.

@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.

Caution

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

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

696-698: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Protect all batch artifacts before pruning.

Deferring pruneArtifacts until after the batch is not sufficient when keepCount is smaller than the number of artifacts produced in that batch. For example, with two generated files and keepCount = 1, pruning can delete the first path before it is serialized into messages, leaving the model a stale artifact link. Mixed image/video turns can also apply the video keep count to image artifacts.

Use the shared batch-aware retention path, or defer pruning while protecting every artifact path included in the pending tool results.

🤖 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/images/loop.ts` around lines 696 - 698, Update the batch artifact
handling around pruneArtifacts so every artifact path referenced by pending tool
results remains protected until serialization completes. Use the shared
batch-aware retention mechanism, or defer pruning while tracking all generated
image and video paths; do not apply the video keep count to image artifacts, and
preserve valid artifact links in messages.

652-655: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Classify submission deadline aborts as timeouts.

submitVideoJob receives the combined deadline signal, but this catch only checks the client signal. If the configured timeout aborts submission, the code returns a generic abort message instead of video generation timed out after ..., bypassing the timeout-specific handling used during polling.

Proposed fix
                 } catch (e) {
                   if (signal.aborted) throw new LoopError(499, "client closed request during video-bridge");
+                  if (deadlineSignal.aborted) {
+                    vResult = {
+                      ok: false,
+                      model: videoPlan!.model,
+                      prompt: vArgs.prompt,
+                      files: [],
+                      count: 0,
+                      error: `video generation timed out after ${Math.floor(videoTimeout / 1000)}s`,
+                    };
+                  } else {
                   const error = e instanceof Error ? e.message : String(e);
                   vResult = { ok: false, model: videoPlan!.model, prompt: vArgs.prompt ?? "", files: [], count: 0, error };
+                  }
                 }
🤖 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/images/loop.ts` around lines 652 - 655, Update the catch around
submitVideoJob to distinguish configured deadline-signal aborts from
client-request aborts. When the submission deadline aborts, route it through the
same timeout-specific handling used during polling so the result reports “video
generation timed out after ...”; preserve the existing 499 client-closed
behavior for signal.aborted caused by the client.
🤖 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.

Outside diff comments:
In `@src/images/loop.ts`:
- Around line 696-698: Update the batch artifact handling around pruneArtifacts
so every artifact path referenced by pending tool results remains protected
until serialization completes. Use the shared batch-aware retention mechanism,
or defer pruning while tracking all generated image and video paths; do not
apply the video keep count to image artifacts, and preserve valid artifact links
in messages.
- Around line 652-655: Update the catch around submitVideoJob to distinguish
configured deadline-signal aborts from client-request aborts. When the
submission deadline aborts, route it through the same timeout-specific handling
used during polling so the result reports “video generation timed out after
...”; preserve the existing 499 client-closed behavior for signal.aborted caused
by the client.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f8ec8cb3-5e78-4d12-a29e-9d3c45161e2f

📥 Commits

Reviewing files that changed from the base of the PR and between a906923 and c613b05.

📒 Files selected for processing (2)
  • src/images/fulfill-video.ts
  • src/images/loop.ts

@Wibias Wibias left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maintainer re-review (post c613b05)

Verdict: REQUEST CHANGES

HEAD: c613b05e. The prior required item (pass linkedDeadline into poll) is in place — submit, poll fetch, and sleep now share one deadline signal. CodeRabbit's pass on c613b05 raised two more findings (verified below). No new Codex review on this tip.

Prior required vs c613b05

Item Status
Shared timeout through poll (linkedDeadline into poll generator) Fixedloop.ts passes linkedDeadline; poll fetch + sleep abort on budget expiry
Timeout / abort classification Still incomplete — see #1
Batch prune vs delivered paths Still incomplete — see #2 (CodeRabbit Major, verified)

Required before merge

1. Classify deadline aborts without the 0.95 heuristic (fulfill-video.ts ~109–128, loop.ts ~652–655)

The new elapsed≥95% check mislabels a real deadline abort as "client closed" when submit consumed most of the budget (e.g. 58s of a 60s videoTimeoutMsremainingMs≈2s → deadline fires with elapsed ≪ 95% of remaining). Bun's AbortSignal.timeout / AbortSignal.any propagate TimeoutError on signal.reason — use that (or deadlineSignal.aborted declared outside the try) instead of a wall-clock fraction.

CodeRabbit (verified) — submit catch at loop.ts:652-655: submitVideoJob already receives linkedDeadline, but the catch only checks the client signal. A configured-timeout abort during submit returns a generic abort string instead of video generation timed out after Ns, skipping the timeout-specific handling used during polling. Declare deadlineSignal outside the try and when deadlineSignal.aborted (and the client signal is not), return the same timeout error used for poll expiry.

2. Don't hand the model pruned-away artifact paths (loop.ts ~696–698, CodeRabbit Major — verified)

Deferring pruneArtifacts until after the batch is necessary but not sufficient when artifactsKeepCount is smaller than that batch (e.g. two video_gen successes with keepCount 1). Prune can delete the older file before messages are built, so the tool_result still reports ok: true with a dead path. Image fulfillment already filters via retainAfterBatch + existsSync in fulfill.ts. Reuse that pattern (export/share retainAfterBatch, or prune then drop/rewrite results whose paths no longer exist) so delivered results never point at deleted files.

Note: the "video keepCount applied to images" angle is overstated — both plans read the same config.images.artifactsKeepCount. The stale-path failure mode is still real and should be fixed here.

3. Wire video aliases or remove the dead helpers (synthetic-tool.ts VIDEO_GEN_NAMES / isVideoGenName, plan.ts, loop.ts, core.ts)

isVideoGenName is defined but unused. Seed videoPlan.toolNames with VIDEO_GEN_TOOL_NAME plus any unnamespaced request tools that match isVideoGenName (image-bridge parity), and intercept those names in the loop. While touching the filter: only strip by leaf name when !t.namespace so a namespaced MCP video_gen is not removed.

4. Fix plan-video test fixtures (tests/videos/plan-video.test.ts)

authMode: "api_key" is not a valid OcxProviderConfig value ("key" | "forward" | "oauth" | "local"). Casts hide it today; use "key".

Also fix while here (small, same PR)

  • DNS error noun: resolvePublicAddresses hardcodes "image URL …" and surfaces that on video CDN failures. Add an optional noun (default "image") and pass "video" from downloadVideoToArtifact.

Not blocking

Finding Verdict
Web search wins over video Accepted / by design — docs + warn
Heartbeat test real 5s sleep CI cost only

…aliases, DNS noun

Address Wibias fourth CHANGES_REQUESTED review:

1. Deadline abort classification (required lidge-jun#1):
   - Replace 0.95 wall-clock heuristic with signal.reason.name === 'TimeoutError'
   - deadlineSignal declared outside try so catch can check deadlineSignal.aborted
   - Submit catch returns timeout error when deadlineSignal fires (not generic abort)

2. Don't deliver pruned artifact paths (required lidge-jun#2):
   - After batch prune, scan fulfilled results and rewrite any whose path
     no longer exists to ok:false with explanatory error

3. Wire video aliases (required lidge-jun#3):
   - planVideoBridge seeds toolNames with VIDEO_GEN_TOOL_NAME + any request
     function tools matching isVideoGenName (image-bridge parity)
   - core.ts filter only strips unnamespaced video_gen aliases (namespaced
     MCP tools are left alone)

4. Fix test fixtures (required lidge-jun#4):
   - authMode 'api_key' → 'key' (valid OcxProviderConfig value)

5. DNS error noun (small):
   - resolvePublicAddresses accepts optional noun param (default 'image')
   - downloadVideoToArtifact passes 'video'
@tizerluo

Copy link
Copy Markdown
Contributor Author

All items addressed in 938f25eb:

1. Deadline abort classification

  • Removed the 0.95 wall-clock heuristic. Now uses signal.reason.name === "TimeoutError" (Bun propagates TimeoutError on AbortSignal.timeout aborts).
  • deadlineSignal declared outside the try so the catch can inspect deadlineSignal.aborted.
  • Submit catch: when deadlineSignal.aborted && !signal.aborted, returns video generation timed out after Ns.

2. Pruned artifact paths

  • After pruneArtifacts, scan fulfilled[] and rewrite any result whose path no longer exists to { ok: false, error: "artifact was pruned before delivery (increase artifactsKeepCount)" }.

3. Video aliases wired

  • planVideoBridge seeds toolNames from VIDEO_GEN_TOOL_NAME + any request function tools matching isVideoGenName.
  • core.ts filter only strips unnamespaced video_gen aliases — namespaced MCP video_gen is preserved.
  • isVideoGenName is now actually used (was dead code).

4. Test fixture

  • authMode: "api_key""key" in both makeConfig and makeProvider.

5. DNS error noun

  • resolvePublicAddresses(url, noun="image")downloadVideoToArtifact passes "video".

36 tests pass, typecheck clean.

@Wibias Wibias left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maintainer re-review (post 938f25e)

Verdict: REQUEST CHANGES

HEAD: 938f25eb. Required items from the prior review are largely in. No new Codex review on this tip; CodeRabbit auto-paused (no new actionable comments). macos/windows CI green; ubuntu-latest cancelled after hitting the 12m job timeout during Test (typecheck had passed) — needs a green re-run before merge.

Prior required vs 938f25e

Item Status
#1 Deadline abort classification (TimeoutError / deadlineSignal outside try) Fixed — poll + submit catch verified; Bun propagates TimeoutError through AbortSignal.any
#2 Pruned-path guard Partial — see below
#3 Wire video aliases + unnamespaced strip Partial — see below
#4 authMode: "key" in tests Fixed
DNS noun "video" Fixed

Required before merge

1. Finish the pruned-path guard (loop.ts ~705–710)

Rewriting ok: false still spreads the old result, so markdown keeps the dead file: URI in the tool_result JSON. Also only path is checked — multi-image results can keep deleted entries in files[] after the post-batch prune.

Do the retainAfterBatch equivalent: filter files with existsSync; if none remain → ok: false with the prune error and no markdown/path; if some remain → refresh path/files/count/markdown from survivors only.

2. Don’t seed namespaced tools into videoPlan.toolNames (plan.ts alias loop)

core.ts correctly strips only !t.namespace, but planVideoBridge still adds every isVideoGenName(t.name) match, including MCP tools with a namespace. Those leaf names then make videoPlan.toolNames.has(call.name) intercept the MCP call in the loop. Skip namespaced tools when seeding (if (t.namespace) continue), matching the filter rule you already landed.

3. Green ubuntu CI

ubuntu-latest on 938f25e hit the 12m execution cap mid-Test (cancelled, not a clean fail). Re-run Cross-platform CI. If it keeps timing out, mock sleep / poll interval in tests/videos/fulfill-video.test.ts (the real 5s×2 heartbeat wait) so the suite stays under the cap — that was previously “CI cost only”; it’s now merge-relevant.

Not blocking

Finding Verdict
Web search wins over video Accepted / by design
CodeRabbit paused No new findings to triage

…k sleep in tests

Address Wibias fifth CHANGES_REQUESTED review:

1. Finish pruned-path guard:
   - Filter files[] through existsSync, not just path
   - If all pruned: ok:false with no markdown/path
   - If some survive: refresh path/files/count/markdown from survivors

2. Skip namespaced tools when seeding videoPlan.toolNames:
   - if (t.namespace) continue — matches core.ts filter rule

3. Fix ubuntu CI timeout:
   - Mock setTimeout in heartbeat test so 5s poll interval resolves instantly
   - Suite now runs in ~156ms (was ~5s)
@tizerluo

Copy link
Copy Markdown
Contributor Author

All three items addressed in cf76fb1e:

1. Finish pruned-path guard

  • files[] is now filtered through existsSync (was: only checked path)
  • If all files pruned → { ok: false, files: [], count: 0, error: "..." } with no markdown or path
  • If some survive → path/files/count/markdown refreshed from survivors only
  • Works for both single-video and multi-image results

2. Skip namespaced tools

  • planVideoBridge alias loop: if (t.namespace) continue before checking isVideoGenName
  • Namespaced MCP video_gen tools are no longer seeded into videoPlan.toolNames, matching the core.ts filter rule

3. Ubuntu CI timeout

  • Heartbeat test mocks globalThis.setTimeout to resolve instantly
  • Full suite: 156ms (was ~5.2s)
  • No real sleep(5000) wait in any test path

36 tests pass, typecheck clean. Ready for CI re-run.

cf76fb1 mocked globalThis.setTimeout to skip the 5s poll sleep. That races
parallel Bun workers and cancelled ubuntu-latest at the 12m job cap
(938f25e was green; cf76fb1 hung). Inject sleep/poll seams instead and drop
mock.module so video tests no longer poison each other.
Wibias
Wibias previously requested changes Jul 28, 2026

@Wibias Wibias left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maintainer re-review (post cf76fb1e + 50bd696)

Verdict: REQUEST CHANGES (merge hold = green ubuntu only)

Prior required vs author cf76fb1e

Item Status
#1 Finish pruned-path guard (files[] + no dead markdown/path) Fixed
#2 Skip namespaced tools when seeding videoPlan.toolNames Fixed
#3 Green ubuntu CI Not fixed by cf76fb1 — see below

CI hang root cause (verified)

938f25e had green Cross-platform CI. cf76fb1e re-cancelled ubuntu-latest at the 12m Test cap. The new “fix” mocked globalThis.setTimeout to no-op the 5s poll sleep. That races parallel Bun workers and hung the suite (~11 min silence after CLI status tests).

Pushed to this PR as 50bd696 (maintainer can-modify):

  • Injectable sleepFn / pollFn seams on pollVideoWithHeartbeats
  • Removed global setTimeout monkeypatch
  • Dropped mock.module from fulfill-video tests (also stopped cross-file pollution when tests/videos/* run together)

Bot triage (this tip)

Finding Verdict
Codex: cancel oversized bodies Already reader.cancel() in readBoundedText finally — skip
Codex: docs JSON brace / provider key Fixed in docs
Codex: restore fetch Fixed (afterEach)
Rabbit Critical: OAuth never activates By design / docs — skip
Rabbit Major: videoTimeoutMs clamp disagreement Invalid — plan clamps; core passes vidPlan.timeoutMs
Rabbit: authMode in tests / JSON reparse / web-search docs wording Fixed or trivial — skip
Codex prune-after-batch Fixed earlier

Required before merge

  1. Green ubuntu-latest on 50bd696 (or tip). macos/windows were already green on the prior tip.
  2. No further product changes needed from the prior review list.

Not merging from here.

@Wibias

Wibias commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the Pr. Will get merged after interal code work lands on dev.

@Wibias
Wibias merged commit 257c626 into lidge-jun:dev Jul 29, 2026
9 checks passed
Wibias added a commit that referenced this pull request Jul 29, 2026
TS oracle + docs on dev2-go. Go video port tracked separately — image bridge exists under go/internal/images/ but no video yet. Source: 257c626 (#582).
@Wibias

Wibias commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Transition workflow completed

  1. Merged into dev: 257c626c (feat(videos): add Grok video bridge for non-OpenAI models #582)
  2. Carried onto dev2-go: feat(videos): carry Grok video bridge onto dev2-go (#582) #660 (TS oracle + docs + tests)
  3. Go port: needed — image bridge exists under go/internal/images/, but no video counterpart yet. Tracking: port(go): Grok video bridge from #582 #661 (needs-go-port)

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