Skip to content

fix(daemon): Route ACP images through the vision bridge - #6111

Merged
yiliang114 merged 4 commits into
QwenLM:mainfrom
yiliang114:codex/issue-6110-acp-vision-bridge
Jul 1, 2026
Merged

fix(daemon): Route ACP images through the vision bridge#6111
yiliang114 merged 4 commits into
QwenLM:mainfrom
yiliang114:codex/issue-6110-acp-vision-bridge

Conversation

@yiliang114

@yiliang114 yiliang114 commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Routes daemon/ACP image prompts through the existing vision bridge before they are sent to the primary model. When the bridge is enabled for a text-only primary model, ACP now converts image parts to text with the configured vision model instead of letting the downstream converter replace images with placeholders. It also preserves unsupported image file parts while resolving @ paths when the bridge can handle them.

Why it's needed

The interactive CLI already applies the vision bridge, but the daemon/ACP prompt path resolved image parts and sent them directly to the primary model. With a config such as a text-only qwen3.7-max primary model plus a same-provider qwen3.7-plus vision-capable model, image prompts still reached qwen3.7-max and were degraded to text placeholders. This makes the daemon behavior match the CLI behavior and lets existing provider/model discovery work for ACP image prompts.

Reviewer Test Plan

How to verify

Configure a text-only primary model with an available vision bridge model, then send an ACP image prompt through qwen serve. The prompt should be converted by the bridge before the primary model call; the downstream message should contain the image transcript and should not contain raw inlineData image parts for the text-only primary model. The targeted regression test mocks the bridge and verifies that ACP calls it and forwards converted text.

Evidence (Before & After)

Before: the reported daemon logs showed Model 'qwen3.7-max' does not support image input. Replacing with text placeholder: image/png, and no vision bridge conversion logs appeared. After: the regression test verifies runVisionBridge is invoked for an ACP image prompt and the primary chat receives the converted transcript without inlineData. Local daemon smoke also verified qwen serve startup, /health, ACP initialize, and ACP session/new with mcpServers: []. Manual Web UI verification also passed:

Manual Web UI verification screenshot

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

Local macOS worktree with Node.js 22. Verified qwen serve on 127.0.0.1:4171 using the dev entrypoint and --no-web.

Risk & Scope

  • Main risk or tradeoff: ACP image prompts can now trigger the configured vision bridge side call when the primary model is text-only, matching the existing CLI behavior and bridge notice semantics.
  • Not validated / out of scope: a live paid/provider image prompt was not sent during local smoke; the regression test covers the bridge routing without consuming model quota.
  • Breaking changes / migration notes: None.

Linked Issues

Resolves #6110
Related #6086

中文说明

这个 PR 做了什么

将 daemon/ACP 图片 prompt 在发送给主模型之前接入现有 vision bridge。当主模型不支持图片但配置了可用的视觉模型时,ACP 现在会先用 bridge 把图片转换成文本,而不是让下游转换器把图片替换成占位文本。对于 @ 路径解析出来的不受主模型支持的图片文件,在 bridge 可处理时也会保留下来供转换使用。

为什么需要

交互式 CLI 已经会应用 vision bridge,但 daemon/ACP prompt 路径之前只解析图片 part 后直接发给主模型。像主模型是文本模型 qwen3.7-max,同 provider 下还有支持视觉的 qwen3.7-plus 这种配置,图片仍然会到达 qwen3.7-max,然后被降级成文本占位符。这个改动让 daemon 行为和 CLI 行为保持一致,也让现有 provider/model 自动发现能用于 ACP 图片 prompt。

Reviewer Test Plan

如何验证

配置一个文本主模型和可用的 vision bridge 模型,然后通过 qwen serve 发送 ACP 图片 prompt。预期 prompt 会先被 bridge 转换,再调用主模型;下游消息应包含图片转写文本,并且对于文本主模型不应再包含原始 inlineData 图片 part。新增回归测试通过 mock bridge 验证 ACP 会调用 bridge,并把转换后的文本传给主模型。

证据(Before & After)

