feat(tools): accept natural web_search tool declarations and emit native result blocks - #523
Conversation
A client that cannot be told to say `otari_web_search` never reached a configured search backend. Claude Code, the Anthropic SDK, and Claude Desktop send Anthropic's own `web_search_20250305`, which passed straight through to a provider that may not serve it. Add an opt-in `web_search_intercept` setting: with it on, the gateway also claims the provider-named web-search keywords (bare `web_search`, any `web_search_<suffix>`) and runs them against its own backend. Off by default, because turning it on takes a search away from a provider that would have run it, so an upgrade never silently changes who searches. It also requires a backend URL: with nothing to intercept to, the keyword still passes through rather than failing the request. An OpenAI `function` named `web_search` is deliberately never claimed. That is the caller's own tool, and running it server-side means their handler never fires and they never get back a tool_call to dispatch. LiteLLM excludes the same case for the same reason. On `/v1/messages`, a caller that declared web search natively now gets `server_tool_use` + `web_search_tool_result` blocks so citations render. This reverses a documented decision not to emit them: `web_search_tool_result` requires `encrypted_content`, an Anthropic-signed blob. Rather than forge one, send it empty (as LiteLLM does) and strip both minted block types off inbound `messages`, so an echoed turn never ships an unsignable block upstream. Only callers using the native vocabulary get the blocks; `otari_web_search` and the bare short form keep their plain-text result. Also add `GET /v1/tools` and a "how to call this" card on the dashboard Tools page, so the contract is discoverable instead of docs-only, and retarget a forced `tool_choice` when a caller declares web search under its own name. Fixes #488 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Narrow ServerToolUseBlock.name to Anthropic's Literal at the one construction site, and cast the union-typed content blocks the new tests assert against. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 2 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (30)
WalkthroughThe gateway adds opt-in provider-native web-search interception, Anthropic-native result blocks, managed-tool discovery at ChangesWeb-search interception and native results
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 1 | ❌ 4❌ Failed checks (4 warnings)
✅ Passed checks (1 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
✨ Simplify code
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. Comment |
Register /v1/tools standalone-only. It was registered in both modes, but hybrid mode never calls init_db, so the auth dependency's session opened against an uninitialized engine and every request 500'd. Standalone-only rather than a mode-aware auth path, because hybrid cannot answer the question honestly either: there the platform owns the per-workspace tool policy, so a tool this gateway has configured can still be refused with a 403 for the caller asking. Gate the inbound minted-block stripping on interception being *active* (opted in AND a backend configured), not just the opt-in. With the toggle on and no backend the keyword is forwarded, so the provider ran the search and signed its own blocks; stripping those off an echoed turn broke the citations round-trip Anthropic established. Add web_search_intercept to the two hand-maintained mirrors it was missing from: settings._CONFIG_VIEW (so it appears in the dashboard's effective config view like its six siblings) and docker-compose.yml. Also document the mixed-batch case in docs/tools.md and add the missing border on the unavailable-tool pill. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eclarations # Conflicts: # src/gateway/static/dashboard/assets/ActivityPage-C28CtpkN.js # src/gateway/static/dashboard/assets/ActivityPage-CHnjo06U.js # src/gateway/static/dashboard/assets/ActivityPage-YqPguqY9.js # src/gateway/static/dashboard/assets/BudgetsPage-B9iEC7ec.js # src/gateway/static/dashboard/assets/BudgetsPage-CKz29CLE.js # src/gateway/static/dashboard/assets/BudgetsPage-H5EC9TjF.js # src/gateway/static/dashboard/assets/ConfirmDialog-BUTewg4-.js # src/gateway/static/dashboard/assets/ConfirmDialog-DzIVWoi5.js # src/gateway/static/dashboard/assets/ConfirmDialog-mbnZRETP.js # src/gateway/static/dashboard/assets/DocsPage-D53o1bCm.js # src/gateway/static/dashboard/assets/DocsPage-DMCHyXnB.js # src/gateway/static/dashboard/assets/DocsPage-DT-vQq9F.js # src/gateway/static/dashboard/assets/KeysPage-BaZsnXlK.js # src/gateway/static/dashboard/assets/KeysPage-CtY-9BqY.js # src/gateway/static/dashboard/assets/KeysPage-fg3Rz_lV.js # src/gateway/static/dashboard/assets/ModelScopeControl-BBYX_HiM.js # src/gateway/static/dashboard/assets/ModelScopeControl-CvryzceJ.js # src/gateway/static/dashboard/assets/ModelScopeControl-dT2wvSYp.js # src/gateway/static/dashboard/assets/ModelsPage-DwGsArXU.js # src/gateway/static/dashboard/assets/OverviewPage-0PkW5qfi.js # src/gateway/static/dashboard/assets/OverviewPage-B3hDScv0.js # src/gateway/static/dashboard/assets/OverviewPage-B5D-L-_p.js # src/gateway/static/dashboard/assets/ProvidersPage-bLdWCa5G.js # src/gateway/static/dashboard/assets/RoutingPage-CD9-r0aL.js # src/gateway/static/dashboard/assets/RoutingPage-D1os8M2m.js # src/gateway/static/dashboard/assets/RoutingPage-tnhqIdEV.js # src/gateway/static/dashboard/assets/SettingsPage-BtoAJ9Y8.js # src/gateway/static/dashboard/assets/SettingsPage-C2Hp1OPt.js # src/gateway/static/dashboard/assets/SettingsPage-aPYjzoIy.js # src/gateway/static/dashboard/assets/TablePagination-BEmYAlSB.js # src/gateway/static/dashboard/assets/TablePagination-Bnlr8FbD.js # src/gateway/static/dashboard/assets/TablePagination-D-Hd6viC.js # src/gateway/static/dashboard/assets/ToolsGuardrailsPage-DNC3Wa-x.js # src/gateway/static/dashboard/assets/UsagePage-DVleTLk-.js # src/gateway/static/dashboard/assets/UsagePage-pCJIBBUc.js # src/gateway/static/dashboard/assets/UsagePage-tyubYvXE.js # src/gateway/static/dashboard/assets/UsersPage-Be1Tcz9b.js # src/gateway/static/dashboard/assets/UsersPage-CRNgQ9x1.js # src/gateway/static/dashboard/assets/UsersPage-CxBBmCnt.js # src/gateway/static/dashboard/assets/index-BE7E0N2z.js # src/gateway/static/dashboard/index.html
|
@coderabbitai review Note: this comment was posted by Claude via back-and-forth with @njbrake. The reasoning and decisions are his. |
|
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/gateway/api/routes/_tools.py (1)
9-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReplace prohibited prose separators.
Use punctuation or rephrase these comments. Do not use em dashes or double-hyphen separators in Python prose.
src/gateway/api/routes/_tools.py#L9-L25: Replace the em dash separator.src/gateway/services/mcp_loop_messages.py#L172-L183: Replace the em dash in the inline comment.tests/unit/test_chat_request_helpers.py#L3-L14: Replace the em dash separator.tests/unit/test_chat_request_helpers.py#L164-L164: Replace the hyphen heading separator.tests/unit/test_chat_request_helpers.py#L230-L230: Replace the hyphen heading separator.tests/unit/test_chat_request_helpers.py#L278-L278: Replace the hyphen heading separator.tests/unit/test_mcp_loop_messages.py#L1077-L1082: Replace the hyphen heading separator.🤖 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 `@src/gateway/api/routes/_tools.py` around lines 9 - 25, Replace prohibited em dash and hyphen separators in the prose comments and test headings at src/gateway/api/routes/_tools.py lines 9-25, src/gateway/services/mcp_loop_messages.py lines 172-183, tests/unit/test_chat_request_helpers.py lines 3-14, 164, 230, and 278, and tests/unit/test_mcp_loop_messages.py lines 1077-1082. Rephrase or use standard punctuation while preserving the existing meaning; no behavioral code changes are needed.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 `@src/gateway/api/routes/messages.py`:
- Around line 154-158: Update the filtering predicate used to build kept_blocks
so it removes only gateway-minted native blocks with unambiguous gateway
provenance, preserving provider-generated server_tool_use and
web_search_tool_result blocks. Add a regression test covering an
enabled-interception request with a provider-generated native pair and verify
the provider receives both blocks unchanged.
In `@src/gateway/services/mcp_loop_messages.py`:
- Around line 443-446: Update the mixed tool-batch paths in
src/gateway/services/mcp_loop_messages.py at lines 443-446 and 647-649 to
collect native blocks whenever a gateway-owned search executes, not only in the
all-owned continuation path; ensure both non-streaming and streaming exits emit
the corresponding server_tool_use and web_search_tool_result blocks, and add
tests covering one gateway search combined with one caller-managed tool in each
mode.
---
Nitpick comments:
In `@src/gateway/api/routes/_tools.py`:
- Around line 9-25: Replace prohibited em dash and hyphen separators in the
prose comments and test headings at src/gateway/api/routes/_tools.py lines 9-25,
src/gateway/services/mcp_loop_messages.py lines 172-183,
tests/unit/test_chat_request_helpers.py lines 3-14, 164, 230, and 278, and
tests/unit/test_mcp_loop_messages.py lines 1077-1082. Rephrase or use standard
punctuation while preserving the existing meaning; no behavioral code changes
are needed.
🪄 Autofix
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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 9bbbcd47-dd88-4869-8292-20419de6ac4f
⛔ Files ignored due to path filters (1)
docs/public/openapi.jsonis excluded by!docs/public/openapi.json
📒 Files selected for processing (52)
docker-compose.ymldocs/api-reference.mddocs/configuration.mddocs/dashboard.mddocs/public/otari.postman_collection.jsondocs/tools.mdscripts/sdk_codegen/sdk-endpoints.txtsrc/gateway/api/main.pysrc/gateway/api/routes/_pipeline.pysrc/gateway/api/routes/_tools.pysrc/gateway/api/routes/chat.pysrc/gateway/api/routes/messages.pysrc/gateway/api/routes/responses.pysrc/gateway/api/routes/settings.pysrc/gateway/api/routes/tool_settings.pysrc/gateway/api/routes/tools.pysrc/gateway/core/config.pysrc/gateway/services/mcp_loop_messages.pysrc/gateway/services/sandbox_backend.pysrc/gateway/services/tool_settings_service.pysrc/gateway/services/web_search_backend.pysrc/gateway/static/dashboard/assets/ActivityPage-CBkPEqTm.jssrc/gateway/static/dashboard/assets/BudgetsPage-CYOXYjZv.jssrc/gateway/static/dashboard/assets/ConfirmDialog-DgcF58qQ.jssrc/gateway/static/dashboard/assets/DocsPage-mZIYniek.jssrc/gateway/static/dashboard/assets/KeysPage-LWzPuWOd.jssrc/gateway/static/dashboard/assets/ModelScopeControl-D0HVPN2y.jssrc/gateway/static/dashboard/assets/ModelsPage-CwFQs9Yf.jssrc/gateway/static/dashboard/assets/OverviewPage-BkCDNMJs.jssrc/gateway/static/dashboard/assets/ProvidersPage-DUovBYoY.jssrc/gateway/static/dashboard/assets/RoutingPage-Cus6VumX.jssrc/gateway/static/dashboard/assets/SettingsPage-N5HYVsA_.jssrc/gateway/static/dashboard/assets/TablePagination-C7a0MSTB.jssrc/gateway/static/dashboard/assets/ToolsGuardrailsPage-DNC3Wa-x.jssrc/gateway/static/dashboard/assets/ToolsGuardrailsPage-tLXMuWh_.jssrc/gateway/static/dashboard/assets/UsagePage-hKpUiHP6.jssrc/gateway/static/dashboard/assets/UsersPage-CUI67MS_.jssrc/gateway/static/dashboard/assets/index-BE7E0N2z.jssrc/gateway/static/dashboard/assets/index-Bywr3Bjy.jssrc/gateway/static/dashboard/index.htmltests/integration/test_hybrid_mode_messages.pytests/integration/test_messages_route_dispatch.pytests/integration/test_web_search_interception.pytests/unit/test_chat_request_helpers.pytests/unit/test_mcp_loop_messages.pytests/unit/test_messages_minted_block_stripping.pytests/unit/test_tools_endpoint.pytests/unit/test_web_search_backend.pyweb/src/api/hooks.tsweb/src/api/types.tsweb/src/pages/ToolsGuardrailsPage.test.tsxweb/src/pages/ToolsGuardrailsPage.tsx
💤 Files with no reviewable changes (2)
- src/gateway/static/dashboard/assets/ToolsGuardrailsPage-DNC3Wa-x.js
- src/gateway/static/dashboard/assets/index-BE7E0N2z.js
… batches Two correctness defects CodeRabbit caught on the review pass. Stripping was too blunt. With interception on, every server_tool_use and web_search_tool_result was removed from an inbound transcript, including the Anthropic-signed blocks of a search the provider ran itself, silently degrading a citations chain the gateway does not own. The empty encrypted_content the gateway sends is itself the provenance marker (Anthropic always populates that field), so stripping is now scoped to blocks carrying it, and a server_tool_use is dropped only alongside the minted result that answers it, matched by tool_use_id, so a provider's pair is never split into an orphan the API would reject. This is stricter than the Responses path, which strips wholesale because no equivalent marker is available there. Mixed batches emitted no citations. When the model asked for a gateway search alongside a caller tool, the search ran but a native client got no pair describing it. Non-streaming now collects in execute_owned, which takes the accumulator (threaded through the loop protocol; chat and responses accept and ignore it), and fold_usage prepends the blocks on that exit. Streaming collects in finalize_exit and terminal_events emits them ahead of message_delta / message_stop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…eclarations # Conflicts: # src/gateway/static/dashboard/assets/ActivityPage-CBkPEqTm.js # src/gateway/static/dashboard/assets/BudgetsPage-CCjnKhY4.js # src/gateway/static/dashboard/assets/BudgetsPage-CYOXYjZv.js # src/gateway/static/dashboard/assets/BudgetsPage-H5EC9TjF.js # src/gateway/static/dashboard/assets/ConfirmDialog-BqztIUQ8.js # src/gateway/static/dashboard/assets/ConfirmDialog-DgcF58qQ.js # src/gateway/static/dashboard/assets/ConfirmDialog-DzIVWoi5.js # src/gateway/static/dashboard/assets/DocsPage-BLX9Ugok.js # src/gateway/static/dashboard/assets/DocsPage-DT-vQq9F.js # src/gateway/static/dashboard/assets/DocsPage-mZIYniek.js # src/gateway/static/dashboard/assets/KeysPage-CtY-9BqY.js # src/gateway/static/dashboard/assets/KeysPage-LWzPuWOd.js # src/gateway/static/dashboard/assets/KeysPage-eN3TspGP.js # src/gateway/static/dashboard/assets/ModelScopeControl-BTAbFoU-.js # src/gateway/static/dashboard/assets/ModelScopeControl-D0HVPN2y.js # src/gateway/static/dashboard/assets/ModelScopeControl-dT2wvSYp.js # src/gateway/static/dashboard/assets/ModelsPage-BHFs32Be.js # src/gateway/static/dashboard/assets/ModelsPage-BHrGQHDE.js # src/gateway/static/dashboard/assets/ModelsPage-CwFQs9Yf.js # src/gateway/static/dashboard/assets/OverviewPage-B5D-L-_p.js # src/gateway/static/dashboard/assets/OverviewPage-BkCDNMJs.js # src/gateway/static/dashboard/assets/OverviewPage-CJaIAUu7.js # src/gateway/static/dashboard/assets/ProvidersPage-BYCOF7U0.js # src/gateway/static/dashboard/assets/ProvidersPage-DUovBYoY.js # src/gateway/static/dashboard/assets/ProvidersPage-Jb_jxGkj.js # src/gateway/static/dashboard/assets/RoutingPage-CD9-r0aL.js # src/gateway/static/dashboard/assets/RoutingPage-CuN4PH28.js # src/gateway/static/dashboard/assets/RoutingPage-Cus6VumX.js # src/gateway/static/dashboard/assets/SettingsPage-BtoAJ9Y8.js # src/gateway/static/dashboard/assets/SettingsPage-DAz4lpRs.js # src/gateway/static/dashboard/assets/SettingsPage-N5HYVsA_.js # src/gateway/static/dashboard/assets/TablePagination-Bnlr8FbD.js # src/gateway/static/dashboard/assets/TablePagination-C7a0MSTB.js # src/gateway/static/dashboard/assets/TablePagination-CodBBmt3.js # src/gateway/static/dashboard/assets/ToolsGuardrailsPage-Bh69IbXF.js # src/gateway/static/dashboard/assets/UsagePage-hKpUiHP6.js # src/gateway/static/dashboard/assets/UsersPage-CRNgQ9x1.js # src/gateway/static/dashboard/assets/UsersPage-CUI67MS_.js # src/gateway/static/dashboard/assets/UsersPage-DgtI9L1U.js # src/gateway/static/dashboard/index.html
Description
Clients that speak a provider's own web-search vocabulary never reached a configured search backend. Claude Code and the Anthropic SDK send
web_search_20250305, which passed straight through to a provider that may not serve it.Adds an opt-in
web_search_interceptsetting. With it on, Otari also claims the provider-named keywords (bareweb_search, anyweb_search_<suffix>) and runs them against its own backend. Off by default, so an upgrade never silently changes who runs a search, and it requiresweb_search_url: with nothing to intercept to, the keyword still passes through rather than failing the request.Three deliberate deviations from the issue, all matching LiteLLM's behaviour after reading its
websearch_interceptionimplementation:functionnamedweb_searchis never claimed. That is the caller's own tool; running it server-side means their handler never fires and they never get back atool_callto dispatch. LiteLLM excludes this case explicitly for the same reason.max_usesis accepted but not enforced.max_tool_iterationsbounds the loop; searches per iteration are not 1:1./v1/messagesnow emitsserver_tool_use+web_search_tool_resultblocks so citations render, but only for a caller that declared natively. This reverses a decision documented indocs/tools.md: the result block requiresencrypted_content, an Anthropic-signed blob. Rather than forge one, it is sent empty (as LiteLLM does) and both minted block types are stripped off inboundmessages, so an echoed turn never ships an unsignable block upstream. Stripping only applies when interception is on, so deployments that never opted in are byte-identical.Also adds
GET /v1/toolsplus a "how to call this" card on the dashboard Tools page, and retargets a forcedtool_choicewhen a caller declares web search under its own name.Verified end to end against a
llamacppprovider over/v1/messages, patching only the provider's_acompletionso any-llm's real Messages/Completions bridge ran both ways: the search executed, results reached the model, and the response carried the native blocks with gapless indices in both streaming and non-streaming mode.PR Type
Relevant issues
Fixes #488
Checklist
tests/unit,tests/integration).make lint,make typecheck,make test).uv run python scripts/generate_openapi.py).Two integration tests fail on this branch and fail identically on
origin/main:test_error_detail_leakage::test_provider_error_does_not_leak_detailsandtest_streaming_error_event::test_streaming_creation_error_returns_http_error. Both expect a 502 from a live provider call.AI Usage
AI Model/Tool used:
Claude Opus 5 via Claude Code.
Any additional AI details you'd like to share:
Written by Claude through back-and-forth with @njbrake. The three deviations above and the empty-
encrypted_contentapproach were his calls, made after the agent researched how LiteLLM, Bifrost, and the Vercel AI SDK handle the same problem; the prose is the agent's.Summary
GET /v1/toolsfor managed-tool discovery.