Skip to content

feat(messages): 支持 Anthropic 服务端工具 web_search_20250305,经 CC 的 /alpha/web-search 执行 - #46

Open
atogumo wants to merge 3 commits into
MAXeaglet:masterfrom
atogumo:feat/web-search
Open

atogumo wants to merge 3 commits into
MAXeaglet:masterfrom
atogumo:feat/web-search

Conversation

@atogumo

@atogumo atogumo commented Sep 17, 2026 •

Copy link
Copy Markdown

问题

Anthropic 的 web_search_20250305 是服务端工具,由 Anthropic 的 API 执行;经 CC 上游时没有人执行它。现有实现按普通工具映射(t.input_schema || {}),模型拿到的是一个无描述、空参数的 function tool——要么不用,要么调了之后客户端找不到处理器。Claude Code 的 WebSearch 正是通过这个工具类型实现的,因此在本代理后面不可用。

改动

新增开关 CC_WEB_SEARCH=1,开启后由代理执行该工具,调 CC 自带的 /alpha/web-search(官方 CLI 内置搜索用的同一端点、同一把 key):

模型发出 web_search{query} → 代理 POST /alpha/web-search → 以 server_tool_use + web_search_tool_result 块回给客户端
→ 调用/结果对追加进历史、重发上游 → 模型在同一条消息里续写

  • 线格对齐 Anthropic:server_tool_use / input_json_delta / web_search_tool_result(含 error 形态)/ usage.server_tool_use.web_search_requests;Claude Code 按原生 Web Search(...) 渲染
  • 遵守 max_uses:超额返回 max_uses_exceeded,并将该工具移出后续轮次的定义(CC 的 tool_choice 只有 auto / any / tool,没有 none);模型仍连续调用则本地收尾,保证 message_stop 一定发出
  • 历史回放:上一轮的 server_tool_use / web_search_tool_result 块拆成 assistant{tool_calls} → tool → assistant{text}
  • 剥掉 DDG 赞助结果并多要几条补位;allowed_domains / blocked_domains 按 hostname 生效;单次超时 CC_WEB_SEARCH_TIMEOUT_MS(默认 8s)
  • 搜索请求经 upstreamFetch 发出,配置了 CC_UPSTREAM_PROXY (feat: 支持为上游请求配置 HTTP(S) 代理(ProxyAgent / 环境变量) #18) 时与 generate 一样走代理

默认行为变化

开关默认关闭。关闭时唯一的变化:Anthropic 服务端工具(web_search_20250305 / web_fetch_20250910)
被剥掉,而不是压成坏工具转发。其余路径输出不变,既有 22 条测试未改全过。

范围

  • 仅 /v1/messages 流式;非流式仍剥掉服务端工具
  • /v1/chat/completions、/v1/responses、buildCcRequest、forwardToCC 未动
  • 未实现:citations、web_fetch_20250910(Claude Code 的 Fetch 在客户端执行,不依赖它)

测试

test/web-search.test.mjs 11 条,mock 上游、不需要真 key:完整循环与事件序列、块索引唯一递增、
多轮 usage 汇总、上游第 2 轮请求形状、开关关闭 / 非流式剥掉、普通工具不误拦、max_uses 超额与工具移除、
失控封顶、搜索端点 5xx、并行双调用、历史回放。全量 33 条通过。

多次全量运行中偶见一条既有用例(/v1/responses 路径,本 PR 未触碰)时序抖动失败,单独复跑即过;
新增测试抬高了并行进程数,CI 若偶发可重跑。

验证

真机(Go 档 key):模型一轮并行搜索 2 次 → 额度用完工具移除 → 作答引用来源 → web_search_requests: 2 → message_stop;
Claude Code 端到端原生渲染 Web Search(...) / Did 1 search。

改动:proxy.mjs +378/−37,test/web-search.test.mjs +375,README / README_zh 补充配置项与「Web search」小节。

…WebSearch 走 CC 自带的 /alpha/web-search

Anthropic 的 `web_search_20250305` 是服务端工具,由 Anthropic 的 API 服务器执行;经 CC 上游时链路里
没有人执行它。现有实现按普通工具映射(`t.input_schema || {}`),模型拿到的是一个无描述、空参数的
function tool——要么不用,要么瞎调后客户端找不到处理器。Claude Code 的 WebSearch 是客户端工具,执行时
会另起一个约 1K token 的子请求、在子请求里带这个服务端工具——坏的正是这个子请求。

- feat: 新增开关 `CC_WEB_SEARCH=1`。默认关闭,此时服务端工具(`web_search_20250305` / `web_fetch_20250910`)被剥掉,不再变成坏工具
- feat: `convertAnthropicToOpenAI` 识别服务端工具类型;开启时把 `web_search` 改写成带真实 schema 的 function tool,`max_uses` / `allowed_domains` / `blocked_domains` 记入 `openaiReq._serverTools`
- feat: 新增 `executeWebSearch`:POST CC 的 `/alpha/web-search`(官方 CLI 内置搜索的同一端点、同一把 key,请求头与 `forwardToCC` 一致);经 `upstreamFetch` 发出,配置了 `CC_UPSTREAM_PROXY`(MAXeaglet#18)时同样走代理;剥掉 DDG 赞助结果并多要 3 条补位;域名白/黑名单按 hostname 过滤;错误码对齐 Anthropic(`too_many_requests` / `invalid_input` / `unavailable`);超时 `CC_WEB_SEARCH_TIMEOUT_MS`(默认 8s)
- feat: `createAnthropicSseTranslator` 把 `web_search` 的 tool-call 改发 `server_tool_use` 块(不当客户端 `tool_use` 下发)并登记 `ctx.pendingSearches`;有待执行搜索时 finalize 早退不发终结事件,usage 累进 `ctx.usageCarry`;块索引跨翻译器实例接续
- feat: `handleMessages` 流式路径改为搜索循环:执行搜索 → 发 `web_search_tool_result` 块 → 以 `tool_calls` + `tool` 消息追加历史(含本轮 reasoning,CC 在 thinking 模式下校验其随历史带回)→ 重建 `ccBody` 重发上游 → 模型续写;最终 `message_delta` 汇总各轮 usage 并补 `server_tool_use.web_search_requests`
- feat: `max_uses` 语义:超额返回 `web_search_tool_result_error: max_uses_exceeded`,并把该工具移出定义让模型用文本收尾(CC 的 `tool_choice` 只有 auto / any / tool,没有 none——真机 400 验证);模型连续两轮仍调则本地补终结事件,流一定有 `message_stop`
- feat: 历史回放支持 `server_tool_use` / `web_search_tool_result`(含 error 形态):一条 Anthropic assistant 消息拆成 `assistant{tool_calls}` → `tool{result}` → `assistant{text}`,`encrypted_content` 解回摘要
- fix: 非流式 `/v1/messages` 不走循环,服务端工具退回剥掉(Claude Code 只走流式)
- test: 新增 `test/web-search.test.mjs` 11 条(mock 上游,不需要真 key):完整循环与事件序列、块索引唯一递增、两轮 usage 汇总、上游第 2 轮请求形状、开关关闭 / 非流式剥掉、普通工具不误拦不循环、`max_uses` 超额与工具移除、失控封顶、搜索端点 5xx、并行双调用两例、两种历史回放;全量 33 条通过
- docs: README / README_zh 补充 `webSearch` / `CC_WEB_SEARCH` / `CC_WEB_SEARCH_TIMEOUT_MS` 与「Web search」小节
- chore: 启动日志增加 `webSearch` 状态行

真机验证:Go 档 key 直打 proxy,模型一轮并行搜索 2 次 → 额度用完工具被移除 → 作答并引用来源 →
`web_search_requests: 2` → `message_stop`,第 2 轮命中 prompt cache;Claude Code 端到端原生渲染
`Web Search(...) / Did 1 search`。

未实现:citations(模型不产出结构化引用)、`web_fetch_20250910`(一律剥掉)。
另:`convertAnthropicToOpenAI` 里既有的 Anthropic `tool_choice: {type:"none"}` → OpenAI `"none"` → CC `{type:"none"}`
映射同样会被 CC 拒绝,属既有问题,另开 issue。
… + 释放 respReq 内存 (MAXeaglet#31)

从上游 MAXeaglet/commandcode-proxy@40b338e 挑入。CC 上游的 tool_choice 枚举只有
auto / any / tool,原样下发 { type: 'none' } 会被 400 拒绝;改为下发空 tools
列表且不带 tool_choice,等价于「本轮禁用工具」。同时在 /v1/responses 路径里
及时释放 respReq。

上游的回归测试写在 test/wire.test.mjs,本分支没有该文件,改放到
test/tool-choice-none.test.mjs,用本分支的 helpers 跑同样的两条断言。
带入上游 10 月的修复:finish 原因归一化(a474cbc)、/v1/responses 零输出
不再谎报 completed(aa4a3c1)、chat 路径的上游闪断透明重试(ce5a217、
639fc6e)、Codex 工具截图与用户图片的线格修正(2eccdbf、7f6bf21、8c45a05),
以及 MAXeaglet#47 的 tool_choice: none 处理(40b338e,此前已挑入)。

冲突一处:/v1/messages 入口上游把 anthropicReq / openaiReq 提前置空回收
内存,本分支的搜索循环要复用这棵树,流式路径后面也仍引用
openaiReq._serverTools,保留本分支版本;chat / responses 路径的提前回收照常
合入。README 功能行取上游版本并保留搜索一项。

上游 wire 测试已含 MAXeaglet#47 两条用例,删除本分支临时补的
test/tool-choice-none.test.mjs。合并后全套 158 条测试通过,web search 的
11 条原样通过。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant