Skip to content

feat: VLM vision analysis - intercept images for text-only models#1405

Open
kanchengw wants to merge 6 commits into
BigPizzaV3:mainfrom
kanchengw:feat/vlm-vision-analysis
Open

feat: VLM vision analysis - intercept images for text-only models#1405
kanchengw wants to merge 6 commits into
BigPizzaV3:mainfrom
kanchengw:feat/vlm-vision-analysis

Conversation

@kanchengw

@kanchengw kanchengw commented Jul 10, 2026

Copy link
Copy Markdown

Summary

Add VLM (Vision Language Model) analysis to the protocol proxy. When a text-only model (e.g. DeepSeek V4) receives an image, the proxy strips the image blocks, calls a VLM API (e.g. qwen-vl-plus) to describe the images in the latest user message, and injects the descriptions as text -- enabling text-only models to see images without upstream errors. Furthermore, with VLM analysis enabled for a text-only model in the manager, previously corrupt sessions are recovered automatically since the proxy strips images from all messages (including history) on every request, and only calls VLM when latest user message includes images.

Closes #574 - DeepSeek session permanently broken by images
Closes #1191 - Feature request: auto-intercept images for non-vision models
Closes #631 #790 #1158 #1221 #1365 #839 #703 #1194 - unknown variant image_url deserialization errors

How it works

User sends image --> Codex Desktop --> Codex++ Protocol Proxy (:57321) --> Upstream API (e.g. DeepSeek)
                                         |
                                         |-- 1. Check VLM toggle for this model
                                         |-- 2. Guard: api_key & model & base_url non-empty?
                                         |-- 3. Collect image_url from latest user message
                                         |-- 4. Strip all image blocks from all messages
                                         |-- 5. Call VLM API (parallel batches of 5)
                                         |-- 6. Inject analysis text into last user message
                                         |-- 7. Forward cleaned request upstream

Vision analysis covers:

  • OCR / text extraction -- reads all visible text in the image (code screenshots, error messages, logs, documents, UI labels)
  • Visual description -- describes layout, objects, charts, UI components, color, spatial relationships

Together these cover the user's primary intents when sending images in a chat:

  • Asking "what does this error mean?" -- text extraction captures the error
  • Asking "what's in this screenshot?" -- description captures the context
  • Asking "read this document / whiteboard / diagram" -- OCR reads the content
  • Asking "compare these two charts" -- description captures the data patterns
  • Any combination of the above with a follow-up text instruction (e.g. "fix this bug" + screenshot) -- analysis merges with the user's original instruction

The original image_url block is removed entirely. Only the text description remains, ensuring the text-only model can process the message without issues.

Why not route the entire conversation to a vision model?

The proxy applies VLM at the network layer without changing the user's model, session, or tool configuration. This preserves full conversation context, avoids dual-model latency/cost, and requires zero changes to Codex Desktop or upstream APIs. Per-model VLM toggles also allow fine-grained control (e.g. flash with VLM, pro without) within the same supplier.
In contrast, routing the entire conversation to a vision model introduces a context dilemma: sending the full history burns excessive tokens on the vision model, while sending only the latest message strips the vision model of the context needed to understand the user's intent.

Files Changed

12 files, +587 / -71。

vision.rs (new, ~304 lines)

Core VLM pipeline with 13 unit tests:

  • should_process() - Check VLM toggle in per-model JSON config
  • strip_image_blocks() - Main entry: collect, strip, analyze, inject
  • collect_urls() - Read-only: collect image URLs from latest user message
  • strip_all_images() - Remove image blocks from all messages
  • analyze_all() - Parallel VLM calls in batches of 5
  • inject_analysis() - Append VLM text to last user message

Key design: collect BEFORE strip. Only the latest user message images are VLM-analyzed.

protocol_proxy.rs - Two interception points (+59/-15)

5-condition guard: model non-empty, should_process true, api_key/model/base_url non-empty.

  • Responses API path: processes messages and input arrays
  • Chat Completions path: processes messages array

Config and UI

  • settings.rs (+20): 4 new RelayProfile fields (model_vlm, vlm_api_key, vlm_model, vlm_base_url)
  • App.tsx (+89/-29): per-model VLM checkbox, conditional VLM provider section
  • model-windows.ts (+18/-6): vlm field on row type + serialization
  • model-windows.test.ts (+31/-16): updated tests
  • styles.css (+22): VLM section styles
ScreenShot_2026-07-10_130556_834

Testing

cargo test -p codex-plus-core vision:: --lib -> 13 passed
node --test src/model-windows.test.ts -> 10 passed
cargo clippy -p codex-plus-core -> clean

