【免费下载链接】opencodex
Universal provider proxy for OpenAI Codex & Claude Code — use any LLM (Claude, Gemini, Grok, DeepSeek, Ollama…) with Codex CLI, App, SDK, and Claude Code
导读
本文讲解 opencodex(Universal provider proxy for OpenAI Codex & Claude Code)在「原生 sidecar 对齐(native-sidecar parity)」阶段完成的一项关键协议改造:把 Web 搜索产生的来源引用(sources/citations)以 OpenAI Responses 标准的output_text.annotations形态转发给 Codex 客户端。读完本文,你将掌握该能力的前因后果、url_citation的线格式、从src/web-search/loop.ts到src/bridge/*的完整数据链路、流式与非流式两条路径的落地方式,以及去重、清空与安全过滤等细节,并能对照仓库源码与测试用例自行验证。
本文对应仓库文档:devlog/_fin/260630_native-sidecar-parity/40_phase3_websearch-sources.md,属于「260630 原生 sidecar 对齐」系列的第 3 阶段成果。
背景:问题与决策
问题现象
Codex 桌面应用(desktop app)在助手消息(assistant message)中执行过 Web 搜索后,会渲染一个Sources 芯片(chip)以及内联引用(inline citations)。而当时的 opencodex 代理已经能做到:
- 从 sidecar 解析出
url_citation并存入outcome.sources(携带 url + title); - 将这些来源通过
formatWebSearchResults拼进 toolResult 的文本中,供模型在生成回答时引用。
但问题在于:最终助手消息始终输出output_text.annotations: [],即注解数组为空,因此 GUI 永远收不到引用信息,Sources 芯片自然无法渲染。
决策(用户拍板)
决策为:归一化到 APP 的线格式(wire shape)——即用output_text.annotations[]承载url_citation条目。这样做的原因与影响:
- 对 codex-rs(TUI)无影响:TUI 目前忽略 annotations,该改动是纯增量(additive)的;
- 对桌面应用有收益:桌面应用读取 annotations 来绘制 Sources 芯片。
从源码看,这一决策的落地痕迹非常清晰:src/types/request.ts中OcxUrlCitation的注释直接写明「Surfaced on the search-end event and rendered by the bridge as aurl_citationannotation on the following assistant message (the desktop app's Sources chip reads these; the TUI ignores annotations, so this is additive)」,见 src/types/request.ts#L380-L388。
线格式:OpenAI Responses 标准注解
改造后的目标是让最终消息呈现如下标准结构(原文档中的 wire shape 示例):
{ "type": "output_text", "text": "...answer...", "annotations": [ { "type": "url_citation", "url": "https://...", "title": "Node.js Releases", "start_index": 0, "end_index": 0 } ] }关键字段说明:
| 字段 | 含义 | 本实现中的取值 |
|---|---|---|
type | 注解类型 | 固定为"url_citation" |
url | 来源链接 | sidecar 返回的实际 URL |
title | 来源标题 | 可选;sidecar 有则带,无则省略 |
start_index/end_index | 引用在正文中的字符区间 | 固定为0/0(见下文 OUT of scope) |
变更地图(IN scope):四条改动路径
原文档将改动范围划为 4 项,逐一对应到仓库源码如下。
1. 类型定义:OcxUrlCitation与搜索结束事件
在 src/types/request.ts#L385-L388 新增接口:
export interface OcxUrlCitation { url: string; title?: string; }它被携带在搜索结束事件上:web_search_call_end.sources?: OcxUrlCitation[]。也就是说,搜索结束事件现在可选地携带sources数组,为空时连字段都不出现(保持向后兼容)。
2. 搜索循环:批量去重并挂载 sources
在 src/web-search/loop.ts 的runSearchCall中,完成批量(batch)查询结果的来源收集与去重:
const sources: { url: string; title?: string }[] = []; const seenSrc = new Set<string>(); for (const r of results) { for (const s of r.outcome.sources) { if (seenSrc.has(s.url)) continue; seenSrc.add(s.url); sources.push(s.title ? { url: s.url, title: s.title } : { url: s.url }); } } yield { type: "web_search_call_end", id: call.id, queries: call.queries, status: anySuccess ? "completed" : "failed", ...(sources.length > 0 ? { sources } : {}), };见 src/web-search/loop.ts#L807-L821。要点:
- 按 URL 去重:同一批次内多条查询可能命中同一链接,只保留一条;
- title 可选:有 title 带 title,没有则只带 url;
- 无来源不出字段:
sources为空时不输出该属性,避免污染事件结构; status依「是否存在成功结果」取completed或failed,queries保留全部尝试过的查询以便 Codex 渲染原生复数标签。
3. 流式路径:bridgeToResponsesSSE 挂载注解
在流式桥接 src/bridge/sse.ts 中维护pendingWebSources缓冲:
let pendingWebSources: { url: string; title?: string }[] = [];当收到web_search_call_end事件时通过appendSafeWebSearchSource(pendingWebSources, source)逐条累积(带安全过滤,见下文);当下一条助手消息闭合时,将其取出并映射为 annotations:
const anns = pendingWebSources.map(s => ({ type: "url_citation", url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0, }));随后同时通过content_part.done与output_item.done两条 SSE 事件下发(分别对应part: { type: "output_text", text, annotations }与content: [{ type: "output_text", text, annotations }],见 src/bridge/sse.ts#L443-L499),并在取走后立即清空缓冲,确保来源只绑定到恰好一条消息。
4. 非流式路径:buildResponseJSON 挂载注解
非流式路径同样维护pendingWebSources(src/bridge/response-json.ts#L195-L196),在flushText()中挂载并清空:
const annotations = pendingWebSources.map(s => ({ type: "url_citation", url: s.url, ...(s.title ? { title: s.title } : {}), start_index: 0, end_index: 0, })); pendingWebSources = []; content: [{ type: "output_text", text, annotations }],见 src/bridge/response-json.ts#L221-L228。同时该路径还包含一个容量保护:sourceBytes统计了待挂载来源的 JSON 字节数(src/bridge/response-json.ts#L221、L546-L548),配合安全过滤共同约束进入消息的注解规模。
OUT of scope:明确不做的事
原文档明确划出两项范围外工作,仓库实现也确实未涉及:
- 不做内联字符区间引用:
start_index/end_index本应指向正文中的具体字符位置,本实现固定发射0/0;桌面应用通过 url/title 绘制 Sources 芯片,不需要精确区间。同时 src/web-search/parse.ts 在解析侧本就丢弃 start/end 索引,只保留 url 与 title。 - 不改 toolResult 文本格式:模型仍然在文本内收到来源(
formatWebSearchResults保持原样),只是消息注解层新增了结构化的引用。
数据链路全景
综合上述改动,一次带 Web 搜索的对话在 opencodex 代理内的完整引用链路为:
- sidecar 返回结果:解析侧(src/web-search/parse.ts)从完成态的 Responses
output[]数组与流式注解事件中提取来源——支持注解形态(response.output_text.annotation.added携带url_citation)与正文尾部Sources:区块两种形态; - search call 事件:src/web-search/loop.ts 的
runSearchCall产出web_search_call_begin→web_search_call_end,后者携带去重后的sources; - bridge 缓冲:
src/bridge/sse.ts(流式)与src/bridge/response-json.ts(非流式)分别把 sources 累积进pendingWebSources; - 消息闭合挂载:下一条助手消息闭合时,把缓冲映射为
url_citationannotations,通过content_part.done/output_item.done(流式)或最终 JSON 的output[].message.content(非流式)下发; - 清空缓冲:挂载后立即清空,保证引用只绑定到恰好一条消息。
安全与健壮性细节
引用来自外部搜索后端,属于不可信输入,仓库实现了多层防护:
- 协议安全过滤:
appendSafeWebSearchSource会拒绝不安全的 URL(如javascript:alert(1))、含凭证的 URL(如https://user:pass@...)、含控制字符的路径,以及含非法控制字符的 title; - 去重:同一 URL 只保留首次出现的 title;
- 数量与字节上限:注解数量与 JSON 字节数均受约束,防止单条消息被大量来源撑爆(见 src/bridge/response-json.ts#L221 的
sourceBytes统计)。
对应测试用例 tests/adapters/bridge.test.ts#L1200-L1210 明确验证了「unsafe and oversized search sources are absent from cells and annotations」——恶意与超限来源既不会进入搜索 cell,也不会进入注解。
验收标准与测试印证
原文档给出的验收标准在仓库测试中均有对应覆盖:
验收 1:真实搜索后,流式与非流式路径的output_text.annotations均包含每个唯一来源的url_citation。
- 流式路径:tests/adapters/bridge.test.ts#L1161-L1180「streaming: web_search_call_end sources attach as url_citation annotations on the next message」——重放
web_search_call_begin→web_search_call_end(sources)→text_delta→done,断言response.output_item.done的 message 首 part 的annotations等于[{ type: "url_citation", url: "https://nodejs.org", title: "Node.js", start_index: 0, end_index: 0 }]; - 非流式路径:tests/adapters/bridge.test.ts#L1182-L1198 以
buildResponseJSON验证同样的注解出现在最终output[].message.content[0].annotations。 - 端到端: tests/web-search/web-search.test.ts#L2050-L2097 用真实 sidecar 返回的
url_citation注解(https://nodejs.org/en/about/previous-releases/ "Node.js Releases")验证注解到达助手消息;L2101 起 还覆盖了「注解为空 + 正文 Sources 区块」的真实场景——sidecar 通常省略注解而把来源列在正文尾部,此时解析侧从Sources:区块提取并仍能产出url_citation注解(如 Node.js Download page、Node.js release archive 两条)。
验收 2:无搜索(或搜索失败/为空且无来源)的回合保持annotations: [],不回归。
桥接层在无pendingWebSources时发射空注解数组:流式路径的takeWebAnnotations()在缓冲为空时返回[](src/bridge/sse.ts#L448-L456),非流式路径同样在flushText()中输出空数组。
验收 3:来源绑定到搜索后的第一条助手消息,随后缓冲清空。
pendingWebSources在挂载后立即置空(src/bridge/response-json.ts#L225 与 src/bridge/sse.ts#L449 的take语义),确保不会泄漏到后续消息。
批量去重专项测试:tests/web-search/web-search.test.ts#L2169-L2259「web-search batched sources -> url_citation annotations」验证了两条查询命中同一 URL 时只产出一条注解(https://shared.test/doc只出现一次),不同 URL 则各自保留(https://shared.test/uniqueA独立成条),与loop.ts中seenSrc去重逻辑一一对应。
总结
第 3 阶段「Web-search sources/citations to GUI」为 opencodex 补齐了 Web 搜索引用到 GUI 的最后一公里:以output_text.annotations承载url_citation的标准线格式,让 Codex 桌面应用的 Sources 芯片得以渲染,同时通过「挂载后即清空」保证引用绑定语义精确、通过去重与安全过滤保证注解内容可信,且对忽略注解的 TUI 完全透明、对无搜索回合零回归。对于希望深入协议实现的读者,建议按 src/types/request.ts → src/web-search/loop.ts → src/web-search/parse.ts → src/bridge/sse.ts / src/bridge/response-json.ts 的顺序阅读,并以 tests/adapters/bridge.test.ts 与 tests/web-search/web-search.test.ts 中的注解相关用例作为行为契约。
【免费下载链接】opencodex
Universal provider proxy for OpenAI Codex & Claude Code — use any LLM (Claude, Gemini, Grok, DeepSeek, Ollama…) with Codex CLI, App, SDK, and Claude Code
相关推荐
AgentsView Desktop 桌面端实战:用 Tauri Sidecar 把 Go 后端打包进原生桌面应用
AgentsView Desktop 桌面端实战:用 Tauri Sidecar 把 Go 后端打包进原生桌面应用 本文基于仓库内 desktop/README
AI 应用数据分析数据可视化可观测性终极指南:5步将Web应用桌面化,打造原生体验的桌面应用
终极指南:5步将Web应用桌面化,打造原生体验的桌面应用 Nativefier是一款强大的工具,能让你轻松将任何网页转变为桌面应用,带来原生应用般的使用体验。无
CLI桌面应用开发工具opencodex 的 Kiro 适配器实现解析:从 AWS Eventstream 解码到 Codex CLI 端到端打通
opencodex 的 Kiro 适配器实现解析:从 AWS Eventstream 解码到 Codex CLI 端到端打通 本文以 opencodex 仓库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考