Skip to content

接入 Web Search Gateway 支持 BYOK - #141

Open
Postroggy wants to merge 25 commits into
superduck-ai:mainfrom
Postroggy:feat/web-search-main
Open

接入 Web Search Gateway 支持 BYOK#141
Postroggy wants to merge 25 commits into
superduck-ai:mainfrom
Postroggy:feat/web-search-main

Conversation

@Postroggy

@Postroggy Postroggy commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

背景与边界

Claude Code 2.1.120 的实际链路分成两层:

  1. 主 agent loop 向 BYOK 模型声明普通 client tools,其中包括 name=WebSearch;这一层由 Claude Code 自己执行,OMA 不接管 Bash、Edit、MCP 等普通 client tool。
  2. 当主模型实际选择 WebSearch 后,Claude Code 的内置执行器会另发一条 Messages 请求,声明 type=web_search_20250305 的 Anthropic server tool。OMA 只拦截这一层的已知 Web Search server-tool type。

OMA 将被拦截的 server tool 投影为 BYOK 可调用的普通 web_search tool,但声明工具不等于执行搜索。只有 BYOK 响应真的返回 tool_use(name=web_search) 时,OMA 才调用 Tavily/Brave;模型不调用时不会访问搜索 provider。

当前只支持 web_search_20250305web_search_20260209web_search_20260318。未登记的 type=web_search 和其他 Anthropic server tools 保持透明转发;后续统一 registry 见 #185

本次实现

  • 提供 provider-neutral Web Search gateway,Tavily/Brave 各自解析和校验配置,凭据只保留在 OMA 服务端。
  • max_uses、域名过滤、allowed_callersresponse_inclusion 作为调用方策略校验;不能可靠模拟的 caller/location 组合在 BYOK 请求前明确拒绝,不静默降级。
  • 支持一次响应中的多个搜索调用,以及 Web Search 与多个普通 client tool 混合调用。
  • mixed turn 只把普通 tool_use 返回 Claude Code 执行;OMA 暂缓搜索。Claude Code 下一条 user message 必须只包含全部 client tool_result 并保留 Web Search tool,OMA 才执行 pending search,并按 tool_use_id 将所有结果合并回同一 BYOK user message。OMA 不再为普通 client tool 伪造 unsupported result。
  • 正确处理 JSON/SSE pause_turn:完成本次已允许的搜索后返回配对的 server_tool_use/web_search_tool_result checkpoint;续传时支持已完成结果回放、末尾未完成 server call 执行,以及连续多次暂停。
  • 将 OMA 私有的 max_tool_loops 重命名为 max_server_tool_iterations,默认值与 Anthropic server-side sampling loop 的每请求 10 次对齐。一次 iteration 是一次 BYOK Messages 请求(含初始采样),与限制实际搜索次数的 tools[].max_uses 无关。
  • 对外 web_search_result 只输出 titleurl 和可选 page_age 等 OMA 支持的可见字段,不生成或伪造 Anthropic 原生 encrypted_content、citation location 或 server-tool usage;原生 Anthropic provider 返回的 encrypted_content 是 opaque 数据,OMA-managed search 当前不实现其加密恢复合同。
  • 补充 e2b-local key 文档:本地服务不鉴权,但 E2B SDK 仍要求 e2b_ 加小写十六进制格式。

真实 E2E

使用最新公开 e2b-local 源码、本地重建的 sandbox/rclone 镜像、Claude Code 2.1.120、DeepSeek-V4-Pro 和真实 Tavily 完成两轮端到端验证;未使用 CVTE 内网镜像或 package。

最终代码在 max_server_tool_iterations=1 下的抓包结果:

  • OMA-facing stop reasons:tool_use -> pause_turn -> end_turn
  • BYOK stop reasons:tool_use -> end_turn
  • Tavily:恰好 1 次请求,HTTP 200,5 个结果
  • Claude Code 接受 OMA 返回的 server_tool_use/web_search_tool_result checkpoint,续传后给出带来源的最终答案并回到 idle;OMA 没有伪造 encrypted_content

另有真实 pass-through 场景验证 Bash/WebFetch 由 sandbox 内 Claude Code 执行,Tavily 调用数为 0。

测试与门禁

  • go test ./internal/messages ./internal/config -count=1:104 passed
  • go test ./internal/messages -race -count=1:43 passed
  • go test ./tests -run 'TestMessagesWebSearchGateway' -count=1:2 passed
  • just lintjust dead-codejust duplicatesjust complexityjust large-files:通过
  • pre-commit 除仓库既有的两个测试私钥 fixture 被 detect-private-key 命中外,其余 hook 全部通过;本次 diff 不包含这两个 fixture

References

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds web search configuration, Tavily and Brave provider clients, and a Messages gateway that executes server-side searches during Anthropic tool-use continuations with JSON and SSE response support.

Changes

Web Search Gateway and Providers

Layer / File(s) Summary
Web search configuration
internal/config/..., config/config.example.yaml, docs/configuration-reference.yaml
Adds provider settings, defaults, YAML resolution, wildcard-aware contract validation, and configuration tests.
Provider-neutral search contract
internal/websearch/types.go, internal/websearch/provider.go, internal/websearch/http.go, internal/websearch/*_test.go
Defines shared search models, provider registration, strict option decoding, and bounded HTTP response handling.
Tavily and Brave provider clients
internal/websearch/tavily.go, internal/websearch/brave.go, internal/websearch/*_test.go
Implements authenticated clients with option mapping, result decoding, pagination, freshness handling, and validation tests.
Messages web search gateway
internal/messages/gateway.go, internal/messages/gateway_protocol.go, internal/messages/gateway_test.go
Detects web search tools, runs bounded continuation loops, executes searches, projects transcripts, enforces policies, and emits JSON or SSE responses.
Messages handler gateway integration
internal/messages/handler.go, tests/messages_api_test.go
Constructs and conditionally invokes the gateway for eligible code-session OAuth requests while preserving normal proxy behavior and validating BYOK isolation.
Proxy and provider research documentation
docs/design/be/messages-proxy.md, docs/design/be/web-search-provider-research.md
Documents gateway boundaries, continuation behavior, provider-neutral contracts, and Brave/Exa integration differences.
Local development support
.gitignore
Ignores the local .agents/ directory.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Handler
  participant Gateway
  participant Anthropic
  participant Provider
  Client->>Handler: POST /v1/messages
  Handler->>Gateway: handle candidate request
  Gateway->>Anthropic: send projected web_search request
  Anthropic-->>Gateway: return tool_use
  Gateway->>Provider: Search query and policy
  Provider-->>Gateway: return SearchResponse
  Gateway->>Anthropic: send continuation with tool_result
  Anthropic-->>Gateway: return final response
  Gateway-->>Handler: return JSON or SSE
  Handler-->>Client: response
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The .gitignore change adding .agents/ is unrelated to the Web Search Gateway work. Move the .gitignore tweak to a separate PR or remove it if it is not required for Web Search Gateway.
Docstring Coverage ⚠️ Warning Docstring coverage is 1.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements the WebSearch gateway, providers, config, docs, and tests required for issue #139.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title clearly summarizes the main change: adding Web Search Gateway support for BYOK scenarios.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (7)
internal/messages/gateway.go (1)

350-414: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate result-mapping logic between gatewayToolResult and projectGatewayResponse.

Both functions independently map websearch.Result fields (title/url/content, published_date/page_age) into output maps with slightly different key semantics (Line 364-369 uses separate published_date/page_age keys; Line 401-406 collapses both into page_age). Extracting a single resultToContentItem(result)-style helper would avoid the two mappings drifting further apart as fields are added.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/messages/gateway.go` around lines 350 - 414, Extract the shared
websearch.Result-to-content mapping from gatewayToolResult and
projectGatewayResponse into a helper such as resultToContentItem. Reuse it in
both functions, preserving consistent title, URL, content, published-date, and
page-age key behavior; remove the duplicated field-mapping logic and avoid
collapsing published date into page age.
internal/messages/handler.go (1)

99-100: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant PrincipalFromContext lookup.

principal is re-fetched at Line 99 even though the same value was already retrieved at Line 86 for the auth check. Reuse the earlier variable instead of calling auth.PrincipalFromContext twice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/messages/handler.go` around lines 99 - 100, Reuse the principal
variable already retrieved for the auth check earlier in the handler instead of
calling auth.PrincipalFromContext again before the h.gateway condition. Remove
the redundant lookup while preserving the existing
CredentialTypeCodeSessionOAuth check.
internal/messages/gateway_test.go (1)

35-216: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Test order interleaves failure and success scenarios.

Tests alternate: transparent-fallback (35-49) → loop-limit failure (51-63) → upstream-failure passthrough (65-79) → success transcript projection (81-134) → provider-failure/panic (136-193) → success SSE (195-216). Per path instructions, **/*_test.go should group all failure scenarios before success scenarios. Reordering so TestGatewayToolLoopProjectsTranscript and TestGatewaySSEResponse come after TestGatewayProviderFailureBecomesToolError/TestGatewayProviderPanicBecomesToolError would satisfy the convention.