@kanchengw kanchengw force-pushed the feat/vlm-vision-analysis branch 6 times, most recently from 72d4264 to 684e24f Compare July 10, 2026 06:34
@kanchengw kanchengw force-pushed the feat/vlm-vision-analysis branch from 684e24f to e5f529f Compare July 10, 2026 07:06
@BigPizzaV3

Copy link
Copy Markdown
Owner

感谢提交。静态审查和本地测试后,目前有几项阻塞合并的问题需要先处理:

  1. 普通 Responses 供应商不会经过本地 57321 代理,因此 UI 虽然允许开启 VLM,但 VLM 逻辑实际不会执行。当前代理只在 Chat Completions 或聚合模式启动。请明确支持范围:要么让启用 VLM 的 Responses 配置也经过代理,要么限制 UI 并明确说明仅支持 Chat/聚合模式。

  2. VLM HTTP 请求没有显式连接、响应头和总超时。VLM 服务不返回时,整个 Codex 请求以及聚合故障切换都会一直等待。请增加有界超时和可诊断的错误处理。

  3. 当前只分析最新用户消息,却会删除所有历史图片。VLM 描述只存在于本次转发请求,没有写回 Codex 会话;下一轮追问历史图片时,原图和描述都会丢失。需要缓存/重放历史图片描述,或者重新分析历史图片,不能直接静默删除。

  4. analyze_all 的“全部失败”判断目前不可达:每个失败批次也会向 results 写入错误字符串,因此 results.is_empty() 不会成立。结果是原图被删除,原始 VLM 错误正文被注入提示词,然后请求仍继续发送。请明确采用 fail-closed、保留原图或结构化占位策略,并避免把完整上游错误正文注入模型上下文。

  5. 开启 VLM 后,Key、模型或 Base URL 为空时后端会静默跳过处理,用户仍会看到开关已开启。请在保存和运行时增加校验及明确错误提示。VLM API Key 输入框也应使用 password 类型。

  6. join_all 会同时启动所有图片批次,没有图片数量、并发数和错误响应体大小限制,可能造成突发费用和内存/上下文膨胀。请增加有界并发、图片上限和错误正文截断。

本地验证结果:

  • cargo test -p codex-plus-core vision:: --lib:13 项通过
  • model-windows.test.ts:10 项通过
  • 新增测试均为 JSON helper 测试,尚未覆盖真实 HTTP、超时、全部失败、普通 Responses 模式、聚合重试和历史图片后续轮次
  • cargo fmt --all -- --check 在 PR 分支失败,而当前主分支通过,请先执行格式化

以上问题修复并补充回归测试后再进行下一轮审查。

Strip image_url/input_image blocks, call VLM to describe them, inject descriptions as text. Per-round analysis with LRU cache (500 entries, 24h TTL), depth limit (20 rounds), per-request image cap (10 historical), bounded concurrency (3 per-request, 5 global), HTTP timeouts (connect 5s, request 30s, total 120s). Fail-closed: preserve images when VLM unreachable. 42 tests including 8 wiremock integration tests.
@kanchengw

kanchengw commented Jul 11, 2026

Copy link
Copy Markdown
Author

感谢审查。VLM 管线采用两阶段分析,逐点回复如下。


1. Responses 模式不经过代理

UI 侧已处理:RelayProfileEditorprotocol === "responses" && !isAggregateRelayProfile 的配置禁用 VLM checkbox,tooltip 提示"VLM 仅支持 Chat Completions 协议和聚合模式"。

运行时确认 Codex 请求统一走 /v1/responsesopen_responses_proxy_requestupstream_request_parts,VLM guard 在协议转换之前执行,因此 Chat/聚合模式的代理请求均可正常拦截。


2. HTTP 超时

连接超时      5s(reqwest connect timeout)
单请求超时    30s(prod)/ 2s(test,cfg(test) 切换)
总超时        120s(ANALYZE_ALL_TIMEOUT,tokio::select!)
错误正文截断  256 chars(ERROR_BODY_TRUNCATE)

超时后未完成的 batch task 被 JoinSet drop 取消,已完成的结果保留。全部失败则返回 Err 走 fail-closed。


3. 历史图片处理(两阶段分析)

strip_image_blocks 引入 X = available / 100 作为注入预算(每条 VLM 描述 ≈100 tokens),按 X vs 10(黄金窗口深度)做决策分叉:

Phase 1(同步):

位置 策略
当前轮(latest user msg) 当轮全量图片进行 VLM 分析,剥离图片注入描述(仍可被上下文保护截断)
黄金窗口(最近 10 轮 user 消息中的图片) VLM 分析或缓存命中,注入量受 X 约束:X ≤ 10 时 cap = min(N, X),X > 10 时 cap = min(N, 10)
深层(10~50 轮范围) LRU 缓存命中在 X 预算余量内从近到远填充,不额外调 VLM

决策矩阵:

X vs 10 黄金窗口注入 后台 Phase 2
X ≤ 10 最多 X 张 不启动
X > 10 最多 10 张 补到 X 张

Phase 2(后台,仅 X > 10 时启动):

  • tokio::spawn 异步分析深层未缓存图片,先补完黄金窗口未被 Phase 1 覆盖的图片,再向深层推进
  • 只写缓存不注入消息,用户本轮不感知延迟
  • 目标量 = X − Phase 1 已注入数;超时 120s,全局并发 5

上下文溢出保护:

safe_window = 模型上下文窗口 × 0.9(CONTEXT_SAFETY_MARGIN)
available = safe_window − 纯文本估算 token 数
available ≤ 1 → strip 图片 + 注入占位符告知模型,不调 VLM,不启动后台

截断兜底: 所有注入合并 ≤ available × 2 字符上限,超出从远端(最旧轮)砍。

概括: 同步路径覆盖当前轮+黄金窗口(保障核心相关性),后台静默推进深层(零延迟),X 统管所有注入总量,上下文消耗完全可控。


4. fail-closed 策略

analyze_all 全部 batch 失败时返回 Errstrip_image_blocks 检测到后 early return → 全部图片保留,零文本注入

各场景处理:

  • VLM 完全不可达 → 图片全部保留
  • 当前轮 VLM 失败 → 当前轮图片保留(已有视觉信息,不注入占位符)
  • 历史轮 VLM 失败 → 静默跳过该轮,不影响其他轮次

不再可能把上游错误正文注入模型上下文。


5. UI 校验

  • VLM API Key 输入框已使用 type="password"
  • 开关开启但 key / model / base_url 任一为空时,显示警告:"VLM 配置不完整:API Key、Model 和 Base URL 为必填项,否则 VLM 不会生效。"
  • 纯 Responses 模式 checkbox 已置灰并显示 tooltip

如需保存时强制阻断(当前为被动警告),请确认。


6. 并发 / 图片上限 / 错误截断

限制项 说明
单请求并发 3 PER_REQUEST_CONCURRENCY,tokio::Semaphore
全局并发 5 MAX_GLOBAL_VLM_CONCURRENCY,跨请求 Semaphore
每 batch 图片数 5 BATCH_SIZE
黄金窗口深度 10 轮 user 消息 GOLDEN_WINDOW_DEPTH,Phase 1 同步分析范围
历史深度限制 50 轮 user 消息 ANALYZE_DEPTH_LIMIT,超出部分仅剥离不分析
注入总量上限 X = available / 100 X 统管 VLM 新分析 + 缓存命中,截断仅兜底
后台分析 X > 10 时 tokio::spawn 异步补深层未缓存图片,只写缓存不注入消息
重试 最多 2 次 MAX_RETRIES,指数退避 500ms / 1000ms
错误正文截断 256 chars ERROR_BODY_TRUNCATE
总超时 120s ANALYZE_ALL_TIMEOUT,超时后保留已完成 batch,未完成的丢弃

测试

cargo test -p codex-plus-core vision:: --lib
→ 43 passed; 0 failed; 0 ignored

cargo fmt --all -- --check
→ 通过

cargo clippy -p codex-plus-core
→ clean

测试覆盖类别:

类别 数量 说明
wiremock 真实 HTTP 5 成功 / 全失败 / 部分失败 / E2E 描述注入 / 超时+重试
聚合重试 2 重试成功(503→200)/ 重试耗尽 fail-closed
Plain Responses 模式 1 input_image 类型 + 直接字符串 image_url
历史图片多轮 3 双轮 VLM 分析 / 25 轮深度+per-round 限额+溢出截断 / X ≤ 10 黄金窗口 cap
缓存 + VLM 混合 2 全缓存命中 / 混合缓存+VLM 不可达 fail-closed
上下文溢出 / 无图 / VLM 不可达 3 溢出占位符注入 / 空消息不变 / 不可达保留图片
retryable 判断 4 超时 / 5xx+429 / 4xx 不重试 / 其他错误
context window 解析 4 model_windows 优先 / 全局 fallback / 硬兜底 / provider 前缀剥离
collect / inject / 缓存基础 6 空数组 / input_image / depth limit / 占位符 / 字符串 content 转换 / 缓存驱逐

jarvislee90s-dot added a commit to jarvislee90s-dot/CodexPlusPlus-VsionForTextmodel that referenced this pull request Jul 13, 2026
- VL 单次调用加 .timeout(30s),10 张图最多 300s
- VL_IMAGE_LIMIT=10,超出部分直接 strip 不调 VL API
- PR 文档:已知局限更新(去掉已 fix 的超时),新增第十章与 BigPizzaV3#1405 完整对比
- 删除废弃的未命名.txt handoff 文件

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants