Studio: standalone Fetch pill for Anthropic web_fetch#5742
Conversation
web_fetch used to be silently bundled with the Search pill on the assumption that "search returns URLs, fetch reads them" is the typical workflow. Two problems with that: - Anthropic bills each web_fetch invocation separately from web_search hits, so combining them made the per-message cost surface ambiguous. - It blocked "just fetch this one URL" workflows where the user already knows the page they want read and does not want a search round-trip. Adds: - `webFetchToolsEnabled` to the chat-runtime-store, persisted to localStorage under `unsloth_chat_web_fetch_tools_enabled`, with a matching `supportsBuiltinWebFetch` capability flag and a `setWebFetchToolsEnabled` setter. - A new Fetch pill in the chat composer, rendered next to Images and only when the active provider returns true from `providerSupportsBuiltinWebFetch` (Anthropic today). The pill defaults off so per-fetch billing is always a deliberate opt-in. - chat-page bootstraps `webFetchToolsEnabled` from the same stored- preference fallback the other pills use. - chat-adapter reads `webFetchToolsEnabled` directly when deciding whether to append "web_fetch" to `enabled_tools`, decoupling it from `toolsEnabled` (Search). Backend translation is unchanged: when `enabled_tools` already contains "web_fetch", `_stream_anthropic` appends the `web_fetch_20250910` / `web_fetch_20260209` tool exactly as before (test_anthropic_web_fetch.py pins the standalone-only path at `test_web_fetch_tool_appended_to_request_body` and the combined path at `test_web_fetch_combined_with_web_search_and_code_execution`). Frontend tsc passes.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fa074e8bde
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const webFetchEnabledForThisTurn = | ||
| Boolean( | ||
| externalProvider && | ||
| toolsEnabled && | ||
| webFetchToolsEnabled && | ||
| providerSupportsBuiltinWebFetch(externalProvider.providerType), |
There was a problem hiding this comment.
Treat web_fetch as available in disabled-tool guard
After introducing the standalone webFetchToolsEnabled path, createOpenAIStreamAdapter computes webFetchEnabledForThisTurn but the disabledToolGuard branching still only checks webSearchEnabledForThisTurn/codeExecEnabledForThisTurn, so an Anthropic turn with Fetch ON and Search OFF is told “you do not have web search or web fetch tools.” In that state the model is instructed not to use a capability that is actually enabled, which can suppress web_fetch tool calls and break the new standalone-fetch workflow.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Code Review
This pull request decouples the 'Web Fetch' tool from the 'Web Search' tool, introducing a standalone 'Fetch' pill in the chat composer. The changes span the chat runtime store, the chat adapter for request construction, and the UI components to manage the independent state and persistence of the fetch tool. A review comment identifies a critical oversight where the disabledToolGuard logic was not updated, which may cause the system prompt to incorrectly inform the model that it lacks fetch capabilities even when the tool is enabled.
| const webFetchEnabledForThisTurn = | ||
| Boolean( | ||
| externalProvider && | ||
| toolsEnabled && | ||
| webFetchToolsEnabled && | ||
| providerSupportsBuiltinWebFetch(externalProvider.providerType), | ||
| ); |
There was a problem hiding this comment.
The logic for webFetchEnabledForThisTurn is correctly implemented here, but it appears that the disabledToolGuard logic (around line 954) was not updated to account for this new standalone tool.
Currently, if a user enables the Fetch pill but leaves Search and Code disabled, the system prompt will incorrectly inform the model that it does not have access to web fetch capabilities. You should update the guard conditions to check webFetchEnabledForThisTurn as well.
…-org release fetch)
Reviewer P1 / High on PR #5742 (codex + gemini): after introducing the standalone Fetch pill, `disabledToolGuard` still only branched on `webSearchEnabledForThisTurn`. With Fetch ON and Search OFF the system prompt would tell Claude "you do not have web search or web fetch tools in this conversation", which contradicts the actual tool schema being sent and suppresses `web_fetch` tool calls, defeating the standalone-fetch workflow this PR adds. Treat search and fetch as a single "any web tool enabled" axis. The guard only needs to warn the model when no web tool is wired in for this turn; once either pill is on the model can pick the right one from the tool schema. The existing `webLabel` already covers both names, so the user-visible guard text stays accurate in every combination. tsc clean.
|
Pushed 9d2b0c2 addressing the codex P1 and gemini High on the disabled-tool guard. Confirmed the gap: after introducing Fix: treat search and fetch as a single "any web tool enabled" axis ( tsc clean. Re-confirmed backend translation is unchanged (test_anthropic_web_fetch already pins standalone + combined paths). |
The web_fetch tool body in `_stream_anthropic` hardcoded `web_fetch_20250910` instead of calling `_anthropic_web_fetch_version`, so Opus 4.6 / 4.7 and Sonnet 4.6 missed the `web_fetch_20260209` dynamic-filtering variant. The picker, the unit tests for it, and a deliberate "follow-up" note in `test_anthropic_web_fetch.py` already existed; this just threads it through the emission site. Mirrors how web_search and code_execution are dispatched per model. Old models still resolve to `web_fetch_20250910` and continue to work.
for more information, see https://pre-commit.ci
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
Round 4 cross-cutting fix: merged origin/main into this branch (no conflicts) to bring in PR #5735 (orphan tool_call XML strip widening + 263-line test_tool_xml_strip.py). All 8 PRs in this audit cohort had been forked off a pre-#5735 main, so a squash-merge of any of them would have silently reverted the widened _TOOL_XML_RE regex and deleted the dedicated test file. Verified: diff against origin/main now shows zero unintended changes to routes/inference.py and test_tool_xml_strip.py outside the actual PR scope. |
Main moved forward 17 commits during PR review (latest: 953c8bf). Real conflicts in five files; resolved by combining both branches' changes. studio/backend/core/inference/external_provider.py - Add fast_mode (Anthropic Opus 4.6/4.7 speed flag, unslothai#5715) to stream_chat_completion and Anthropic-branch call site, alongside existing Gemini tools/tool_choice forwarding. - Add _openai_image_generation_tool() helper (action:"edit" for follow- up image edits, unslothai#5712) and use it inside the existing _responses_hosted_builtins_allowed gate so the forced-function / tool_choice="none" suppression added in rounds 21+ still applies. - Keep Anthropic web_fetch gated on _anthropic_hosted_builtins_allowed (round 19+ hosted-builtin gate) while taking main's per-model version selector (web_fetch_20260209 vs _20250910). studio/backend/routes/inference.py - Add `openai = provider_type == "openai"` (used by main's reasoning content forwarding for follow-up image edits). - Keep the round 25/26 Gemini filter chain (_filter_tool_calls drops synthetic server-builtin cards, marks tc_id so the matching role="tool" follow-up gets skipped, extra_content gated to native Gemini host). - Forward fast_mode alongside tools/tool_choice. studio/backend/tests/test_openai_image_generation.py - Combine assertions: both _server_tool: True (PR) and openai_image_generation_call_id (main) are present on the tool_start arguments. studio/frontend/src/features/chat/shared-composer.tsx - Add supportsBuiltinWebFetch declaration (separate Fetch pill from unslothai#5742) before the PR's isExternalGemini constant so both the Gemini image-tier gating and the standalone Anthropic Fetch pill compile. studio/frontend/src/features/chat/api/chat-adapter.ts - Add main's normalizeOpenAIReasoningItem, toOpenAIImageEditReferenceMessage, isAnthropicRefusalMessage helpers alongside PR's collectAssistantToolCalls, collectToolResultMessages, SerializedMessage, collectAssistantTextThoughtSignature. - toOpenAIMessages (PR) now also early-returns on isAnthropicRefusalMessage so refused turns get pruned from outbound history. - Add a thin toOpenAIMessage (singular) wrapper for the OpenAI image- edit replay path's flat .map() usage. - Merge per-turn enable flags: keep PR's imageGenerationEnabledForThisTurn, geminiImageModeForThisTurn, codeExecEnabledForThisTurn !geminiImageMode gate; take main's webFetchEnabledForThisTurn (sourced from independent webFetchToolsEnabled pill state). - Outbound build chains main's anthropic_refusal survivingMessages prune, then flatMap(toOpenAIMessages) (PR), then PR's selectedImageEditReference reference message prepend; image-edit unavailable toast from main fires before any of that when the pill is off. - tool_end merge: do main's nextArgs spread first, then PR's Gemini native_part parts concat so both OpenAI image-call ids and Gemini executableCode/codeExecutionResult/inlineData round-trip. - Cumulative + final yields: orderAssistantContent(pinTextThoughtSignature(...)) composes main's tool-vs-text ordering with PR's per-text thoughtSignature pin. Tests: gemini provider 148/148; openai_responses_translation + openai_code_execution + openai_image_generation + anthropic_code_execution + anthropic_web_fetch + external_provider_usage_chunk + providers_api: 50 passed, 42 skipped; main's new anthropic_fast_mode + citations + openai_citation_markers + openai_tool_result_fallbacks suites all 43/43.
Summary
web_fetch_20250910/web_fetch_20260209from the Search pill so users can read a single URL or PDF without also turning on web_search.webFetchToolsEnabled/unsloth_chat_web_fetch_tools_enabledin localStorage). The pill is hidden on providers that don't ship web_fetch.Why
web_fetch used to be silently bundled with the Search pill on the assumption that "search returns URLs, fetch reads them" is the typical workflow. Two problems with that:
web_fetchinvocation separately fromweb_searchhits, so combining them made the per-message cost surface ambiguous.Implementation
chat-runtime-store.ts: newwebFetchToolsEnabledboolean (persisted vialoadBool/saveBoolonCHAT_WEB_FETCH_TOOLS_ENABLED_KEY), matchingsupportsBuiltinWebFetchcapability,setWebFetchToolsEnabledsetter, plus reset entries inresetExternalModelState.chat-page.tsx: both runtime-bootstrap sites computesupportsBuiltinWebFetchfromproviderSupportsBuiltinWebFetchand hydratewebFetchToolsEnabledfrom stored localStorage (default off).chat-adapter.ts:webFetchEnabledForThisTurnnow sources fromwebFetchToolsEnabledinstead oftoolsEnabled.shared-composer.tsx: Fetch pill rendered next to Images, only whensupportsBuiltinWebFetch. Disabled when no model is loaded.provider-capabilities.ts: comment updated to reflect the standalone pill.Backend translation is unchanged: when
enabled_toolsalready contains\"web_fetch\",_stream_anthropicappends theweb_fetch_20250910/web_fetch_20260209tool exactly as before. Coverage already pinned instudio/backend/tests/test_anthropic_web_fetch.pyattest_web_fetch_tool_appended_to_request_body(standalone-only) andtest_web_fetch_combined_with_web_search_and_code_execution(combined).Test plan
npx tsc --noEmitinstudio/frontend/(clean).enabled_tools=[\"web_fetch\"]and the model can read a URL without web_search being enabled.[\"web_search\", \"web_fetch\"]and behaves identically to the pre-change combined path.