Coverage itself is solid — the tool-loop transcript, provider-error, and provider-panic assertions correctly exercise the gateway's continuation and error paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/messages/gateway_test.go` around lines 35 - 216, Reorder the tests
in this section so all failure scenarios run before success scenarios: move
TestGatewayToolLoopProjectsTranscript and TestGatewaySSEResponse after
TestGatewayProviderFailureBecomesToolError and
TestGatewayProviderPanicBecomesToolError. Do not change test logic or coverage.

Source: Path instructions

tests/messages_api_test.go (1)

269-342: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Good end-to-end coverage of the happy path; consider adding a mixed-tool regression test.

This test correctly validates BYOK key isolation (Line 291-293), tool projection (Line 296-303), continuation message count (Line 307-310), and the reconstructed server_tool_use/web_search_tool_result content (Line 335-337) — solid coverage of the single-tool continuation flow.

It does not cover the case where the request's tools array includes a non-web_search tool alongside web_search_20250305 and the model calls both in one turn — see the critical continuation bug flagged in internal/messages/gateway.go (Lines 138-153). Once that's fixed, a regression test here (or in gateway_test.go) would help lock in the fix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/messages_api_test.go` around lines 269 - 342, Add a regression test
alongside TestMessagesWebSearchGateway for a request containing
web_search_20250305 plus a non-web-search tool, with the model invoking both in
the same turn. Assert the continuation request preserves the non-web-search tool
call and includes the web-search result, then verify the final gateway response
and expected upstream call count.
internal/websearch/brave.go (1)

92-109: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant recomputation of merged options for pagination.

mergeSearchOptions(c.defaultOptions, request.Options) is computed once at line 67 to build the URL and recomputed again here (line 98) purely to read PageToken. Since it's a pure function the result is identical, but it clones slices twice and creates two places that must stay in sync if merge logic changes.

♻️ Proposed fix: compute once and reuse
 func (c *BraveClient) Search(ctx context.Context, request SearchRequest) (SearchResponse, error) {
 	if c == nil || c.apiKey == "" {
 		return SearchResponse{}, errors.New("web search provider is not configured")
 	}
 	query := strings.TrimSpace(request.Query)
 	if query == "" {
 		return SearchResponse{}, errors.New("web search query is required")
 	}
-	endpoint, err := c.searchURL(query, mergeSearchOptions(c.defaultOptions, request.Options))
+	options := mergeSearchOptions(c.defaultOptions, request.Options)
+	endpoint, err := c.searchURL(query, options)
 	if err != nil {
 		return SearchResponse{}, fmt.Errorf("build brave search endpoint: %w", err)
 	}
@@
 	if decoded.HasMore {
 		offset := 0
-		if options := mergeSearchOptions(c.defaultOptions, request.Options); options.PageToken != "" {
+		if options.PageToken != "" {
 			offset, err = strconv.Atoi(options.PageToken)
 			if err != nil {
 				return SearchResponse{}, fmt.Errorf("parse brave search page token: %w", err)
 			}
 		}
 		if offset < braveMaxOffset {
 			decoded.NextPageToken = strconv.Itoa(offset + 1)
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/websearch/brave.go` around lines 92 - 109, Reuse the merged search
options already computed for URL construction instead of calling
mergeSearchOptions again in the decoded.HasMore pagination block. Keep the
existing PageToken parsing and NextPageToken behavior unchanged, using the
previously computed options value throughout the surrounding search request
flow.
internal/websearch/tavily.go (1)

68-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Shared HTTP response-limiting/status-check logic duplicated between tavily.go and brave.go. Both clients independently implement the same "bounded read → oversize check → 2xx status check" sequence with only naming/message differences; consolidating into one helper in the websearch package removes the duplication and keeps both providers' error handling in sync as they evolve. As per coding guidelines, **/*.go production code has a duplication-rate cap ("Go 生产代码重复率上限为 3.75%") and the check must not be bypassed by raising thresholds or excluding files.

  • internal/websearch/tavily.go#L68-L99: extract the "response, err := c.client.Do(...)" through "status code check" sequence (lines 74-88) into a shared helper (e.g. fetchLimitedBody(client, request, maxSize) ([]byte, error) plus a checkHTTPStatus helper) and call it here.
  • internal/websearch/brave.go#L77-L91: replace the equivalent duplicated block with a call to the same shared helper(s).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/websearch/tavily.go` around lines 68 - 99, Consolidate the
duplicated bounded-read and HTTP status handling into shared helpers in the
websearch package, such as fetchLimitedBody and checkHTTPStatus. In
internal/websearch/tavily.go lines 68-99, replace the response execution through
status-check sequence with the shared helper while preserving request
construction and response decoding. Apply the same replacement in
internal/websearch/brave.go lines 77-91, keeping provider-specific error context
through the shared helper’s returned errors.

Source: Coding guidelines

internal/websearch/brave_test.go (1)

15-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the success-path test after failure coverage.

Declare TestBraveClientSearch after TestBraveClientFailures so test organization matches the required failure-before-success order. As per coding guidelines, "**/*_test.go: 测试组织顺序应先覆盖失败场景,再覆盖成功场景。"

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/websearch/brave_test.go` around lines 15 - 58, Move the
TestBraveClientSearch function to appear after TestBraveClientFailures in the
test file, preserving its implementation unchanged and placing failure coverage
before success coverage.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/design/be/messages-proxy.md`:
- Line 26: Update the Messages proxy documentation to explicitly list the
supported Anthropic web search server-tool versions: web_search_20250305,
web_search_20260209, and web_search_20260318. Keep the documented behavior
aligned with the version checks in the gateway implementation rather than
implying that every web_search_* version is supported.

In `@internal/messages/gateway.go`:
- Around line 178-193: Replace the untyped map[string]any/[]any business-field
handling in extractGatewayToolCalls, assistantGatewayMessage, gatewayToolResult,
projectGatewayResponse, hasWebSearchTool, and projectGatewayFields with small
named structs for the fields read or written, including tool type/name/id,
input.query, and content. Decode boundary JSON into these DTOs before routing or
reconstructing messages, and retain json.RawMessage only for unknown or
passthrough payloads. Remove unchecked type assertions while preserving the
existing behavior for valid and malformed gateway data.
- Around line 195-240: Preserve caller-supplied web_search options through
projectGatewayFields and the g.search request path instead of replacing every
matching tool with the fixed searchToolDefinition or forcing
SearchOptions{MaxResults: 5}. Thread supported limits and filters, including
max_uses and domain restrictions, into the generated tool/request options; if an
option cannot be propagated, explicitly reject it rather than silently dropping
it.
- Around line 138-153: Update extractGatewayToolCalls and the assistant-turn
replay flow so every tool_use block receives a corresponding tool_result,
including non-web_search tools in mixed turns. Preserve web_search execution
through g.search, and create the established unsupported-tool/error result for
other tool uses before appending the user message, preventing partial
tool-result transcripts.

