[front] feat: provider-native web search behind a feature flag - #32242
Draft
Nils-Fedrigo wants to merge 1 commit into
Draft
[front] feat: provider-native web search behind a feature flag#32242Nils-Fedrigo wants to merge 1 commit into
Nils-Fedrigo wants to merge 1 commit into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds the
provider_native_web_searchfeature flag (dust_only). When it is on and the request's host is Anthropic or OpenAI Responses, Dust's ownwebsearchtool is dropped from the request and the provider's server-side web search tool is sent instead. The point is to measure the provider path against ours on real traffic — latency, answer quality, cost.Scope is deliberately narrow:
webbrowser(Firecrawl/Exa) is untouched on both providers. Anthropic has a nativeweb_fetchand OpenAI Responses has no fetch equivalent, so swapping browse too would make agent capabilities diverge by provider and would lose screenshots, link extraction, summarization and the tool-output file offload.isNativeWebSearchEnabledkeys onANTHROPIC_HOST/OPENAI_RESPONSES_HOST, which excludesAGENT_PLATFORM_HOST(Vertex) even though it shares Anthropic's provider id and input converter — server-tool support there is unverified, and the regional endpoints exist for workspaces that don't want queries leaving the region. Adding a host is the single switch to flip once verified.How it is wired, mirroring the existing tool-search precedent end to end:
run_model.tsderives the flag and filters the tool out of the action list, then it ridesLLMStreamParameters→buildConfig→InputConfig.nativeWebSearchEnabled→ the per-SDK input converters, which prepend the server tool.Two consequences worth calling out for review:
:cite[REF]chips can't work here — refs are keyed on a persistedAgentMCPActionrow and a server-side search produces none. So provider citation metadata is rendered into the answer text as([title](url)). On Anthropic thecitations_deltaarrives right after the span it cites, so the link is emitted as a delta and appended to the accumulator, keeping the final persisted text byte-identical to what streamed. OpenAI reports offsets into the finished text, so links are spliced byend_indexatoutput_item.done(the authoritative, persisted event) with matching deltas for the live view. Consecutive repeats of the same source are suppressed.citationMetaPromptstops firing for a web-search-only agent, since it would then have no real refs to cite.webSearchProvidermetadata, theset_web_providerspoke plugin), theglobal_disable_firecrawlkill switch andgetWebsearchNumResultsbudgeting also don't apply. Query volume, result counts and provider-side errors are covered by logs plus allm_native_web_search.requestscounter instead.Replay needed real work: Anthropic's
server_tool_use+web_search_tool_resultblocks were previously dropped, and the tool-search passthrough parser actively rejected aweb_searchblock and logged it as unparseable — which would have broken interleaved thinking signatures. Both families now have their own zod schemas and replay rules behind aparseAnthropicServerToolBlockdispatcher, with a matching sanitizer that strips a family's blocks when its tool is absent from the request (auxiliary calls such as title generation). OpenAI'sweb_search_callitems are replayed for the same reason the tool-search items are: the API rejects a reasoning item whose following item was dropped, and a search call routinely sits between a reasoning item and the message.Refs 400652.
Tests
Unit tests only — every surface touched is a pure converter or predicate.
npx tsgo --noEmitis clean; 438 tests pass with 0 failures.New coverage: server-tool ordering and the force-call/never-defer rules on both providers; the passthrough schemas and replay sanitation (including that the common path returns its input array by identity, so prompt-cache bytes are unchanged); the name-aware
server_tool_usebranch, with an explicit guard against it swallowing tool search; citation splicing on both the streaming and non-streaming paths;isDustWebsearchToolacross both internal servers that mountwebsearch, and that it leaveswebbrowserand same-named third-party tools alone; and the host gate's no-op guarantee for every other host.Not yet verified, and the one thing to check before this is opened for review: that
tool_choice: {type:"none"}actually suppresses the server tool. See Risk.Risk
Low with the flag off — the tool array, prompt and token-count estimate are all unchanged, and the two shared passthrough parsers keep their existing behavior (they only stopped owning their own "unparseable" warning, which moved to the dispatcher).
Three things to know before rolling past
dust_only:tool_choice: "none"is an unverified assumption. The native tool is deliberately sent on every step, including the forced final generation, so the tools prefix stays byte-stable and the prompt cache is never invalidated mid-run. Suppression on the last step therefore rests entirely ontool_choice— documented as "the model will not be allowed to use tools" with no server-tool carve-out, and server tools are invoked through the same mechanism (the model emits aserver_tool_useblock). Needs a live check: drive a run to its last step with a search-hungry prompt and confirm noserver_tool_useblock appears. If one does, the fallback is one line — drop the tool whendisableToolUseis set — because the replay sanitizer is already keyed on whether the tool is in the request. Dropping it unconditionally instead would cost a cache miss on every last step, and a cache breakpoint can't rescue that: the tools array sits ahead of the system prompt and messages in the cached prefix.usage.server_tool_use— on the very object the usage converter already receives — butTokenUsageContenthas no slot for it, so nothing reachesRunUsageTypeor credit attribution. This is a cost shift rather than new cost (Dust'swebsearchalready pays Firecrawl/Exa and is metered viatoolCostCategory: "basic"), but it moves from a metered path to an unmetered one. Mitigated for now bymax_uses: 8on the tool plus per-search logging and a StatsD counter; aserverToolRequestsfield onTokenUsageContentshould land before broader rollout.dust_only; wants an explicit decision beforeask_owner.Also: don't flip the flag off mid-conversation. Anthropic is safe (the sanitizer strips the now-unreplayable blocks), but OpenAI replays
web_search_callitems unconditionally, and stripping them would re-expose the dropped-reasoning-item problem.Rollback is flag-off, with one prompt-cache miss on the first request after the flip.
Deploy Plan
Nothing special — no migration, no config. Ship, then enable
provider_native_web_searchon a Dust workspace and run the verification in Tests/Risk before enabling anywhere else.