一次审批门翻转:opencodex 让 Cursor 适配器的模型重新用上 Web 搜索
【免费下载链接】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 代理的 Cursor 适配器,把三个 Web 搜索审批查询的应答从 rejected 翻转为 approved,模型在 Cursor 路径上的原生 Web 搜索随之恢复。
先看结果
改造之后,经由 Cursor 适配器路径服务的模型(比如cursor/grok-4.5)可以发起原生联网检索了:搜索由 Cursor 服务端执行,结果注入模型上下文,答案照常以textDelta帧流回客户端。之前用户日志里那个web_search -> non-interactive bridge reject的死门,变成了真正搜得到内容的联网回合。🔓
它为什么之前卡住
opencodex 的 Cursor 适配器 负责把上游模型请求按 Cursor agent 协议转发给 Cursor 服务端。
关键在于协议里的一个细节:模型想联网之前,Cursor 服务端会先向客户端发一个interactionQuery权限审批请求——"我想执行webSearchRequestQuery,准吗?"客户端不答或拒绝,模型就放弃搜索。
opencodex 的桥接层是个无头客户端,没有真人坐在回合中间点按钮。之前的默认策略是所有交互查询一律回rejected,统一理由:"opencodex bridge is non-interactive; proceed without this interaction." 类比一下:门卫的原则是"谁要出门都不放行",模型伸手要搜索工具,桥接层在门口把它拦下了。搜索能力不是模型不会用,是被这一层挡死的。
开关是怎么翻的:空批准就是把搜索委托出去
Cursor 的审批协议是个二态开关。看生成的 protobuf 定义 agent_pb.ts:WebSearchRequestResponse的result是个oneof,只有两个分支——Approved和Rejected。
有意思的是Approved分支是个空消息:不带reason,不带任何参数,也没有"结果"字段可填。所以回一个空批准,语义上就等于"授权加委托"——搜索本体不在客户端执行,而是交给 Cursor 服务端去跑,服务端把结果注入模型上下文,模型的回答再流回来。
批准后服务端还会发来web_search_tool_call/exa_*_tool_call这类原生工具帧。它们只是展示层帧,protobuf-events.ts 里的mcpArgsFromToolCall()只对 opencodex 桥接的 Responses 工具返回参数,live-transport.ts 里的isClientToolFrame()也只认自己桥接的工具帧,原生工具帧会被事件映射器安全忽略——流不会停滞,也不会凭空冒出"不完整的工具调用"。✅
改动明细
- 导入区(
live-transport.ts):新增WebSearchRequestResponse_ApprovedSchema、ExaSearchRequestResponse_ApprovedSchema、ExaFetchRequestResponse_ApprovedSchema三个导入;移除翻转后不再使用的三个*RequestResponse_RejectedSchema;保留SwitchModeRequestResponse_RejectedSchema(switchMode 仍拒绝)。 - 分支翻转:live-transport.ts 中
planInteractionQueryReply()的webSearchRequestQuery/exaSearchRequestQuery/exaFetchRequestQuery三个分支,内部result从 rejected 换成空批准消息,replyCase标记同步改为*RequestResponse:approved:
// 翻转前:result: { case: "rejected", value: create(WebSearchRequestResponse_RejectedSchema, { reason: NON_INTERACTIVE_REASON }) } // 翻转后:result: { case: "approved", value: create(WebSearchRequestResponse_ApprovedSchema, {}) } // replyCase: "webSearchRequestResponse:approved"- 不动的部分:
askQuestion、switchMode继续拒绝(回合中间没人应答,代理必须自主推进);createPlan回 success,plan 文本透传给 Codex 侧可见;setupVmEnvironment与未知查询回空应答加unsupported标记——宁可空答也不能抛错,抛错会把整条 gRPC 连接拖死。
📝 全部改动只落在src/adapters/cursor/live-transport.ts一个源文件加一个测试文件。
怎么确认它生效了:翻转之后怎么写测试
原来测试用一个参数化断言矩阵,一把梭断言 switchMode + web + exa 全部rejected。翻转之后,一个矩阵装不下两种结论,于是拆开:
- switchMode 单列一个 case,断言结果仍是
rejected; - web 和 exa 三兄弟一组,断言
result.case === "approved"、replyCase带:approved后缀、id原样回传。
断言只读响应对象的结构,不需要导入三个 Approved schema。同一测试文件还守着其余分支的回归断言:createPlan回显 plan 文本、askQuestion被拒、空应答与unsupported标记、原生readToolCall不算客户端工具活动等,保证翻转没有波及其他交互路径。
验证命令:
bun test tests/providers/cursor/cursor-*.test.ts bun x tsc --noEmit第一条跑 Cursor 适配器的全部测试,第二条确认类型干净。
代价与边界
批准不是免费的:搜索真的发生在 Cursor 服务端,会消耗用户 Cursor 账户的 web-search/Exa 配额,这部分开销算在用户头上,是这次改动必须认账的代价。⚖️
还有两条路径要分清:opencodex 自带合成 web_search sidecar(server/responses.ts的planWebSearch+runWithWebSearch),那是代理侧的另一条搜索路径,只在客户端请求里带 hostedweb_search工具时触发;这次改的是模型在 Cursor 适配器路径上使用Cursor 原生 Web 搜索的场景。两条路径正交,互不覆盖,也不会让原生搜索"顶掉"代理侧合成搜索。
收尾
这次改动刻意保持最小:一个纯函数决策点、一个测试文件、两个导入清单,不碰其他适配器、配置开关和版本号,把审批门每个分支的语义都用显式测试钉住。后续要给搜索加开关或额度控制,也是在同一个函数里加分支的事。
【免费下载链接】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),仅供参考