In `@internal/messages/handler.go`:
- Around line 67-70: Update NewHandler so the gateway field is nil when
websearch.NewProvider(cfg.WebSearch, client) returns nil, rather than always
storing the non-nil result of newGateway. Preserve the existing gateway
construction for configured providers, ensuring the h.gateway != nil check
prevents readGatewayCandidate from buffering requests when web search is
unavailable.

---

Nitpick comments:
In `@internal/messages/gateway_test.go`:
- Around line 35-216: Reorder the tests in this section so all failure scenarios
run before success scenarios: move TestGatewayToolLoopProjectsTranscript and
TestGatewaySSEResponse after TestGatewayProviderFailureBecomesToolError and
TestGatewayProviderPanicBecomesToolError. Do not change test logic or coverage.

In `@internal/messages/gateway.go`:
- Around line 350-414: Extract the shared websearch.Result-to-content mapping
from gatewayToolResult and projectGatewayResponse into a helper such as
resultToContentItem. Reuse it in both functions, preserving consistent title,
URL, content, published-date, and page-age key behavior; remove the duplicated
field-mapping logic and avoid collapsing published date into page age.

In `@internal/messages/handler.go`:
- Around line 99-100: Reuse the principal variable already retrieved for the
auth check earlier in the handler instead of calling auth.PrincipalFromContext
again before the h.gateway condition. Remove the redundant lookup while
preserving the existing CredentialTypeCodeSessionOAuth check.

In `@internal/websearch/brave_test.go`:
- Around line 15-58: Move the TestBraveClientSearch function to appear after
TestBraveClientFailures in the test file, preserving its implementation
unchanged and placing failure coverage before success coverage.

In `@internal/websearch/brave.go`:
- Around line 92-109: Reuse the merged search options already computed for URL
construction instead of calling mergeSearchOptions again in the decoded.HasMore
pagination block. Keep the existing PageToken parsing and NextPageToken behavior
unchanged, using the previously computed options value throughout the
surrounding search request flow.

In `@internal/websearch/tavily.go`:
- Around line 68-99: Consolidate the duplicated bounded-read and HTTP status
handling into shared helpers in the websearch package, such as fetchLimitedBody
and checkHTTPStatus. In internal/websearch/tavily.go lines 68-99, replace the
response execution through status-check sequence with the shared helper while
preserving request construction and response decoding. Apply the same
replacement in internal/websearch/brave.go lines 77-91, keeping
provider-specific error context through the shared helper’s returned errors.

In `@tests/messages_api_test.go`:
- Around line 269-342: Add a regression test alongside
TestMessagesWebSearchGateway for a request containing web_search_20250305 plus a
non-web-search tool, with the model invoking both in the same turn. Assert the
continuation request preserves the non-web-search tool call and includes the
web-search result, then verify the final gateway response and expected upstream
call count.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 659fbb3d-1741-4940-b915-ae8be886039b

📥 Commits

Reviewing files that changed from the base of the PR and between 57a6e4c and f527903.

📒 Files selected for processing (20)
  • .gitignore
  • config/config.example.yaml
  • docs/configuration-reference.yaml
  • docs/design/be/messages-proxy.md
  • docs/design/be/web-search-provider-research.md
  • internal/config/config_test.go
  • internal/config/defaults.go
  • internal/config/reference_test.go
  • internal/config/types.go
  • internal/config/yaml_types.go
  • internal/messages/gateway.go
  • internal/messages/gateway_test.go
  • internal/messages/handler.go
  • internal/websearch/brave.go
  • internal/websearch/brave_test.go
  • internal/websearch/provider.go
  • internal/websearch/tavily.go
  • internal/websearch/tavily_test.go
  • internal/websearch/types.go
  • tests/messages_api_test.go

Comment thread docs/design/be/messages-proxy.md Outdated
Comment thread internal/messages/gateway.go Outdated
Comment thread internal/messages/web_search_gateway.go Outdated
Comment thread internal/messages/gateway.go Outdated
Comment thread internal/messages/handler.go Outdated
@Postroggy
Postroggy force-pushed the feat/web-search-main branch from f527903 to e08a5ca Compare July 22, 2026 07:36
@Postroggy

Copy link
Copy Markdown
Contributor Author

已处理 CodeRabbit review:

  • 未配置 Web Search provider 时不再创建 gateway,避免无意义地缓冲 code-session 请求。
  • 混合 tool-use 回合为非 web search tool 补充 is_error tool result,避免 continuation transcript 不完整。
  • 使用命名 DTO 解析 gateway 消息,保留 json.RawMessage 处理未知字段。
  • 透传 max_usesallowed_domainsblocked_domainsuser_location 当前显式返回不支持的 tool error。
  • 抽取 Tavily/Brave 的有界 HTTP 响应处理和统一结果映射。
  • 更新 server tool 版本文档、补充混合工具回归测试并调整测试顺序。

验证:全量 Go 测试、race 测试、定向 golangci-lint、dead-code、duplicates、complexity、large-files 均通过。仓库本地 just lint 也已确认本次修改涉及 package 无问题;此前本地失败来自 web/node_modules/flatted 第三方快照,CI Lint 已通过。

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/design/be/web-search-provider-research.md`:
- Around line 3-7: Remove the blank line inside the introductory blockquote in
the research document, keeping the research scope, date, and implementation
status as one continuous blockquote without empty `>` lines.
- Around line 207-213: Update the date-mapping row in the comparison table to
distinguish Brave’s page_age from publication timestamps: map page_age to
PageAge, and state that PublishedDate is populated only from an actual
publication timestamp. Remove wording that implies page_age can partially
populate PublishedDate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a07cf381-2a13-4281-b8fd-e8501e4b7573

📥 Commits

Reviewing files that changed from the base of the PR and between f527903 and e08a5ca.

📒 Files selected for processing (21)
  • .gitignore
  • config/config.example.yaml
  • docs/configuration-reference.yaml
  • docs/design/be/messages-proxy.md
  • docs/design/be/web-search-provider-research.md
  • internal/config/config_test.go
  • internal/config/defaults.go
  • internal/config/reference_test.go
  • internal/config/types.go
  • internal/config/yaml_types.go
  • internal/messages/gateway.go
  • internal/messages/gateway_test.go
  • internal/messages/handler.go
  • internal/websearch/brave.go
  • internal/websearch/brave_test.go
  • internal/websearch/http.go
  • internal/websearch/provider.go
  • internal/websearch/tavily.go
  • internal/websearch/tavily_test.go
  • internal/websearch/types.go
  • tests/messages_api_test.go
🚧 Files skipped from review as they are similar to previous changes (19)
  • internal/config/defaults.go
  • internal/websearch/provider.go
  • internal/config/yaml_types.go
  • .gitignore
  • config/config.example.yaml
  • docs/configuration-reference.yaml
  • internal/websearch/tavily_test.go
  • internal/config/reference_test.go
  • internal/websearch/types.go
  • internal/config/types.go
  • internal/websearch/tavily.go
  • tests/messages_api_test.go
  • internal/websearch/brave.go
  • internal/messages/handler.go
  • internal/config/config_test.go
  • docs/design/be/messages-proxy.md
  • internal/messages/gateway.go
  • internal/websearch/brave_test.go
  • internal/messages/gateway_test.go

Comment thread docs/design/be/web-search-provider-research.md Outdated
Comment thread docs/design/be/web-search-provider-research.md
@Postroggy
Postroggy force-pushed the feat/web-search-main branch from e08a5ca to ed313e1 Compare July 22, 2026 07:52
@Postroggy

Copy link
Copy Markdown
Contributor Author

补充处理了新一轮 review:

  • 修复研究文档 blockquote 的 MD028 格式问题。
  • 明确 Brave page_age 映射到 PageAgePublishedDate 仅承载实际发布日期。

当前 PR 仍保持单一 commit:ed313e1

@Postroggy
Postroggy force-pushed the feat/web-search-main branch from ed313e1 to bbb6a1d Compare July 26, 2026 14:24

@duckpr duckpr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DuckPR reviewer: opencode
Model: anthropic/glm-5.2

✅ 增量复审无新问题。bbb6a1d 把调用方声明的搜索策略正确收敛到服务端强制路径。

Reviewed changes — 本次复审聚焦 bbb6a1d("fix(messages): 保留 Web Search 请求策略"),即上一次评审后新增的唯一提交,核心是把 max_uses/allowed_domains/blocked_domains/user_location 从 model-controlled tool input 提升为调用方策略,由 OMA 在调用 BYOK 前解析并强制。

  • 将搜索策略从模型输入剥离到服务端 gatewaySearchPolicy — 新增 gatewaySearchToolsearchPolicy(),在 projectGatewayFields 阶段一次性解析 max_usesallowed_domainsblocked_domainsuser_location;BYOK 只看到仅含 querysearchToolDefinition(),避免模型改写或绕过调用方约束。
  • max_uses 改由 executeToolCalls 统计并强制 — 超限的后续搜索转为 web_search_tool_result_error,而不是依赖模型自控;searchUses 跨轮累计,逻辑集中。
  • user_location 显式拒绝而非静默丢弃 — provider-neutral 合同无法表达该字段时,searchPolicy() 返回明确错误,且在发往 BYOK 前即终止。
  • 拒绝重复 web search tool 与 max_uses 非正值projectGatewayFields 增加重复检测,searchPolicy() 校验 max_uses <= 0allowed_domains/blocked_domains 互斥。
  • 同步更新设计文档与测试messages-proxy.md 补齐策略强制与 user_location 报错说明;新增 TestGatewayEnforcesCallerMaxUsesTestGatewayRejectsUnsupportedCallerLocation 覆盖两条新路径,既有 transcript 测试补充“策略字段未泄漏进模型输入”断言。

本次只复审了增量提交,未重复评审 CodeRabbit 已覆盖的初始实现。go test ./internal/messages/...go vet 均通过。

Pullfrog  | View workflow run | Using anthropic/glm-5.2𝕏

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
internal/messages/gateway_test.go (4)

140-177: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Mixed-tool test doesn't assert is_error on the non-search tool result.

Per the PR objectives, this test targets the fix that supplies an is_error result for non-Web-Search tools in hybrid tool calls. The assertion only checks that tool_use_id values are present (lines 162-164); it never checks that the bash tool's synthesized result actually carries "is_error":true, so a regression that returns a plain (non-error) result for unsupported tools would still pass this test.

✅ Proposed strengthening
 		if !strings.Contains(string(last.Content[0]), `"tool_use_id":"toolu_search"`) ||
 			!strings.Contains(string(last.Content[1]), `"tool_use_id":"toolu_other"`) {
 			t.Fatalf("continuation results = %s", last.Content)
 		}
+		if !strings.Contains(string(last.Content[1]), `"is_error":true`) {
+			t.Fatalf("expected is_error result for unsupported tool, got %s", last.Content[1])
+		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/messages/gateway_test.go` around lines 140 - 177, Strengthen
TestGatewayMixedToolUseReturnsUnsupportedToolResult by inspecting the
continuation content for the non-search tool result and asserting that the entry
with tool_use_id "toolu_other" includes "is_error":true. Keep the existing
assertions for both tool_use_id values and the request flow unchanged.

286-307: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

SSE test never exercises the tool-search continuation loop.

TestGatewaySSEResponse sets "stream":true but the mock upstream immediately returns end_turn with no tool_use, so the gateway's search-and-continuation path is never invoked over SSE — only the trivial pass-through-to-SSE-encoding path is covered. Given SSE/JSON compatibility for the continuation loop is a stated goal of this PR, consider adding a variant modeled on TestGatewayToolLoopProjectsTranscript but with "stream":true, to confirm the reconstructed server_tool_use/web_search_tool_result content is correctly SSE-encoded after an actual provider search round-trip.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/messages/gateway_test.go` around lines 286 - 307, Extend
TestGatewaySSEResponse to exercise the tool-search continuation loop, using the
multi-round upstream behavior and assertions from
TestGatewayToolLoopProjectsTranscript while keeping stream enabled. Verify the
resulting SSE response includes correctly encoded server_tool_use and
web_search_tool_result content after the provider search round-trip, in addition
to the existing SSE headers and lifecycle events.

51-307: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Repeated upstream-mock boilerplate across tests.

