Skip to content

WebSearch @ Anthropic

Zhenyu FU edited this page May 3, 2026 · 1 revision

Anthropic-Compatible API WebSearch 工具桥接方案

背景

本项目(gcli2api)将 Google Gemini CLI (GCLI) 和 Antigravity 内部 API 桥接为 Anthropic-compatible API。WebSearch 工具在两个体系中实现方式不同,需要桥接。

当前状态

  • GCLI 模式:通过模型名后缀 -search(如 gemini-2.5-flash-search)在 normalize_gemini_request() 中自动注入 {"googleSearch": {}} 工具
  • Antigravity 模式:无搜索支持
  • Anthropic-compatible 端点(/v1/messages/antigravity/v1/messages):不支持 Anthropic 原生 web_search_* 工具类型——客户端传入此类工具时会被 convert_tools() 误当作 function 工具处理

Anthropic WebSearch 工具规格

工具定义(tools 数组)

{
  "type": "web_search_20250305",
  "name": "web_search",
  "max_uses": 5,
  "allowed_domains": ["example.com"],
  "blocked_domains": ["untrustedsource.com"],
  "user_location": {
    "type": "approximate",
    "city": "San Francisco",
    "region": "California",
    "country": "US",
    "timezone": "America/Los_Angeles"
  }
}

有两个版本:web_search_20250305(基础)和 web_search_20260209(动态过滤,需 code_execution 工具)。

关键字段:

  • type:版本化工具类型字符串,以 web_search_ 开头
  • name:固定为 "web_search"
  • max_uses:最大搜索次数限制
  • allowed_domains / blocked_domains:互斥的域名过滤
  • user_location:搜索本地化

响应格式(content 数组)

1. server_tool_use       → 模型决定搜索,含搜索 query
2. web_search_tool_result → 搜索结果列表
3. text (with citations)  → 带 source 引用标注的文本回复

server_tool_use 块

{
  "type": "server_tool_use",
  "id": "srvtoolu_01ABC",
  "name": "web_search",
  "input": {"query": "claude shannon birth date"}
}

web_search_tool_result 块

{
  "type": "web_search_tool_result",
  "tool_use_id": "srvtoolu_01ABC",
  "content": [
    {
      "type": "web_search_result",
      "url": "https://en.wikipedia.org/wiki/Claude_Shannon",
      "title": "Claude Shannon - Wikipedia",
      "page_age": "April 30, 2025",
      "encrypted_content": "..."
    }
  ]
}

text 块 with citations

{
  "type": "text",
  "text": "Claude Shannon was born on April 30, 1916.",
  "citations": [
    {
      "type": "web_search_result_location",
      "url": "https://en.wikipedia.org/wiki/Claude_Shannon",
      "title": "Claude Shannon - Wikipedia",
      "cited_text": "Claude Elwood Shannon (April 30, 1916 ...)"
    }
  ]
}

错误格式

错误通过 200 响应内的特殊 content block 返回:

{
  "type": "web_search_tool_result",
  "tool_use_id": "...",
  "content": {
    "type": "web_search_tool_result_error",
    "error_code": "max_uses_exceeded"
  }
}

错误码:too_many_requestsinvalid_inputmax_uses_exceededquery_too_longunavailable

流式 SSE 事件

event: content_block_start  → server_tool_use
event: content_block_delta  → input_json_delta (partial_json)
event: content_block_start  → web_search_tool_result
event: content_block_start  → text
event: content_block_delta  → text_delta

Usage

"usage": {
  "input_tokens": 105,
  "output_tokens": 6039,
  "server_tool_use": {
    "web_search_requests": 1
  }
}

Gemini googleSearch 工具规格

请求注入

在 Gemini API 的 tools 数组中添加:

{"googleSearch": {}}

GCLI/Antigravity 内部 API 使用此简化形式。

响应格式(candidates[].groundingMetadata)

{
  "groundingMetadata": {
    "webSearchQueries": ["..."],
    "groundingChunks": [
      {"web": {"uri": "https://...", "title": "..."}}
    ],
    "groundingSupports": [
      {
        "segment": {"startIndex": 0, "endIndex": 120, "text": "..."},
        "groundingChunkIndices": [0],
        "confidenceScores": [0.95]
      }
    ]
  }
}
  • groundingChunks[].web.uri → Anthropic url
  • groundingChunks[].web.title → Anthropic title
  • groundingSupports[].segment → 文本引用区间

实现方案

整体架构

修改 3 个核心文件,不改动路由层和 API 层:

