ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

opencodex Sidecar 原生化研究:让 Web Search 与 Vision 代理在 Codex UI 中呈现原生体验

2026/9/23 2:52:27 拓冰建站 浏览量
opencodex Sidecar 原生化研究:让 Web Search 与 Vision 代理在 Codex UI 中呈现原生体验 opencodex Sidecar 原生化研究让 Web Search 与 Vision 代理在 Codex UI 中呈现原生体验【免费下载链接】opencodexUniversal provider proxy for OpenAI Codex Claude Code — use any LLM (Claude, Gemini, Grok, DeepSeek, Ollama…) with Codex CLI, App, SDK, and Claude Code项目地址: https://gitcode.com/gh_mirrors/ope/opencodex导读opencodex 作为通用 Provider 代理通过 sidecar旁路代理机制让路由到非 OpenAI 模型Claude、Gemini、Grok、DeepSeek、Ollama 等的请求也能获得 ChatGPT 前向后端才能提供的 Web 搜索与图像理解能力。本篇文章以仓库内devlog/_fin/260630_native-sidecar-parity/研究文档为核心深入剖析原生 sidecar 对齐native sidecar parity这一技术命题当 sidecar 替路由模型执行了真实的 Web 搜索、或替纯文本模型描述了输入图像时如何让 Codex 的 UI 呈现与原生模型一致的活动轨迹而不是只把 sidecar 结果当作一段普通文本塞回路由模型。读完本文你将掌握web_search_call输出项的完整线协议、opencodex 侧 sidecar 拦截/执行/回注的三段式链路、以及为何 Web 搜索能做原生对齐而 Vision 不能的底层协议原因。说明本文核心内容继承自 00_research.md并引用同一目录下的 10_phase1_websearch-native-ui.md、20_phase1_verification.md、30_phase2_websearch-fidelity.md、40_phase3_websearch-sources.md 以及仓库源码作为佐证。一、研究起点sidecar 输出为什么在 Codex UI 里不原生1.1 核心问题研究文档提出的原始问题是Can opencodex sidecars make routed providers look native in Codex UI, instead of only feeding sidecar output back to the routed model as plain text?即opencodex 的 sidecar 能否让被路由的 Provider 在 Codex UI 中看起来像原生的一样而不是仅仅把 sidecar 输出以纯文本形式回注给路由模型要理解这个问题需要先厘清两条路径的差异原生路径Codexcodex-rs 客户端直连 OpenAI/ChatGPT 后端模型输出中包含结构化事件如web_search_callTUI/App 据此渲染出Searched the web活动单元格、Sources 徽标等原生 UI。代理路径opencodex 拦截合成web_search工具调用 → 通过 ChatGPT forward/responses后端真正执行搜索 → 把结果作为tool_result文本回注给路由模型 → 最终以普通 assistant 文本输出给 Codex。在代理路径下Codex 的 UI 只看到一段包含搜索结果的回答文本完全没有原生活动轨迹没有搜索单元格、没有 Sources 徽标、没有进行中的搜索状态。这正是 00_research.md 想解决的原生对齐parity问题。1.2 研究结论概览研究结论可以浓缩为两句话Web 搜索的 parity 是可行的、直接的因为 codex-rs 原生定义并渲染ResponseItem::WebSearchCall线协议web_search_callopencodex 只需保留 sidecar 的真实搜索执行并在最终 assistant 文本之前发射对应的web_search_call输出项即可无需修改 Codex 客户端。Vision 的 parity 与 Web 搜索性质不同Codex 原生并不存在模型读取了图片这样的输出项。用户附加的图片会作为用户消息的一部分渲染本地view_image工具执行时 codex-rs 自己就会发出Viewed Image活动项。若伪造ImageView项在语义上是错误的它要求真实的本地路径、代表一次工具执行因此研究建议视觉 sidecar 的 UI 保持现状。二、Web 搜索 sidecar 的现状三段式链路在深入协议细节前先看 opencodex 目前如何实现 Web 搜索 sidecar对应 00_research.md 的 Current opencodex behavior。其链路横跨 src/web-search/ 目录下多个文件环节文件职责拦截src/web-search/loop.ts拦截路由模型发出的合成web_search工具调用执行src/web-search/executor.ts通过 ChatGPT forward/responses后端运行真实的托管web_search解析src/web-search/parse.ts从 sidecar 的 SSE 流中提取最终文本与 URL 引用sources回注src/web-search/loop.ts将结果注入回路由模型作为tool_result合成工具src/web-search/synthetic-tool.ts定义合成web_search工具的声明WEB_SEARCH_TOOL_NAME2.1 拦截scanEventsForWebSearchloop.ts中的runWithWebSearch是主循环入口它把路由模型置于一个小型 agentic 循环中每次上游迭代被流式读取并整体缓冲若模型调用了web_search则由托管 sidecar 执行把答案作为tool_result注入然后继续循环受maxSearches限制。关键的拦截函数是scanEventsForWebSearch(events)src/web-search/loop.ts。它把一次非流式回合的 adapter 事件拆成两部分calls需要被拦截的web_search工具调用含id与归一化后的queries[]passthrough其余所有需要透传给 Codex 的事件文本、thinking、真实工具调用、done。代码中有一个值得注意的细节parseQueries(argsBuf)会把模型传来的参数归一化为规范的queries[]同时接受原生复数queries: string[]与单数query: string两种形态非字符串/空条目被丢弃畸形 JSON 得到[]下游按空查询调用处理。这一点在后续 phase 2/3 中会成为原生复数语义的基础。2.2 执行runWebSearch 与多后端executor.ts中的runWebSearch是 OpenAIChatGPT forward路径的执行器其要点包括复用前向 OAuth 头前向 adapter 自身没有密钥因此复用已选中的前向请求头FORWARD_HEADERS集合见selectedForwardHeaders的拷贝逻辑保持托管工具配置原样重放把hostedTool来自 ChatGPT 后端的web_search工具定义原样放进请求体tools并设置tool_choice: auto最小推理开销以reasoning: { effort: settings.reasoning }运行迷你模型model与reasoning均来自SidecarSettings两个硬性约束ChatGPTcodex后端拒绝max_output_tokensUnsupported parameter且要求store: false——请求体必须保持最小化永不抛错never throws契约任何失败都返回{ text, sources, error }形态的SidecarOutcome由调用方降级为工具结果而不是中断整个回合。值得一提的还有请求体中的instructions动态拼接BASE_INSTRUCTION指示搜索助手使用 web_search 工具查询、以Sources:段落逐行列出来源当路由模型为纯文本模型settings.describeImages时追加IMAGE_INSTRUCTION要求搜索助手把图片结果用文字描述并附上 URL——这正是研究文档中image-web-search gap的解决方案src/web-search/executor.ts。此外从WebSearchLoopDeps可以看到搜索后端已从最初的 OpenAI 单一后端扩展为多后端架构backend: openai默认ChatGPT forwardbackend: anthropic使用存储的 OAuth Provider 运行web_search_20250305backend: xaiGrok 的x_search经xaiSearchOptions配置backend: geminiAntigravity CCA groundingbackend: exa非 LLM 通道读取exaApiKey。每个非 OpenAI 后端都遵循 fail-closed 原则若对应 sidecar 依赖如xaiSidecar、anthropicSidecar未解析则直接返回错误结果而不是意外回落到使用前向头部的 OpenAI 执行器源码注释明确标注该回退是credential-sensitive必须避免。2.3 解析parseSidecarSSEparse.ts的parseSidecarSSE从 sidecar 的流式 Responses SSE 中解析答案与引用其健壮性体现在多处事件形态容错优先采用权威的response.completed中的output[]其次用response.output_text.done的完整文本再退而累积response.output_text.delta。引用多渠道收集从response.output_text.annotation.added流式事件、done 块的annotations[]、最终output[]三个渠道收集url_citation并去重。Sources:尾部段落提取托管web_search通常不输出结构化url_citation注解而是在回答末尾以 MarkdownSources:段落列出来源。extractTrailingSources只提取尾部的 Sources 段落避免把正文中偶然出现的 URL 误判为引用支持- title: url、- title (url)、- title、- url、编号列表、Markdown 前缀标题、URL 在下一行的多行条目等形态并在提取后把该段落从答案文本中剥离避免 tool_result 渲染时重复打印来源。安全边界响应体字节上限MAX_SIDECAR_RESPONSE_BYTES 64KiB、流式线字节上限MAX_SIDECAR_STREAM_BYTES 64KiB × 16、解码文本字符上限MAX_SIDECAR_DECODED_CHARS 64KiB任一超限即截断并标记错误防止多图/超长回合撑爆主模型上下文。三、codex-rs 消费端的原生 Web 搜索轨迹研究文档明确指出codex-rs已经原生理解Web 搜索活动这是整个 parity 方案可行性的基石。相关消费链路在 codex-rs 仓库非本仓库源码仅作协议依据codex-rs 位置内容protocol/src/models.rsResponseItem::WebSearchCall线类型web_search_call字段id、status、actionprotocol/src/models.rsWebSearchAction::{Search, OpenPage, FindInPage, Other}core/src/event_mapping.rs把ResponseItem::WebSearchCall映射为TurnItem::WebSearchcore/src/session/turn.rsresponse.output_item.added→ item startedresponse.output_item.done→ item completedtui/src/history_cell/search.rs渲染 Web 搜索历史单元格因为 opencodex 已经在讲 codex-rs 消费的 Responses SSE 语言见 src/bridge.ts所以发射web_search_call输出项是一条 opencodex 侧的纯 bridge 改动保留 sidecar 搜索执行在最终 assistant 文本之前发射web_search_call输出项codex-rs 无需客户端改动即可解析并渲染。3.1 最小事件形态研究文档给出了两种极简事件形态。开始Start{ type: response.output_item.added, output_index: 0, item: { type: web_search_call, id: ws_sidecar_..., status: in_progress } }完成Done{ type: response.output_item.done, output_index: 0, item: { type: web_search_call, id: ws_sidecar_..., status: completed, action: { type: search, query: ... } } }两个事件的关键点addedin_progress与donecompleted必须共用同一个idcodex-rs 才能把一次搜索的生命周期拼合为同一个单元格done事件必须携带action: { type: search, query }这是 codex-rsWebSearchAction::Search的合法形态事件通过 SSE 线协议传输event.item会被直接反序列化为ResponseItem::WebSearchCall。3.2 可行性风险评估研究文档列出了三个主要风险全部聚焦于时序与真实性避免把 sidecar 搜索与路由模型的原生能力混为一谈——UI 上呈现的搜索必须确实由 sidecar 执行过保持output_index单调递增与终端response.completed的排序合法——新增的输出项会占用一个索引后续消息项索引不能错乱不得泄漏隐藏的 sidecar 提示文本——例如 2.2 节中注入的instructions、forcedAnswerNudge等开发者角色提示绝不能出现在最终输出里。四、Web 搜索原生化的落地三个 Phase 的演进研究文档之后同一目录下的三个 Phase 文档完整记录了从研究结论到实现落地的演进。这正好可以作为源码级纵深的实证链条也印证了研究文档中可行、风险主要在时序与真实性的判断。4.1 Phase 1原生web_search_callUIPABCD 计划Phase 110_phase1_websearch-native-ui.md的改动范围被严格限定IN 范围src/types.ts— 在AdapterEvent联合类型中新增web_search_call变体src/bridge.ts— 在流式bridgeToResponsesSSE与非流式buildResponseJSON中把新事件发射为自包含的web_search_call输出项src/web-search/loop.ts— 记录已执行的搜索并预置新事件tests/bridge.test.tstests/web-search.test.ts— 覆盖测试。OUT 范围不改 adapter、不改 sidecar 执行器、不改parse.ts不改 Vision sidecar此阶段不把sources作为引用/注解转发原生单元格只需要action.query注解留待后续阶段。Phase 1 还定下了一个关键排序决策先发射所有搜索活动项再发射最终答案项——这与原生流程一致先搜索、后作答且因为循环在执行finalEvents之前已经完成所有搜索实现上也最简单。验证记录20_phase1_verification.md确认bun x tsc --noEmit通过bun test tests/bridge.test.ts tests/web-search.test.ts tests/sidecar-abort.test.ts→ 29 通过、0 失败含 4 个新测试独立审查者对照 codex-rs 确认SSE 将event.item直接反序列化为ResponseItem::WebSearchCallid字段名是id而非call_id尽管skip_serializing反序列化仍读取它action: { type: search, query }是合法形态同时发出 added 与 done 匹配原生生命周期。细节修正使用event.id而非新生成的 uuid只在实际runWebSearch分支记录增加循环级测试。4.2 Phase 2原生保真度3 个 PABCD 循环Phase 230_phase2_websearch-fidelity.md针对 5 个代理侦察发现的三个原生对齐缺口做了三轮回合循环 1 — 强制应答反映搜索结果当forceAnswer为真且至少执行过一次真实搜索时向强制应答轮次的请求注入一条瞬态 developer 角色提示nudge指示模型基于已收集的 Web 结果作答并引用来源。该提示只加到iterParsed迭代局部绝不污染持久化的messages。这是对研究文档避免泄漏隐藏提示风险的一个正面处理提示是给路由模型看的不进入任何输出线。循环 2 — 实时 in_progress → completed 生命周期原实现把所有迭代缓冲后统一重放[...searchEvents, ...finalEvents]导致added(in_progress)与done(completed)背靠背发射Searching the web 状态在真实的数秒 sidecar 调用期间从未显示。改造方案把单一web_search_call事件拆为两个生命周期事件——web_search_call_begin { id }在runWebSearch之前发射bridge 输出in_progress与web_search_call_end { id, query, status }在返回后发射bridge 输出completed/failed与action.search重构runWithWebSearch使 SSE body 由 async generator 驱动搜索单元格的开始/结束与真实 sidecar 时序交错地实时发射。这里还保留了一个硬契约来自既有测试只有第一次模型调用被急切执行其 fetch/parse 错误仍返回jsonError如 502/499第 2 次迭代的失败已处于 200 流内以流内error事件呈现。Sidecar 搜索失败不致命通过recordSidecarOutcome记录后循环仍返回 200 SSE 且模型作答。循环 3 — 原生复数查询语义原生action.search.queries复数此前不受支持。改造后synthetic-tool.ts的合成web_search工具同时接受querystring或queriesstring[]scanEventsForWebSearch把模型参数解析为规范queries[]runSearchCall对一次批量调用逐条执行 sidecar 搜索各自计入maxSearches/failedQueries预算但注入一个assistant toolCall参数{ queries }与一个聚合 toolResult保证函数调用配对合法只发射一个开始与结束单元格结束单元格携带全部queriesCodex 原生显示Searched first ...format-result.ts新增聚合器把多条 (query, outcome) 渲染为一个 tool_result 字符串散文路径为带标签块结构化路径为单个{ results: [...] }JSON单查询路径保持向后兼容。4.3 Phase 3sources/citations 到达 GUIPhase 340_phase3_websearch-sources.md解决了Codex 桌面 App 在搜索结果后渲染 Sources 徽标与行内引用的问题。背景代理已经从 sidecar 解析出url_citation为outcome.sourcesurltitle但只通过formatWebSearchResults喂给了 toolResult 文本最终 assistant 消息始终发射output_text.annotations: []GUI 永远收不到引用。决策归一化为 App 线协议形态——output_text.annotations[]携带url_citation条目。codex-rsTUI目前忽略 annotations因此这是纯增量改动TUI 不受影响桌面 App 读取 annotations 绘制 Sources 徽标。标准线形态{ type: output_text, text: ...answer..., annotations: [ { type: url_citation, url: https://..., title: Node.js Releases, start_index: 0, end_index: 0 } ] }改动映射src/types.ts新增OcxUrlCitation { url; title? }并让搜索结束事件携带sources?: OcxUrlCitation[]src/web-search/loop.tsrunSearchCall跨批量查询去重outcome.sources并挂到web_search_call_end事件src/bridge.ts流式累积来自web_search_call_end的pendingWebSources在下一条 assistant 消息关闭时以其output_text.annotations发射url_citation[]随后清空——保证引用精确绑定到搜索后的第一条消息src/bridge.tsbuildResponseJSON非流式同样的累积在flushText()中挂接。OUT 范围行内字符范围引用start/end 索引指向文本不做统一发射 0/0App 只从 url/title 绘制徽标不改 toolResult 文本格式模型仍然在文本中收到来源。4.4 当前源码形态印证研究文档所描述的当前行为在仓库源码中得到了完整印证。以 src/web-search/loop.ts 为例可以观察到runSearchCall中真实搜索分支在runWebSearch前发射{ type: web_search_call_begin, id: call.id }在聚合结果后发射{ type: web_search_call_end, id, queries, status: anySuccess ? completed : failed, ...(sources.length 0 ? { sources } : {}) }——正是 Phase 2 拆分后的实时生命周期形态status只在真实命中 sidecar的搜索上发射空查询/超限/重复占位不发射单元格与 Phase 1 的记录只针对真实runWebSearch分支决策一致搜索结果会去重后携带sources集合供 bridge 在后续 assistant 消息上挂接url_citation注解——Phase 3 的落点。相关测试可进一步查看 tests/web-search/web-search.test.ts、tests/web-search/web-search-progress-stream.test.ts 与 tests/adapters/bridge.test.ts。五、Vision / image-read sidecar为什么不能做同样的对齐5.1 当前行为请求预处理而非输出项桥接研究文档明确指出Vision sidecar不是Responses 输出项桥接而是请求预处理src/server.ts在主 Provider 请求之前规划planVision sidecarsrc/vision/index.ts 仅在路由模型被分类为纯文本provider.noVisionModels且请求携带图片部分时激活src/vision/describe.ts 把每张图片发送给 ChatGPT 前向视觉模型describeImagesInPlace(...)在路由模型看到请求之前把图片内容部分替换为文本描述。从源码看src/vision/index.ts这个预处理还包含一整套工程细节有界 LRU 描述缓存默认最多 256 条目、1 MiB 字节VISION_DESCRIPTION_CACHE_MAX_BYTES以图片内容哈希 上下文哈希 后端/模型/推理设置组合为键data:base64 内联图片被视为可持久缓存persistent true可配合enforceAppOwnedMemoryBudget做内存回收有界并发VISION_CONCURRENCY 3多图回合不会串行累加每张图的延迟输出钳制每张图片描述硬上限DESC_MAX_CHARS 2000字符用户文本上下文上限CONTEXT_MAX_CHARS 800字符防止多图回合撑爆主模型上下文降级语义失败或超过maxDescriptionsPerTurn时替换文本为[An image was attached but could not be processed: ...]或[Image content — described by a vision model because you cannot see images directly: ...]形式的标记保证纯文本模型仍然能看到这里有一张图。5.2 两条原生图片路径研究文档区分了 codex-rs 中两种不同的原生图片能力1. 用户附加图片user-attached imagesContentItem::InputImage与UserInput::Image以图片 URL 形式携带TUI/App 把它们作为用户消息的一部分渲染remote_image_urls对普通视觉输入不存在单独的模型读取图片输出项——原生视觉模型只是收到图片。2. 本地view_image工具codex-rs 暴露view_image工具core/src/tools/handlers/view_image.rs加载本地文件发射TurnItem::ImageView随后返回包含input_image内容项的function_call_outputprotocol/src/items.rs把TurnItem::ImageView映射为EventMsg::ViewImageToolCalltui/src/history_cell/patches.rs渲染Viewed Image。5.3 可行性分析不对称的结论研究文档给出了与 Web 搜索不同的可行性结论对于view_image工具结果当本地view_image工具实际运行时codex-rs已经产生了原生式 UIopencodex 会收到携带input_image的后续function_call_output对于纯文本路由模型Vision sidecar 描述该图片并替换为文本无需额外原生 UI 事件——新增一个反而会重复已有的Viewed Image活动。对于用户附加图片Codex 已经在用户消息中显示附件原生 OpenAI 视觉不产生单独的读取图片活动项sidecar 可以可选地发射一个诊断/仅代理状态但codex-rs 原生不存在与web_search_call对应的Vision sidecar 描述了这张输入图片的 Responses 输出项伪造ImageView在语义上是错误的除非图片确实来自真实的本地view_image工具——因为ImageViewItem要求本地路径并代表一次工具执行。5.4 建议先做 Web 搜索Vision 保持现状研究文档的最终建议非常明确优先实现原生式web_search_call重发射即 4.1–4.3 节的落地路径Vision sidecar UI 暂时保持现状view_image已有原生 UI用户附件已作为用户消息图片渲染sidecar 描述是内部预处理步骤不是原生 Codex 事件若确实需要可见性以后添加一个透明的 opencodex 诊断/状态事件而不是假装模型或 Codex 运行了本地图片查看工具。六、底线结论研究文档的 Bottom line 可以概括为一张不对称的结论表维度Web 搜索 sidecarVision / image-read sidecarcodex-rs 原生输出项有web_search_callResponseItem::WebSearchCall无图片已读取输出项本地view_imageUI—已由 codex-rs 在工具实际执行时发出用户附件渲染—已作为用户消息图片渲染parity 可行性直接可行opencodex 侧 bridge 改动不可行/不必要伪造事件语义错误落地形态发射web_search_calladded/done 对 → 原生 Searched the web 单元格 Sources 徽标请求预处理如需可见性走透明诊断事件一句话总结web_searchparity 之所以直截了当是因为 codex-rs 原生拥有web_search_call输出项opencodex 只需在正确时序发射即可Vision sidecar parity 之所以性质不同是因为原生 Codex 不把图片被读取暴露为模型输出项而本地view_image工具的真实 UI 已经由 codex-rs 自身负责——任何试图伪造的ImageView都是语义错误。附进一步阅读指引研究文档00_research.md落地计划与验证10_phase1_websearch-native-ui.md、20_phase1_verification.md、30_phase2_websearch-fidelity.md、40_phase3_websearch-sources.md核心源码src/web-search/loop.ts、src/web-search/executor.ts、src/web-search/parse.ts、src/web-search/synthetic-tool.ts、src/vision/index.ts、src/bridge.ts相关测试tests/web-search/web-search.test.ts、tests/web-search/web-search-progress-stream.test.ts、tests/web-search/web-search-passthrough-bridge.test.ts、tests/adapters/bridge.test.ts【免费下载链接】opencodexUniversal provider proxy for OpenAI Codex Claude Code — use any LLM (Claude, Gemini, Grok, DeepSeek, Ollama…) with Codex CLI, App, SDK, and Claude Code项目地址: https://gitcode.com/gh_mirrors/ope/opencodex创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考