Before:用户提供的 daemon 日志显示 Model 'qwen3.7-max' does not support image input. Replacing with text placeholder: image/png,且没有 vision bridge 转换日志。After:回归测试验证 ACP 图片 prompt 会调用 runVisionBridge,主 chat 收到转换后的文本且不包含 inlineData。本地 daemon 烟测也验证了 qwen serve 启动、/health、ACP initialize、以及带 mcpServers: [] 的 ACP session/new。 人工 Web UI 验证也已通过:

Manual Web UI verification screenshot

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

环境(可选)

本地 macOS worktree,Node.js 22。使用 dev 入口在 127.0.0.1:4171 启动 qwen serve --no-web 完成验证。

风险和范围

  • 主要风险或取舍:ACP 图片 prompt 在主模型不支持图片时会触发配置的 vision bridge 侧向调用,这与现有 CLI 行为和 bridge notice 语义一致。
  • 未验证 / 不在范围内:本地烟测没有发送真实付费/provider 图片 prompt;回归测试覆盖了 bridge 路由且不消耗模型额度。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

Resolves #6110
Related #6086

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Local daemon smoke report:

  • Built workspace packages first because qwen serve dev mode loads workspace package dist output.
  • Started qwen serve via npm run dev -- serve --port 4171 --hostname 127.0.0.1 --workspace /Users/jinjing/projects/projj/github.com/QwenLM/qwen-code/.worktrees/issue-6110-acp-vision-bridge --no-web with QWEN_SERVER_TOKEN=devtoken.
  • Verified GET /health returned 200 with {"status":"ok"}.
  • Verified ACP initialize returned 200 with an Acp-Connection-Id and advertised image prompt capability.
  • Verified ACP session/new succeeded with mcpServers: [] and returned a session id.

I did not send a live image prompt to the remote model during smoke testing to avoid consuming provider quota; the regression test covers the ACP vision bridge routing by mocking runVisionBridge and asserting the primary chat receives converted text without inlineData.

@yiliang114
yiliang114 marked this pull request as ready for review July 1, 2026 07:47
@yiliang114 yiliang114 changed the title fix(cli): Route ACP images through the vision bridge fix(daemon): Route ACP images through the vision bridge Jul 1, 2026
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓ — all required sections present, bilingual, with evidence.

On direction: this is a straightforward parity fix. The interactive CLI already runs the vision bridge for text-only models, but the daemon/ACP path skips it — images silently degrade to placeholders. Real user problem, clearly described in #6110. No direction concerns.

On approach: the scope is tight — 2 files, focused entirely on wiring the existing runVisionBridge into Session.#resolvePrompt. No new abstractions, no scope creep. The preserveUnsupportedImageForBridge flag correctly bypasses clampInlineMediaPart so oversized images reach the bridge. The #applyVisionBridgeIfNeeded private method mirrors the CLI's applyVisionBridgeIfNeeded pattern. The one known tradeoff — #formatVisionBridgeNotice duplicates the CLI formatter — is a reasonable follow-up, not a blocker for this PR.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓ — 所有必要部分齐全,中英双语,附带证据。

方向:这是一个明确的对等性修复。交互式 CLI 已经会对文本模型调用 vision bridge,但 daemon/ACP 路径跳过了这一步——图片被静默降级为占位符。问题真实存在,#6110 描述清晰,无方向问题。

方案:范围紧凑——2 个文件,专注于将已有的 runVisionBridge 接入 Session.#resolvePrompt。没有新抽象,没有范围蔓延。preserveUnsupportedImageForBridge 标志正确绕过了 clampInlineMediaPart,确保大尺寸图片能到达 bridge。#applyVisionBridgeIfNeeded 私有方法复刻了 CLI 的 applyVisionBridgeIfNeeded 模式。已知的取舍——#formatVisionBridgeNotice 与 CLI 的格式化函数重复——适合作为后续改进,不阻塞本 PR。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Code Review

The implementation closely mirrors the CLI path in useGeminiStream.ts:applyVisionBridgeIfNeeded, which is the right approach — reuse the proven pattern rather than invent a new one.

Correctness: All return paths in #resolvePrompt route through #applyVisionBridgeIfNeeded — the three exit points (no @ commands, no embedded context, and the full @ resolution path) all pass through the bridge gate. The preserveUnsupportedImageForBridge flag correctly prevents clampInlineMediaPart from stripping oversized images before the bridge can process them. The @-file path also passes the flag to readManyFiles, matching the CLI's atCommandProcessor.ts behavior.