Nearly every test hand-rolls an httptest.NewServer(http.HandlerFunc(...)) with a request-count-based sequenced JSON response, followed by the same cfg/newGateway wiring. A small helper (e.g. newSequencedUpstream(t, responses ...string) *httptest.Server) would remove this repetition across ~9 functions and make each test body focus on its distinguishing assertions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/messages/gateway_test.go` around lines 51 - 307, Add a shared test
helper such as newSequencedUpstream(t, responses ...string) that creates and
registers an httptest server, returns each JSON response in sequence, and fails
on unexpected requests or exhausted responses. Refactor the repeated upstream
setup and gateway wiring in the affected TestGateway* cases to use this helper
while preserving tests that require custom request inspection or non-200
responses.

51-63: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Loop-limit boundary not verified by request count.

MaxToolLoops: 1 should bound the exact number of upstream round-trips before the gateway errors out. The test only asserts the final outcome (handled, err, nil body) but never asserts how many times the upstream handler was actually invoked, so an off-by-one in the loop-bound check (e.g., allowing 2 iterations instead of 1) would not be caught.

✅ Proposed addition
+	requestCount := 0
 	upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+		requestCount++
 		w.Header().Set("Content-Type", "application/json")
 		_, _ = io.WriteString(w, "{\"type\":\"message\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_loop\",\"name\":\"web_search\",\"input\":{\"query\":\"query\"}}],\"stop_reason\":\"tool_use\"}")
 	}))
@@
 	if !handled || err == nil || response.body != nil {
 		t.Fatalf("response = %#v, handled = %v, err = %v; want bounded loop error", response, handled, err)
 	}
+	if requestCount != 1 {
+		t.Fatalf("upstream requests = %d, want exactly 1 for MaxToolLoops=1", requestCount)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/messages/gateway_test.go` around lines 51 - 63, Update
TestGatewayToolLoopLimit to count invocations of the upstream httptest handler
and assert that the request count is exactly one when MaxToolLoops is 1. Keep
the existing bounded-loop outcome assertions, ensuring the test catches any
off-by-one iteration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/design/be/web-search-provider-research.md`:
- Around line 14-15: Update the SearchOptions coverage statement in the document
to match the provider-neutral contract defined by internal/websearch/types.go:
either include SafeSearch, Spellcheck, ResultFilter, and Goggles only if they
are exposed there, or revise the text to identify them as Brave-specific
capabilities handled by the adapter.

In `@internal/messages/gateway_test.go`:
- Around line 196-218: Set WebSearchConfig.APIKey to "tavily-key" in the cfg
initialization for this test, while preserving the existing provider
configuration. Keep the strings.Contains assertion against the first BYOK
request so it verifies the configured Tavily key is not forwarded.

---

Nitpick comments:
In `@internal/messages/gateway_test.go`:
- Around line 140-177: Strengthen
TestGatewayMixedToolUseReturnsUnsupportedToolResult by inspecting the
continuation content for the non-search tool result and asserting that the entry
with tool_use_id "toolu_other" includes "is_error":true. Keep the existing
assertions for both tool_use_id values and the request flow unchanged.
- Around line 286-307: Extend TestGatewaySSEResponse to exercise the tool-search
continuation loop, using the multi-round upstream behavior and assertions from
TestGatewayToolLoopProjectsTranscript while keeping stream enabled. Verify the
resulting SSE response includes correctly encoded server_tool_use and
web_search_tool_result content after the provider search round-trip, in addition
to the existing SSE headers and lifecycle events.
- Around line 51-307: Add a shared test helper such as newSequencedUpstream(t,
responses ...string) that creates and registers an httptest server, returns each
JSON response in sequence, and fails on unexpected requests or exhausted
responses. Refactor the repeated upstream setup and gateway wiring in the
affected TestGateway* cases to use this helper while preserving tests that
require custom request inspection or non-200 responses.
- Around line 51-63: Update TestGatewayToolLoopLimit to count invocations of the
upstream httptest handler and assert that the request count is exactly one when
MaxToolLoops is 1. Keep the existing bounded-loop outcome assertions, ensuring
the test catches any off-by-one iteration.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ca56fccb-4abd-412f-a8bf-9847389c3657

📥 Commits

Reviewing files that changed from the base of the PR and between e08a5ca and 968147a.

📒 Files selected for processing (21)
  • .gitignore
  • config/config.example.yaml
  • docs/configuration-reference.yaml
  • docs/design/be/messages-proxy.md
  • docs/design/be/web-search-provider-research.md
  • internal/config/config_test.go
  • internal/config/defaults.go
  • internal/config/reference_test.go
  • internal/config/types.go
  • internal/config/yaml_types.go
  • internal/messages/gateway.go
  • internal/messages/gateway_test.go
  • internal/messages/handler.go
  • internal/websearch/brave.go
  • internal/websearch/brave_test.go
  • internal/websearch/http.go
  • internal/websearch/provider.go
  • internal/websearch/tavily.go
  • internal/websearch/tavily_test.go
  • internal/websearch/types.go
  • tests/messages_api_test.go
🚧 Files skipped from review as they are similar to previous changes (19)
  • internal/config/defaults.go
  • internal/config/reference_test.go
  • internal/websearch/http.go
  • internal/config/types.go
  • docs/configuration-reference.yaml
  • config/config.example.yaml
  • .gitignore
  • internal/websearch/provider.go
  • internal/config/yaml_types.go
  • internal/websearch/tavily_test.go
  • internal/config/config_test.go
  • docs/design/be/messages-proxy.md
  • internal/websearch/brave.go
  • internal/websearch/brave_test.go
  • internal/websearch/types.go
  • tests/messages_api_test.go
  • internal/messages/handler.go
  • internal/websearch/tavily.go
  • internal/messages/gateway.go

Comment thread docs/design/be/web-search-provider-research.md Outdated
Comment thread internal/messages/gateway_test.go Outdated
@superduck-ai superduck-ai deleted a comment from jh0904 Jul 27, 2026
Comment thread config/config.example.yaml
Comment thread internal/config/types.go
@Postroggy

Copy link
Copy Markdown
Contributor Author

这轮 review body 里的 4 个测试 nit 也一起处理了:

  • mixed tool continuation 现在断言非搜索 tool result 带 "is_error":true
  • SSE 测试改成两轮 upstream 响应,实际走 provider search continuation,并检查 server_tool_useweb_search_tool_resultweb_search_result
  • 抽了 newSequencedUpstream helper,保留需要检查请求体和非 2xx 的 custom server;
  • MaxToolLoops: 1 现在断言 upstream 恰好只收到 1 次请求。

本地的 config/websearch/messages 窄测试、race test、lint、dead-code、duplicates、complexity 都已跑过。

@Postroggy

Postroggy commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

补充一次协议语义核对(重新对照 Anthropic 官方文档,并用真实 Claude Code + DeepSeek-V4-Pro + Tavily 跑过请求链路)。

1. max_usesmax_tool_loops 不是同一个限制

  • Anthropic 的 max_uses=3 限制的是每条 Messages 请求中实际执行的 Web Search 次数,不是 OMA 和 BYOK 之间最多往返 3 次,也不是最多 3 个 tool definition。达到预算后不再调用搜索 provider,而应返回 web_search_tool_result_error,错误码为 max_uses_exceeded
  • max_tool_loops 是 OMA 自己的安全上限,用来限制 OMA -> BYOK 的内部请求次数。当前实现每进入一次 loop 就发起一次 BYOK 请求(初始请求也计入),默认值为 3;一个 BYOK 响应里可以有多个 tool_use,因此它不能代替 max_uses
  • OMA 的内部 BYOK continuation 仍属于同一条入站 Messages 请求,因此 max_uses 不能在这些内部请求之间重置。若 mixed turn 已经返回 Claude Code、随后由 Claude Code 发起新的 Messages 请求,官方合同写的是 per request,应在新请求上重新应用它携带的 max_uses;但未完成的 server tool、ID 映射和 transcript 顺序仍必须恢复。

官方参考:Web search tool

2. Web Search 与普通 client tool 的 ownership

当 BYOK 返回普通 client-tool 形式的调用时,OMA 只能执行自己拥有的 web_search。Bash、Edit 或 MCP tool 仍属于 Claude Code,不能由 OMA 伪造 unsupported tool result 来闭合。

当前 executeToolCalls 对非搜索调用生成 is_error=true,而测试 TestGatewayMixedToolUseReturnsUnsupportedToolResult 也把这个行为固化了。这会让 Claude Code 根本看不到自己的 tool call,行为是不正确的,后续需要改掉。

3. 同一 assistant turn 混合调用时的消息组织

Anthropic 的 mixed server/client 规则是:

  1. BYOK 返回 web_search 和 Bash 等多个 tool_use
  2. OMA 对外返回未完成的 server_tool_use(web_search) 和普通 tool_use(Bash),不提前伪造搜索结果。
  3. Claude Code 执行 Bash,并在下一次请求中发送对应的 tool_result
  4. OMA 在这个续传请求中把历史 server tool 映射回 BYOK 看到的普通 tool_use,执行挂起的搜索,并把搜索结果和 Claude Code 的 client results 放进同一个 user message;所有 tool_result 必须按 tool_use_id 一一对应,并位于文本之前。
  5. 下一次响应再返回匹配的 web_search_tool_result,然后继续正常生成。

参考:Server toolsParallel tool useHandle tool calls

4. 关于“多个 Web Search tool call”

外层请求只需要声明一个逻辑上的 web_search server tool,不需要声明多个同名 tool。官方文档同时明确说明一次请求可能实际进行多次搜索;因此 OMA 不能把“多个定义”和“多个调用”混为一谈:

  • 多个定义属于请求校验问题;
  • 多个 tool_use 属于响应处理问题,必须按 ID 逐个闭合,遵守 max_uses,并保持结果顺序。

这条澄清也意味着此前“混合 tool 的非搜索结果带 is_error”的处理结论需要撤回。当前实现可以继续保留纯 Web Search 的内部 continuation,但 mixed tool、跨请求 transcript 恢复和对应测试应作为 correctness follow-up,而不是把普通 Claude Code tools 当成 OMA 不支持的调用。

@Postroggy

Copy link
Copy Markdown
Contributor Author

再补两处实现级边界,避免把两个“次数”混成一个概念:

  • 当前 projectGatewayResponse 把所有搜索失败都投影成 error_code: "unavailable"。达到调用方声明的 max_uses 时,Anthropic 合同要求的是 max_uses_exceeded;这个错误码需要从内部执行结果保留下来,不能统一吞掉。
  • 当前 max_tool_loops 的循环条件是在最后一个允许的 BYOK 请求里仍先执行 tool call,随后因为没有下一轮而直接返回 loop-exceeded。这可能让 OMA 付出一次搜索成本,却没有把结果交给模型。实现前需要明确它是“最多上游请求数”还是“最多 continuation 次数”,并保证没有 continuation 预算时不执行无法回传的调用。

另外,合成的 server tool block 也应使用 Anthropic 约定的 srvtoolu_* ID,而不是普通 client tool 的 toolu_* 前缀。上述几点和 mixed server/client 的 transcript 状态应一起补测试。

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
internal/messages/gateway_test.go (2)

165-183: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Non-atomic request counters mutated in httptest handler goroutines. Both files count upstream/provider requests with a plain int incremented inside the handler goroutine and read from the test goroutine, which is a data race under -race; newSequencedUpstream already provides an atomic counter for this purpose.

  • internal/messages/gateway_test.go#L165-L183: replace the requestCount int closure with an atomic counter (or newSequencedUpstream), and apply the same change to the requestCount in TestGatewayMixedToolUseDefersSearchUntilClientResults at Lines 218-220.
  • tests/messages_api_test.go#L343-L498: make searchCalls and upstreamCalls atomic.Int64 (or guard with a mutex) and read them via Load() at the assertion sites.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/messages/gateway_test.go` around lines 165 - 183, Replace the plain
request counters in internal/messages/gateway_test.go lines 165-183, including
the requestCount in TestGatewayMixedToolUseDefersSearchUntilClientResults at
lines 218-220, with atomic counters or newSequencedUpstream and use synchronized
reads in assertions. In tests/messages_api_test.go lines 343-498, make
searchCalls and upstreamCalls atomic.Int64 (or protect them with a mutex) and
use Load() when asserting their values.

345-419: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer t.Errorf + return over t.Fatalf inside the httptest handler goroutine.

t.Fatalf is only valid on the goroutine running the test function; from the handler it calls runtime.Goexit on the server goroutine, so the request is aborted without the test necessarily failing at that point. Same applies to TestGatewayProjectsCompletedMixedHistoryBackToBYOK (Lines 395-418).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/messages/gateway_test.go` around lines 345 - 419, Replace each
t.Fatalf call inside the httptest handlers of
TestGatewayProjectsCompletedSearchHistoryBackToBYOK and
TestGatewayProjectsCompletedMixedHistoryBackToBYOK with t.Errorf followed by
return, including decode, length, role/content, and unmarshal validation
failures. Leave assertions outside the handler goroutines unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/messages/gateway_protocol.go`:
- Around line 558-570: Update the projection flow around
projectClientSearchCallToServer so an empty execution.call.externalID falls back
to serverGatewayToolUseID(execution.call.id), matching
gatewayWebSearchResultBlock. Use the same resolved ID for both the emitted
server_tool_use and web_search_tool_result pair, while preserving existing
behavior when externalID is present.
- Around line 204-215: Update the mixed tool continuation validation around
gatewayProtocolBlock so non-tool_result content blocks are ignored rather than
rejected, allowing text and other supported blocks to remain in the user message
for transcript projection. Preserve the existing decode error handling and
tool_result-specific validation/checks for tool calls.

In `@tests/messages_api_test.go`:
- Around line 491-497: Replace the vacuous secondPayload secret check in the
mixed-response assertion with validation inside the upstream handler: inspect
the received BYOK request body, decoding from the captured raw body if
necessary, and assert it does not contain "tavily-mixed-key". Remove the
strings.Contains check on secondPayload while preserving the existing response
and call-count assertions.

---

Nitpick comments:
In `@internal/messages/gateway_test.go`:
- Around line 165-183: Replace the plain request counters in
internal/messages/gateway_test.go lines 165-183, including the requestCount in
TestGatewayMixedToolUseDefersSearchUntilClientResults at lines 218-220, with
atomic counters or newSequencedUpstream and use synchronized reads in
assertions. In tests/messages_api_test.go lines 343-498, make searchCalls and
upstreamCalls atomic.Int64 (or protect them with a mutex) and use Load() when
asserting their values.
- Around line 345-419: Replace each t.Fatalf call inside the httptest handlers
of TestGatewayProjectsCompletedSearchHistoryBackToBYOK and
TestGatewayProjectsCompletedMixedHistoryBackToBYOK with t.Errorf followed by
return, including decode, length, role/content, and unmarshal validation
failures. Leave assertions outside the handler goroutines unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 247e50af-79ec-4268-982d-d45457edc58a

📥 Commits

Reviewing files that changed from the base of the PR and between fe89f53 and a43d40b.

📒 Files selected for processing (6)
  • docs/design/be/messages-proxy.md
  • internal/messages/gateway.go
  • internal/messages/gateway_protocol.go
  • internal/messages/gateway_test.go
  • internal/messages/handler.go
  • tests/messages_api_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/design/be/messages-proxy.md
  • internal/messages/handler.go
  • internal/messages/gateway.go

Comment thread internal/messages/web_search_protocol.go
Comment thread internal/messages/web_search_protocol.go
Comment thread tests/messages_api_test.go
@Postroggy

Copy link
Copy Markdown
Contributor Author

@arthur-zhang 这轮已经把你提出的配置和扩展性问题连同后续协议边界一起收尾,当前 head 是 970a31e

  • provider 配置保持在 web_search.providers.<name>,实例化通过 registry/factory,不再把某一家 provider 的字段扩散到公共配置。
  • OMA 只按 type=web_search_20250305|web_search_20260209|web_search_20260318 拦截 Claude Code 请求中的 server tool 定义,并把它投影成下游模型可决定是否调用的普通 client tool;没有调用就不会搜索。其他 client tool 仍交给 Claude Code 执行。
  • mixed tool turn 会先把普通 tool_use 返回 Claude Code,收齐对应 tool_result 后才执行 pending search,并按原顺序合并结果;缺失、重复和未知 result 都会拒绝。
  • pause_turn 作为 HTTP 200 checkpoint 原样续传;max_server_tool_iterations 默认 10,和请求级 tools[].max_uses 分开计数。
  • Web Search result 已改成官方字段形状,并携带 OMA 自己的 opaque encrypted_content,用于跨 HTTP continuation 恢复 provider 内容;文档明确说明它不冒充 Anthropic 原生密文或原生 citation/usage。

我用最新 public e2b-locald8543bbc)和本地重新构建的 sandbox image 跑了两次真实 Claude Code 2.1.120 E2E,模型是 DeepSeek-V4-Pro,搜索走真实 Tavily。最终链路是 Claude Code -> OMA -> BYOK LLM tool_use -> Tavily -> continuation -> Claude Code,最终回答正确;抓包确认 Tavily 只调用 1 次,OMA 对外依次出现 tool_usepause_turnend_turn

本地 targeted、race、handler integration、lint、dead-code、duplicate、complexity、large-files 均通过;当前 GitHub 7 项检查也全部通过。麻烦基于 970a31e 再看一轮。

@Postroggy

Copy link
Copy Markdown
Contributor Author

关于 encrypted_content,我把自定义实现删掉了,并重新跑了真实 Claude Code E2E。

之前的 oma_search_v1_ + Base64(JSON) 不是 Anthropic 生成的 opaque payload,也没有加密或完整性保护。继续放在官方字段名下会让调用方误以为 OMA 实现了 Anthropic 的加密恢复合同。现在 OMA-managed search 只返回 type/title/url/page_age,不返回 provider 明文 content,也不合成 encrypted_content;DTO 仍保留该字段用于解析第三方响应,但 OMA 不解码、不校验,也不投影给 BYOK。

真实链路使用本地重编的最新公开 e2b-local、sandbox/rclone、Claude Code 2.1.120、DeepSeek-V4-Pro 和真实 Tavily:

  1. 主模型请求先声明普通内置 WebSearch client tool。
  2. 主模型选择它后,Claude Code 的内置执行器另发一条只声明 type=web_search_20250305,name=web_search 的 Messages 请求。
  3. OMA 将这条 server tool 投影为 BYOK 普通 web_search tool,Tavily 实际调用 1 次并返回 5 条结果。
  4. OMA 返回 HTTP 200 / end_turn,包含 1 个 server_tool_use、1 个 web_search_tool_result、5 个 web_search_result,整个响应没有 encrypted_content
  5. Claude Code 正常把结果转成主循环的 ordinary tool_result,生成最终回答;同一 session 的下一轮也正常结束,Tavily 总调用次数仍为 1。

