OMP 搜索与 Codex 自定义反代补丁设计

makoMakoGo 于 2026-05-06 发布

1. 先说结论

这次梳理下来,关于 OMP 的 search,可以先记住几件事:

  1. OMP 的 web search 是一层统一抽象,不是某一家 provider 的硬编码逻辑

    • 入口是统一的 web_search 工具
    • 下面再挂 Exa、Codex、Brave、Perplexity、Anthropic、Gemini 等 provider
  2. providers.webSearch 是“首选 provider”,不是“强制唯一 provider”

    • 它会影响优先顺序
    • 但如果首选 provider 不可用或执行失败,OMP 仍然会 fallback 到后面的 provider
  3. Codex 在 OMP 里本来就是一个 search provider

    • 原始设计偏向 OpenAI Codex OAuth 场景
    • 但实际上可以扩展成兼容 Responses 风格的自定义反代 / BYOK 网关
  4. 这次 Codex patch 的核心目标,不是新造一个 provider,而是让现有 Codex provider 能吃下“自定义 Codex 反代”

    • 保留原来的 OAuth 行为
    • 新增对 models.yml 里 Responses-compatible backend 的解析
    • 新增对一些“不返回结构化 citation”的反代网关的兼容
  5. Sources: 0 的根因,不一定是没搜,而可能是反代没有按 OMP 预期返回 citation 结构

    • 我们实际抓到过 web_search_call 已经发生
    • 但返回里没有 url_citation
    • 最终答案正文里只有 markdown 链接
  6. omp q 这条独立 CLI 路径,和 SDK/TUI 主启动路径不是一条初始化链

    • 所以 providers.webSearch = exa 这种偏好设置
    • 需要在 src/cli/web-search-cli.ts 里单独初始化一次
    • 不然它会直接按默认 auto 顺序选 provider

一句话概括:


2. OMP 的 search 功能大体是怎么分层的

我这次实际确认到的关键文件有这些:

2.1 provider 注册与顺序

文件:

这里负责:

当前 auto 顺序里,关键片段是:

也就是说:

这点后来解释了为什么:

2.2 统一执行入口

文件:

这里的职责是:

这层设计说明一件事:

所以后面用户说:

这是对的。

因为从现有实现看:

2.3 CLI 测试命令

文件:

omp q / omp web-search 本质上是一个“测试 search provider 的 CLI 命令”。

它不是完整 agent 会话,也不是完整 SDK 启动链。

所以一个很关键的点是:

这就是后面出现偏差的原因。

2.4 配置项定义

文件:

这里定义了:

这个设置项的语义,不是:

而是:


3. OMP search 的 provider 选择逻辑到底是什么

这个问题最容易被误解。

3.1 显式 --provider 优先级最高

如果命令写了:

omp q --provider codex "what is ai infra"

那就是:

这个适合做:

3.2 没写 --provider 时,才走 preference + fallback

如果命令写的是:

omp q "what is the gold price today"

那才会走:

语义可以理解成:

  1. 先试首选 provider
  2. 如果首选不可用或失败
  3. 再试固定 auto 顺序里的其余 provider

所以:

3.3 这次踩到的 CLI 回归点

实际确认到的问题是:

结果就是:

而默认 auto 顺序里:

所以就出现了:

这不是 search 总体设计问题,而是 CLI 搜索入口漏做初始化


4. 这次 Codex 自定义反代为什么会出问题

这次调的是一个自定义 Codex backend,本地配置大意是:

也就是说:

原始 OMP 里,对 Codex search 的预期比较偏向:

但真实世界的网关并不总是这么稳定。

一些兼容层会出现:

所以这次 patch 里加了:

tool_choice: { type: "web_search" }

设计目的很明确:

4.2 第二个问题:反代不一定返回 url_citation

这是这次最关键的坑。

我们实际抓到的现象是:

也就是说:

于是 OMP 就会误判成:

根因不是“没搜”,而是:


5. 这次 Codex patch 的设计目标是什么

目标不是“为这个反代单独加一堆特殊逻辑”,而是:

我认为这次 patch 的设计目标可以拆成 4 条:

5.1 保留原始 OAuth 路径

原有行为:

这条路径不能被破坏。

所以补丁不是替换它,而是做成:

这样好处是:

5.2 不新造一个 provider id

这次没有新增一个什么:

而是继续沿用:

这点很重要。

因为一旦新造 provider:

而真实问题其实不是“缺一个新 provider”,而是:

所以正确做法是:

这比新造 provider 更干净。

5.3 当前实际生效的是哪一层

先说当前 live backend 的实测结论。

对这台机器当前这条配置:

我重新抓了一次 streaming Responses 结果,观察到的是:

也就是说,对当前这条 live codex-proxy + gpt-5.4(xhigh) 路径,真正命中的提取层是:

  1. url_citation没命中
  2. markdown links:命中,这就是现在实际生效的层
  3. web_search_call.action.url存在,但当前没有被用上