Client Request (Anthropic web_search tool)
    │
    ▼
[Router Layer] — 不变
    │
    ▼
[Converter Layer]  ← 主要改动
    ├── models.py: 扩展数据类型
    ├── anthropic2gemini.py: 工具分离 + 请求/响应/流式转换
    └── gemini_fix.py: Antigravity 搜索注入
    │
    ▼
[API Layer] — 不变

修改 1: src/models.py

ClaudeContentBlock 添加可选字段用于 web_search 响应块:

字段 类型 用途
encrypted_content Optional[str] web_search_result 加密内容
page_age Optional[str] 页面更新时间
citations Optional[List[Dict]] text 块的引用列表

修改 2: src/converter/anthropic2gemini.py

2a. 新增 separate_web_search_tools() 函数

从 Anthropic tools[] 中分离 web_search 工具和 function 工具:

def separate_web_search_tools(tools):
    """
    Returns: (web_search_config, function_tools)
      web_search_config: {"googleSearch": {}} 或 None
      function_tools: 原有的 function 工具列表
    """

检测逻辑:tool.get("type", "").startswith("web_search_")

2b. 修改 anthropic_to_gemini_request()

在 tools 转换后,合并 googleSearch 到 tools 列表:

web_search_config, function_tools = separate_web_search_tools(payload.get("tools"))
tools = convert_tools(function_tools)  # 复用现有函数

if web_search_config:
    tools = tools or []
    tools.append({"googleSearch": web_search_config})

2c. 修改 gemini_to_anthropic_response()(非流式响应)

  1. candidate.get("groundingMetadata") 提取 ground 信息
  2. 如果存在:
    • 构建 server_tool_use 块(搜索查询)
    • 构建 web_search_tool_result 块(搜索结果)
    • 在 text 块中注入 citations(基于 groundingSupports 的 segment 映射)
  3. stop_reason 设为 "end_turn"(server_tool 不走 tool_use 循环)

2d. 修改 gemini_stream_to_anthropic_stream()(流式响应)

流式处理的难点:Gemini 的 groundingMetadata 出现在响应末尾,而 Anthropic 格式要求 server_tool_useweb_search_tool_resulttext with citations

实现策略(简化方案):

  1. 缓存所有完整响应数据
  2. 在流结束前,从缓存的 groundingMetadata 构建 web_search_tool_result 事件
  3. 将 citations 合并到最后一个 text 块中

修改 3: src/converter/gemini_fix.py

  1. 提取 inject_google_search_tool() 公共函数(从当前 geminicli 分支 line 281-285)
  2. 在 antigravity 分支添加搜索注入:检测 model 名中的 -search 后缀,注入 {"googleSearch": {}}
  3. 去重保护:如果 tools 中已有从 Anthropic converter 传入的 googleSearch,不重复添加

字段映射对照表

Anthropic Gemini
tools[{type: "web_search_*"}] tools[{"googleSearch": {}}]
server_tool_use.input.query groundingMetadata.webSearchQueries[0]
web_search_result.url groundingChunks[].web.uri
web_search_result.title groundingChunks[].web.title
citation.url groundingChunks[n].web.uri
citation.cited_text groundingSupports[].segment.text
usage.server_tool_use.web_search_requests (从 groundingMetadata 推断)

不支持的参数与搜索广度

以下 Anthropic web_search 参数在 GCLI/Antigravity 的 googleSearch 中无对应,将记录 warning 后忽略:

  • allowed_domains / blocked_domains — Gemini googleSearch 不支持域名过滤
  • user_location — Gemini googleSearch 使用 Google 账号的地区设置
  • max_uses — Gemini 内部自行管理搜索次数
  • web_search_20260209 的动态过滤 — 需要 code_execution 工具支持

搜索广度分析

格式转换本身不损失搜索广度——groundingMetadata 是 Gemini 启用 googleSearch 工具后返回搜索结果的原生标准格式,桥接层仅做翻译,不是重定向或包装。但以下因素可能实际影响搜索效果:

  1. googleSearch: {} 简化形式:GCLI/Antigravity 传入的是空对象,使用 Gemini 的默认搜索策略。Gemini 完整 API 支持 dynamic_retrieval_config(如 MODE_DYNAMIC + dynamic_threshold)来调节搜索触发阈值和广度,但 GCLI/Antigravity 内部 API 未必支持这些参数。如果后续发现这些后端支持更多参数,可进一步映射。

  2. 域名过滤丢失:由于 Gemini 的 googleSearch 不支持域名白名单/黑名单,无法通过 Anthropic 工具参数限制搜索范围,这些参数在转换时被丢弃。

  3. 地域本地化丢失user_location 参数无法传递,Gemini 会使用账号关联的默认地区设置。

兼容性论证

所有改动遵循"条件分支追加、默认不变"原则,对原有各类接口零副作用。以下逐层论证。

Pydantic 模型变更:纯增量扩展

ClaudeTool 新增 5 个 Optional 字段。原有客户端发送的 tools 不含 typemax_uses 等字段时,Pydantic 将其设为 None(默认值),model_dump(exclude_none=True) 的输出与之前完全一致。路由器调用 model_to_dict(claude_request) 后传给 converter 的数据结构不变。

ClaudeContentBlock 新增 3 个 Optional 字段,外加 class Config: extra = "allow"。原有消息中不含新字段时它们为 None,不参与序列化。extra = "allow" 是纯宽松化——从原来的"拒绝未知字段"变为"容忍未知字段",不会导致任何已有的严格校验通过而现在失败。

ClaudeUsage 新增 server_tool_use: Optional。仅在 _has_grounding_content()True 时才写入该字段。无 ground 的请求走原来的 {"input_tokens": n, "output_tokens": n} 路径,完全不变。

请求转换:条件分支默认不触发

anthropic_to_gemini_request() 的关键路径:

原有: tools = convert_tools(payload.get("tools"))
现在: web_search_config, function_tools = separate_web_search_tools(payload.get("tools"))
      tools = convert_tools(function_tools)
      if web_search_config is not None: tools += [{"googleSearch": ...}]
  • 无 web_search 工具时separate_web_search_tools 返回 (None, anthropic_tools)function_tools 即原始 toolsconvert_tools() 收到的参数和原来完全一致;web_search_config is None 分支不执行,结果等价于原代码。
  • 有 web_search 工具时:在 function tools 之外额外追加 {"googleSearch": {}},不影响原有 function calling 行为。

非流式响应转换:无 ground 时完全跳过

gemini_to_anthropic_response() 中:

原有: content = [text_blocks..., tool_use_blocks...]
现在:      [server_tool_use, web_search_tool_result] + [text_blocks..., tool_use_blocks...]
  • groundingMetadata 的响应_has_grounding_content(grounding_meta)False,两个新增代码块完全跳过,text 块不加 citations。生成的 content 数组与原一致。
  • stop_reason 逻辑完全保留has_tool_use and finish_reason == "STOP""tool_use" 的判断不受影响。ground 用的是 server_tool_use 而非 tool_use,两者互不干扰。
  • Usage 不变:无 ground 时 usage_dict 仅含 input_tokensoutput_tokens,和原来一致。

流式响应转换:完全后置追加

gemini_stream_to_anthropic_stream() 中:

原有: ...text events... → message_delta → message_stop
现在: ...text events... → [可选: server_tool_use + web_search_tool_result events] → message_delta → message_stop
  • groundingMetagrounding_meta 保持 None_has_grounding_content(None)False,整段新增代码不执行。usage 中的 **({...} if False else {}) 展开为空。流事件序列与原完全一致。
  • groundingMeta:在 message_delta 之前插入新事件,不影响已发送的 text/thinking/tool_use 事件,仅在流末尾追加信息。标准 Anthropic 客户端能正确处理额外的 content_block_start/delta/stop 事件。

gemini_fix.py:等价重构

Geminicli 分支中的搜索注入用 inject_google_search_tool() 替代内联代码,行为等价,并加了去重保护。原代码中 tool.get("googleSearch") 对空 dict {} 的 falsy 判断 bug 也已修复(改用 "googleSearch" in tool)。

Antigravity 分支新增的 -search 检测和注入是纯新增代码,不影响原有非搜索请求路径。

其他 API 格式不受影响

  • OpenAI-compatible (/v1/chat/completions):使用 openai2gemini.py,不经过 anthropic2gemini.py
  • Gemini-native (/v1/models/*:generateContent):直接使用 Gemini 格式,converter 不走 Anthropic 路径
  • -search 模型名后缀机制:在 gemini_fix.py 中完整保留
  • Thinking / anti-truncation / fake-streaming:代码路径完全不接触 web_search 逻辑
  • 路由层和 API 层src/router/src/api/ 目录零改动,请求解析、认证、Retry 逻辑全部保持原状

向后兼容

  • -search 模型名后缀机制保持不变
  • 不传 web_search 工具的请求行为完全不变
  • function calling 工具不受影响
  • web_search + function tools 可同时使用