边界也说明一下:Anthropic 公共 schema 仍把原生 encrypted_content 定义为必填,并要求后续 turn 原样回放。因此这个 E2E 证明的是 Claude Code 2.1.120 能接受 OMA-managed search 的省略字段结果,不代表 OMA 完整实现了 Anthropic 原生 encrypted replay。第二轮回放的是 Claude Code 主循环里的 ordinary WebSearch tool_result;direct Messages 客户端的 completed server-history projection 由单元测试覆盖,本次没有把它包装成 E2E 结论。

验证结果:internal/messages 40/40、race 40/40、dead-code 0 issues,受管 pre-commit hook 通过。完整 go test ./... 另有共享测试库 migration、测试配置路径和既有 workbench model 预期导致的环境性失败,与本次 messages 改动无关。

References:

Remove defer/recover in search() method - panic recovery belongs at the
HTTP boundary layer (recoverMiddleware), not in deep business methods.
Also drop unused imports (runtime/debug, httpapi).
@Postroggy
Postroggy force-pushed the feat/web-search-main branch from b5efbe2 to ebb814e Compare July 29, 2026 11:56
…ability

Three defects surfaced by reviewing this branch against the project Go
skills and the "100 Go Mistakes" catalog.

Forwarding the caller's Accept-Encoding to BYOK disabled the Transport's
transparent decompression, so the gateway received raw gzip bytes and
failed to parse every upstream response. The real Anthropic API does
compress when asked, and Node/undici clients always ask, so this broke
the whole gateway path in production while httptest servers hid it.
Delete the header in send() and let the Transport negotiate.

Gateway failures wrote a 502 without logging anything, leaving the
decode failure above completely silent server-side. Log ERROR before the
502 and WARN before the 400, matching the surrounding handlers. Also log
one WARN with request_id and error_code when a provider failure degrades
into web_search_tool_result_error, which gives the previously unused
gateway logger a real purpose.

finalizeWebSearchResponse built the client response from the last BYOK
body, so a multi-iteration search silently under-reported token usage
(a two-iteration run charging 2200/130 reported 1200/80). Accumulate
integer token counters by field name across samples and keep the last
sample's non-numeric fields, so future usage counters aggregate without
a field allowlist. Single-sample requests pass the upstream usage
through unchanged.

Replace the test that asserted the recover() removed in 07e751a with one
asserting the opposite invariant: a provider panic must reach the HTTP
boundary's recoverMiddleware rather than being swallowed. Update the
design doc for the usage, encoding, and logging contracts, and fix its
stale reference to the pre-0889133 gateway_test.go filename.
# Conflicts:
#	tests/messages_api_test.go

@duckpr duckpr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DuckPR reviewer: opencode
Model: anthropic/glm-5.2

✅ 增量复审无新问题。515e36b 把 provider 能力校验和 replayed history 完整性检查前移到 BYOK 请求之前,逻辑正确且测试覆盖充分。

Reviewed changes — 本次复审聚焦 515e36b("fix(messages): validate web search replay contracts"),即上一次 Pullfrog 评审(bbb6a1d)后新增的唯一提交,核心是在发往 BYOK 前校验 provider 选项能力,并在续传历史中拒绝重复 server_tool_use ID。

  • 新增 Provider.ValidateOptions 契约并前移校验websearch.Provider 接口新增 ValidateOptions(SearchOptions) errorBraveClient 拒绝域名过滤、TavilyClient 全部允许。gateway 在 prepareRequest 阶段(首次 BYOK 请求之前)调用,避免发起注定失败的搜索回合。
  • 抽取 webSearchOptions helper 统一选项构造search()prepareRequest 的校验路径复用同一份 webSearchOptions(policy),保证校验和执行看到一致的 SearchOptions
  • 拒绝 replayed history 中的重复 server_tool_use IDfindPendingWebSearchTurnfindPausedWebSearchTurn 均增加 serverToolUses 去重,重复 ID 在转录投影前即报错,防止 tool_use_id 匹配歧义。
  • 补充设计与测试 — mermaid 图修正 oma_web_search 投影名;新增 TestWebSearchGatewayRejectsUnsupportedProviderOptionsBeforeUpstream(断言零 BYOK 请求、零搜索)与 TestWebSearchGatewayRejectsDuplicateServerToolUsesInReplayedHistory(mixed + paused 两条路径)。

go build / go test ./internal/messages/... ./internal/websearch/... 均通过。本次只复审了增量提交,未重复评审 CodeRabbit 已覆盖的初始实现。

Pullfrog  | View workflow run | Using anthropic/glm-5.2𝕏

@duckpr duckpr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DuckPR reviewer: opencode
Model: anthropic/glm-5.2

✅ 增量复审无新问题。b10f8f3 把 provider factory 从运行时 init() 注册改为静态编译期 map,重复名称会在编译期失败,安全性更高。

Reviewed changes — 本次复审聚焦上次 Pullfrog 评审(515e36b)后新增的两个提交,核心是把 provider factory 从运行时注册收敛为静态声明。

  • 用静态 builtInProviderFactories 替换运行时 registryproviderFactory 接口移除 Name() 方法,brave/tavily 各自删除 init() 注册逻辑,改为 provider.go 中可审查的 map[string]providerFactory 字面量;重复 provider 名称会在编译期失败而非运行时静默覆盖。
  • 同步更新测试 — 新增 TestBuiltInProviderFactories 断言内置表恰好包含 brave 与 tavily;原有 registry 测试重命名为 TestNewProviderUsesBuiltInProviderFactory 并保持验证路径不变。
  • 对齐设计文档messages-proxy.mdweb-search-provider-research.mddocker-compose-deployment.md、配置参考与示例补充 ValidateOptions 合同说明、Brave 域名策略限制与 Compose Web Search 部署约定。

go build / go test ./internal/websearch/... ./internal/messages/... 均通过。本次只复审了增量提交,未重复评审 CodeRabbit 与早期 Pullfrog 已覆盖的初始实现。

PullfrogView workflow run | Using anthropic/glm-5.2𝕏

Pullfrog  | View workflow run | Using anthropic/glm-5.2𝕏

@duckpr duckpr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DuckPR reviewer: opencode
Model: anthropic/glm-5.2

ℹ️ 无新问题。重构与 opaque-ID 支持实现正确、测试充分;仅一处文档表述与新增编码路径不一致,已在内联标注。

Reviewed changes — 本次复审聚焦上次 Pullfrog 评审(dbe4ffa)后新增的三个提交:742d675(拆分 web search 文件职责)、32c1356(补齐文件边界设计文档)、42bf4a2(支持 BYOK provider-owned opaque tool ID)。

  • 将 web search 实现按职责拆分为四个文件web_search_request.go(入站 JSON 解析、tools[].type 精确匹配与 oma_web_search 投影)、web_search_gateway.go(BYOK 采样循环、provider 执行与 mixed/pause continuation 编排)、web_search_protocol.go(历史双向投影与 tool_use_id 配对)、web_search_response.go(执行结果映射、usage 合成与 JSON/SSE 编码)。拆分是保持行为的机械迁移,依赖方向清晰,共享类型仍统一使用 webSearch* 前缀。
  • 新增 ## Web Search 代码职责与依赖边界 章节messages-proxy.md 用表格列出每个文件的职责与「不负责的事项」,并补充 Anthropic 官方协议依据(mixed turn 时序、tool_use_id 配对、max_uses per-request 语义、web_search_requests 计费规则)。
  • 支持 provider-owned opaque tool-use ID — 非 toolu_* 的上游 ID(例如 call_00_*)经带版本标记的 URL-safe base64 编码为 srvtoolu_oma_encoded_<payload>,而既有 toolu_* 保持前缀替换以兼容已保存历史;TestWebSearchToolUseIDMappingHasNoCollisions 覆盖 toolu_oma_* / toolu_srvtoolu_* 等可能碰撞的边界形状,TestWebSearchGatewayExecutesOpaqueUpstreamSearchToolUse 端到端验证 opaque ID 的搜索执行与响应投影。
  • SSE 测试补齐 continuation loop 覆盖TestWebSearchGatewaySSEResponse 现在真实触发 tool_use -> end_turn 两轮 BYOK 采样,覆盖 server_tool_use/web_search_tool_result/web_search_resultinput_json_delta 的 SSE 编码。