Error handling: Independent try/catch around runVisionBridge (falls back to text-only parts) and emitAgentMessage (logs and continues). Post-bridge abortSignal.aborted check. Non-applied results strip images before forwarding to the text-only model — never leaks raw inlineData to a model that can't read it.

Prior review comments: All 5 critical issues from the initial review were addressed — error boundaries, abort handling, fallback paths, clampInlineMediaPart ordering, and test coverage. The author and a second reviewer confirmed each fix.

Minor note: #formatVisionBridgeNotice duplicates formatVisionBridgeNotice from useGeminiStream.ts with minor wording differences. Non-blocking — a shared core export is a reasonable follow-up.

Regression Tests (7/7 pass)

 ✓ routes ACP image prompts through the vision bridge for text-only primary models
 ✓ strips image parts when the vision bridge is cancelled before applying
 ✓ preserves oversized inline images for the vision bridge
 ✓ falls back to text-only parts when the vision bridge throws
 ✓ forwards failed bridge replacement parts to the primary model
 ✓ does not run the vision bridge when the primary model supports images
 ✓ preserves unsupported image @ files for the vision bridge

 Test Files  1 passed (1)
      Tests  7 passed | 213 skipped (220)
   Duration  8.69s

All branches covered: happy path, cancelled bridge, oversized images, bridge exception, bridge failure status, negative gate (model supports images), and @-file resolution.

Tmux Smoke Test (daemon startup)

$ qwen serve --no-web
qwen serve: daemon log → /home/github-runner/actions-runner-21/_work/_temp/qwen-home/debug/daemon/serve-1174914-317c2b2f.log
qwen serve listening on http://127.0.0.1:4170 (mode=http-bridge, workspace=/home/github-runner/actions-runner-21/_work/qwen-code/qwen-code)
qwen serve: bound to workspace "/home/github-runner/actions-runner-21/_work/qwen-code/qwen-code"
qwen serve: startup timing: processToListenMs=438 runQwenServeToListenMs=20
qwen serve: bearer auth disabled (loopback default). Set QWEN_SERVER_TOKEN to enable.
qwen serve: session reaper started (interval 60000ms, idle threshold 1800000ms)
qwen serve: /acp WebSocket transport enabled on /acp

$ curl -s http://127.0.0.1:4170/health
{"status":"ok"}

Daemon starts and responds. Full ACP image-prompt routing through the vision bridge requires a configured text-only primary model with an available vision bridge model — not available in this CI environment. The unit tests cover the code path comprehensively with mocked bridge calls.

中文说明

代码审查

实现紧贴 useGeminiStream.ts:applyVisionBridgeIfNeeded 的 CLI 路径,这是正确的做法——复用已验证的模式而非另起炉灶。

正确性: #resolvePrompt 的所有返回路径都经过 #applyVisionBridgeIfNeeded——三个出口(无 @ 命令、无嵌入上下文、完整 @ 解析路径)都通过了 bridge 门控。preserveUnsupportedImageForBridge 标志正确阻止了 clampInlineMediaPart 在 bridge 处理前裁剪大尺寸图片。@-文件路径也将该标志传递给 readManyFiles,与 CLI 的 atCommandProcessor.ts 行为一致。

错误处理: runVisionBridgeemitAgentMessage 各自有独立的 try/catch。bridge 返回后检查 abortSignal.aborted。未应用的结果会在转发给文本模型前剥离图片——不会将原始 inlineData 泄露给无法解析的模型。

先前审查意见: 初始审查的 5 个关键问题已全部解决——错误边界、中断处理、回退路径、clampInlineMediaPart 排序、测试覆盖。作者和另一位审查者逐一确认。

小建议: #formatVisionBridgeNoticeuseGeminiStream.ts 中的 formatVisionBridgeNotice 重复,措辞略有差异。不阻塞——提取为 core 共享导出可作为后续改进。

回归测试(7/7 通过)

所有分支均已覆盖:正常路径、bridge 取消、大尺寸图片、bridge 异常、bridge 失败状态、负向门控(模型支持图片)、@-文件解析。

Tmux 烟测(daemon 启动)

Daemon 正常启动并响应。完整的 ACP 图片 prompt 经 vision bridge 路由需要配置文本主模型和可用的 vision bridge 模型——当前 CI 环境不具备此条件。单元测试通过 mock bridge 调用全面覆盖了代码路径。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

This PR is a clean, focused parity fix. The daemon/ACP path now matches the interactive CLI's vision bridge behavior — text-only primary models get image-to-text conversion instead of silent placeholder degradation.

The implementation mirrors the proven CLI pattern, handles errors gracefully (independent try/catch for bridge and notice, abort checks, text-only fallback), and passes all 7 regression tests covering every meaningful branch. The prior review's 5 critical issues were all addressed before this re-run. The one duplication — #formatVisionBridgeNotice vs the CLI's formatter — is a known follow-up, not a reason to hold this up.

Two files changed, no scope creep, no new abstractions. Ships the fix, moves on.

Approving. ✅

中文说明

这个 PR 是一个干净、专注的对等性修复。daemon/ACP 路径现在与交互式 CLI 的 vision bridge 行为一致——文本主模型会获得图片转文本,而非静默降级为占位符。

实现复刻了已验证的 CLI 模式,错误处理稳健(bridge 和通知各自有独立的 try/catch、中断检查、纯文本回退),7 个回归测试覆盖了所有有意义的分支。先前审查的 5 个关键问题在本次重审前已全部解决。唯一的重复——#formatVisionBridgeNotice 与 CLI 格式化函数——是已知的后续改进,不构成阻塞理由。

2 个文件改动,无范围蔓延,无新抽象。完成修复,继续前进。

批准 ✅

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot 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.

LGTM, looks ready to ship. ✅

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

Review summary (automated): Reviewed the full diff (2 files, +117/-3) covering correctness, security, and code quality.

Correctness: No issues found. All three return paths in #resolvePrompt correctly route through #applyVisionBridgeIfNeeded. The gating (hasImageParts + shouldRunVisionBridge) matches the CLI path in useGeminiStream.ts. The failure helper in runVisionBridge sets applied: true with image-free replacement parts, so the bridge failure case is handled correctly without needing the explicit image-stripping fallback the CLI uses. The preserveUnsupportedImageForBridge flag is correctly passed to readManyFiles only when the bridge is active.

Security: No issues. Egress disclosure is properly surfaced in all relevant cases (success, failure with egress, skipped with egress). The system instruction remains injection-aware.

Code quality (minor, non-blocking): #formatVisionBridgeNotice in Session.ts duplicates formatVisionBridgeNotice from useGeminiStream.ts with minor wording differences (e.g., CLI says "cancelled" for skipped status, ACP says "skipped"; success-case egress note wording differs). Consider extracting a shared formatter to core to prevent future drift.

Test: The regression test properly mocks the bridge and verifies routing + inlineData removal. LGTM.

Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
Comment thread packages/cli/src/acp-integration/session/Session.test.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

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

All review comments have been addressed in subsequent commits (harden fallback + test coverage). The final implementation:

  • Wraps runVisionBridge and emitAgentMessage in independent try/catch blocks (best-effort enhancement)
  • Checks abortSignal.aborted after bridge returns, falls back to text-only parts
  • Returns splitImageParts(parts).nonImageParts in all non-applied/failed/cancelled paths (never leaks raw inlineData to text-only models)
  • formatVisionBridgeNotice wording aligned with CLI behavior
  • preserveUnsupportedImageForBridge correctly bypasses clampInlineMediaPart when bridge is active
  • 7 test cases cover all critical branches including error, cancel, gate-negative, oversized, and @file paths

LGTM. One follow-up suggestion: extract formatVisionBridgeNotice into packages/core to eliminate the remaining duplication between Session.ts and useGeminiStream.ts.

@yiliang114
yiliang114 enabled auto-merge July 1, 2026 09:31

@qwen-code-ci-bot qwen-code-ci-bot 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.

LGTM, looks ready to ship. ✅

@yiliang114
yiliang114 added this pull request to the merge queue Jul 1, 2026
Merged via the queue into QwenLM:main with commit b9b3bce Jul 1, 2026
97 checks passed

@qwen-code-ci-bot qwen-code-ci-bot 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.

No issues found. LGTM! ✅

— qwen3.7-max via Qwen Code /review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ACP image prompts bypass the vision bridge for text-only models

4 participants