所以如果只问“现在到底是哪层在生效”,答案很明确:

tool URL 那层只是代码里保留的安全边界,不是当前这条线上请求正在依赖的主路径。

5.4 保留 OMP 原有 fallback 哲学

用户已经明确说过:

而这和 OMP 现有设计也是一致的。

所以这次 patch 的边界很清楚:


6. 这次补丁在代码层面到底做了什么

下面是我认为最关键的几个设计点。

6.1 从 models.yml 解析自定义 Codex backend

关键文件:

新增逻辑大意是:

  1. 读取 ~/.omp/agent/config.yml
  2. 读取 ~/.omp/agent/models.yml
  3. 看当前默认模型或 PI_CODEX_WEB_SEARCH_MODEL 指向哪个 provider/model
  4. models.yml 里找:
    • api: openai-responses
    • api: openai-codex-responses
  5. 如果该 provider 有:
    • baseUrl
    • apiKey
    • models[].id
  6. 就把它当成 Codex search backend

这个设计的价值是:

6.2 backend 解析顺序:BYOK 优先,OAuth 次之

补丁里抽成了一个 backend 解析层,语义是:

这个顺序是合理的。

因为:

但如果用户根本没配:

6.3 规范化 Responses URL

不是所有网关给的 baseUrl 都完全一致。

有些会写成:

有些可能写成:

所以补丁里加了一个 URL 归一化逻辑:

这类小逻辑非常值钱。

因为它决定了:

这个前面提过,但值得单独强调。

它不是为了“改 search 哲学”,而是为了:

换句话说:

6.5 source 提取从单一 annotation,改成 layered extraction

这个是 Sources: 0 问题的直接修复点。

原来大体是:

现在改成:

  1. 先提 annotationSources
  2. 再提 markdownSources
  3. 再提 toolSources
  4. 最后按优先级选一个非空集合

这使得 OMP 对 gateway 的容错性高很多。

6.6 hasCodexSearch() 也跟着扩展

如果 search backend 的判定逻辑变了:

否则会出现:

所以这里一起改成:


7. 为什么这次设计是合理的

我觉得合理点主要在这里。

7.1 它解决的是“边界定义太窄”,不是“少一个功能按钮”

原来 Codex provider 隐含的边界是:

但真实世界里,很多人用的是:

所以真正的问题不是:

而是:

这次 patch 是把抽象边界修正成更贴近现实,而不是乱加旁路。

7.2 它没有引入双轨设计

一个坏设计会变成:

这会让后续维护复杂度上升。

这次做法没有这么搞。

仍然是:

7.3 它保住了“搜索已发生”这个真相

Sources: 0 最糟糕的地方在于:

这会误导人。

所以这次 layered extraction 的价值,不只是“让 UI 好看一点”,而是:


8. 14.1.1 升级后,为什么还要补一刀 CLI search preference

升级到 14.1.1 后,有个额外发现:

最后确认根因不是 search provider 本体,而是:

所以我们又在这条 CLI 路径补了:

  1. Settings.init()
  2. 读取 providers.webSearch
  3. setPreferredSearchProvider(...)

这样修完之后:

omp config get providers.webSearch
# exa

omp q --compact "what is the gold price today"
# Provider: Exa

omp q --provider codex --compact "what is ai infra"
# Provider: Codex

这说明:


9. 当前本地实际结论

我这边最后确认到的本地状态是:

9.1 当前 search 偏好

文件:

关键项:

providers:
  webSearch: exa

语义:

9.2 当前默认模型

同一份配置里还有:

modelRoles:
  default: codex-proxy/gpt-5.4(xhigh)

这说明:

这两者不是一回事。

9.3 当前自定义 Codex backend

文件:

关键点:

这就是这次 patch 要兼容的典型“自定义 Codex 反代”形态。


10. 这次涉及到的本地补丁位置

10.1 安装态 OMP 运行时代码

当前实际打补丁的位置是:

这里是本机真正运行的 OMP 代码,不是源码仓库里的参考实现。

10.2 fish-claude 里的 patch 文件

为了后续升级后能重放补丁,对应 patch 也同步到了:

另外一个无关 search 但一起维护的补丁是:


11. 我对这套设计的最终理解

如果以后我自己再看这块,我会把它记成下面这几句:

11.1 OMP search 的本质

11.2 Codex patch 的本质

11.3 为什么这次 patch 值得做

因为它修的不是 UI 表面症状,而是三层更根的东西:

  1. backend 识别范围 太窄
  2. source 识别范围 太窄
  3. CLI preference 初始化 漏了一条路径

把这三层补上后:


12. 以后调试这块时,我会优先怎么想

如果以后再遇到 OMP search 问题,我会先分三类看:

12.1 是 provider 没被选中

先看:

12.2 是 provider 被选中了,但 search 没真的发生

先看:

12.3 是 search 发生了,但 source 没显示出来

先看:

这样排就不会把三层问题混成一个问题。