go build ./...go test ./internal/messages/... ./internal/websearch/... 均通过。本次只复审了增量提交,未重复评审 CodeRabbit 与早期 Pullfrog 已覆盖的初始实现。

PullfrogView workflow run | Using anthropic/glm-5.2𝕏

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using anthropic/glm-5.2𝕏

Comment thread docs/design/be/messages-proxy.md Outdated
end
```

内部 transcript 与 Claude Code transcript 通过双向投影保持一致。gateway 声明给 BYOK 的是名为 `oma_web_search` 的 ordinary tool:调用方可以合法地声明自己的 `web_search` 工具,复用 Anthropic 协议名会让同一请求出现两个同名 tool,也无法再按名字判断 `tool_use` 归属。投影只在这两个名字之间双向替换;当顶层 `tool_choice` 强制 `web_search` 时,也只在发给 BYOK 的请求里改为 `oma_web_search`。调用方看到的响应、历史和续传请求始终使用协议名 `web_search`;调用方自有的同名工具按普通 client tool 交还调用方,不由 gateway 执行。BYOK 的 ordinary `toolu_*` 与面向调用方的 `srvtoolu_*` 之间只做前缀替换,因此映射是双射;不引入自定义编码可以避免同一上游 ID 存在多种外部表示、进而在同一条 assistant message 里产生重复 `tool_use` ID。反向解析只接受 gateway 自己铸造的 `srvtoolu_` 形状,其余形状一律拒绝,避免调用方伪造上游 ID。Claude Code 后续重放完整历史时,gateway 会把 `server_tool_use`/`web_search_tool_result` 展开回 BYOK 的 assistant `tool_use` 与下一条 user `tool_result`。mixed continuation 中 Claude Code 只返回自己拥有的 client results,每个 client `tool_use` 必须恰好对应一个 `tool_result`,该 user message 不得包含 text 或其他 block,并保留声明 pending search 的同一 Web Search tool;缺失、重复或未知 result、混入非 result content,以及缺少 pending search tool时返回 `400 invalid_request_error`,不会把 server block 透传给 BYOK。gateway 执行 pending search 后按原 tool call 顺序合并结果。该排序只为可读性:Anthropic 按 `tool_use_id` 匹配 result 而非按位置,也不区分 server/client tool 的 result 先后。真正的硬约束是配对完整性——每个 `tool_use` 必须恰好有一个 result,缺失会触发 `tool_use ids were found without tool_result blocks immediately after`,因此合并时即使出现重复 ID 也不丢弃任何一条 result。普通 client tool 不由 OMA 伪造 error result。已完成的 search 历史即使后续请求不再声明 Web Search,也仍会反向投影,但不会允许 BYOK 发起新的搜索。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ 这段投影说明仍写「toolu_*srvtoolu_* 之间只做前缀替换」,但紧接着的 opaque-ID 段落已经引入了第二条编码路径(非 toolu_* ID 走 srvtoolu_oma_encoded_<base64>)。两者都确实以 srvtoolu_ 开头、反向解析也都只接受 gateway 铸造的形状,所以安全性结论不变;只是「只做前缀替换」这句与下方实现不一致,建议改成「toolu_* 走前缀替换、其他 opaque ID 走带版本标记的编码」之类的两句式描述,避免下一位维护者按旧描述推断映射是单一前缀交换。

@duckpr duckpr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DuckPR reviewer: opencode
Model: anthropic/glm-5.2

✅ 增量复审无新问题。515e36b 之后的重构把 request/response/protocol 职责拆分到独立文件,opaque BYOK tool ID 编码正确且测试覆盖充分。

Reviewed changes — 本次复审聚焦 515e36b(上一次 Pullfrog 评审)之后的 6 个提交,核心是把 web search gateway 的职责按文件拆分、用静态 map 替换 init() 注册、并支持 provider-owned 的任意 opaque tool ID。

  • 拆分 gateway 文件职责 — 把 web_search_gateway.goweb_search_protocol.go 中的请求投影、响应编码、usage 累加等逻辑机械抽取到 web_search_request.goweb_search_response.go;类型与方法签名不变,无行为改变。
  • 支持 provider-owned opaque tool IDserverWebSearchToolUseID/upstreamWebSearchToolUseID 新增 base64 URL-safe 编码路径,兼容既有 toolu_* 前缀替换的同时覆盖 call_* 等任意非空 opaque ID;encodedUpstreamToolUseIDPrefix 命名空间隔离避免 legacy 与 encoded 路径碰撞。
  • 用静态 map 替换 init() 注册builtInProviderFactories 改为编译期常量 map,消除 init() 副作用和 Name() 方法,重复键在编译期即冲突。
  • 补充测试 — 新增 TestWebSearchGatewayExecutesOpaqueUpstreamSearchToolUse(端到端 opaque ID 流程)、TestWebSearchToolUseIDMappingSupportsOpaqueUpstreamIDs(三轮换回测 + 碰撞检测)和 TestBuiltInProviderFactories
  • 同步设计文档messages-proxy.mdweb-search-provider-research.md 补充 opaque ID 编码与 provider 契约说明。

go test ./internal/messages/... ./internal/websearch/... -count=1-race 均通过。

PullfrogView workflow run | Using anthropic/glm-5.2𝕏

Pullfrog  | View workflow run | Using anthropic/glm-5.2𝕏

@duckpr duckpr Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DuckPR reviewer: opencode
Model: anthropic/glm-5.2

✅ 增量复审无新问题。7c89aaf 把 tool ID 映射收敛为单一 opaque 编码路径,消除了 legacy 前缀替换分支,双射性和拒绝逻辑正确,测试覆盖充分。

Reviewed changes — 本次复审聚焦上一次 Pullfrog 评审(515e36b)后的 7 个增量提交(b10f8f37c89aaf),核心是把 tool ID 映射收敛为单一 opaque 编码、按职责拆分 web search 文件,以及显式声明 built-in provider。

  • 将 tool ID 映射统一为单一 opaque 编码路径42bf4a2 + 7c89aaf)— 移除了 toolu_* 前缀替换的 legacy 分支;所有 provider-owned ID(包括 Anthropic 的 toolu_*)统一走 srvtoolu_oma_encoded_<base64>upstreamWebSearchToolUseID 只接受带版本标记的编码形状,拒绝其他 srvtoolu_*,消除了"同一上游 ID 多种外部表示"的歧义。base64 编码保证双射,碰撞测试覆盖 toolu_abc/toolu_oma_abc/toolu_srvtoolu_abc/toolu_1 四种输入。
  • 按职责拆分 web search 代码742d675 + 32c1356)— 将原 gateway.go 拆为 web_search_gateway.go(主流程)、web_search_protocol.go(transcript 投影与 tool ID 映射)、web_search_request.go(请求解析与策略校验)、web_search_response.go(响应组装与 SSE 编码),符合 AGENTS.md 的垂直资源切片与关注点分离方向。
  • 显式声明 built-in providerb10f8f3 + dbe4ffa)— 用静态 builtInProviderFactories map 注册 brave/tavily,新增 provider 成为显式可评审改动;provider contract 文档同步对齐。
  • 同步更新设计文档3b5b629)— messages-proxy.md 明确了入口按 tools[].type 拦截、调用块按 name=oma_web_search 识别的两阶段区分,以及 opaque ID 映射不依赖 provider 格式的说明。

go build ./internal/messages/... ./internal/websearch/...go test ./internal/messages/... ./internal/websearch/... 均通过;tool ID 映射、duplicate server tool use、mixed/paused continuation 相关测试全部通过。本次只复审增量提交,未重复评审 CodeRabbit 已覆盖的初始实现。

Pullfrog  | View workflow run | Using anthropic/glm-5.2𝕏

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.

2 participants