vllm-project / vllm-project/agentic-api
[RFC] Pluggable Web Search Providers and Typed Result Normalization
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 289
- Forks
- 74
- Avg merge
- 1d 17h
- Merged PRs (30d)
- 93
Description
Problem statement / motivation
Summary
The gateway-owned web_search_preview tool is hardwired to You.com. This RFC proposes making the search
backend selectable through a typed WebSearchProviderKind config enum, adding Brave Search as the first
alternative provider, and — as a prerequisite — replacing the current untyped pass-through of provider JSON with
a typed normalization contract (WebSearchResult). The work is split into two PRs: a behavior-preserving refactor,
then the Brave provider with configuration, tests, and docs. SearXNG (keyless, self-hosted) and Tavily follow as
separate PRs against the same contract.
The design keeps the existing WebSearchProvider trait private, keeps the public WebSearchHandler constructors
unchanged, and adds only additive configuration. When no provider is selected, behavior is identical to today.
Motivation
web_search_preview is executed by the gateway (ARCHITECTURE.md, docs/design/tool-framework.md), which is a
real differentiator for a self-hosted, OpenAI-compatible stack. Today it can only be enabled by setting
YOU_API_KEY and YOU_API_BASE_URL
(crates/agentic-server-core/src/tool/web_search.rs:20-21, docs/deploying/kubernetes.md:419-420).
PR #85 (@franciscojavierarceo) anticipated this:
Splits the You.com HTTP integration behind an internal
WebSearchProviderabstraction so additional search
providers can be added without changing the OpenAI tool adapter.
The abstraction exists (web_search.rs:253-260), but nothing else was ever plugged into it. Practical consequences:
- Barrier to entry. As of this writing, You.com's Search API is a paid product without a self-serve free
developer tier. Brave Search offers a free plan (≈2,000 queries/month), Tavily ≈1,000/month. A personal or
evaluation deployment of Agentic API cannot exerciseweb_searchat all without a commercial contract. - Air-gapped / private-cloud deployments cannot use
web_searchbecause the only supported backend is a public
SaaS endpoint. A SearXNG instance (or an internal search API behind the same contract) is the realistic answer for
those environments. - Vendor coupling in the tool contract. The function schema shown to the model, the argument struct, and the
model-facing tool output are all You.com-shaped (details below), so even an embedder who implements their own
executor inherits You.com semantics.
Goals
- Select the search backend by configuration;
youremains the default. - Add Brave Search as the first alternative provider.
- Define a typed, provider-neutral result contract so no provider builds
serde_json::json!blobs. - Keep every existing public constructor, env var, config key, and public output item working unchanged.
- Keep the provider trait private; extension for embedders remains
GatewayExecutorRegistration::WebSearch.
Non-goals
- Pagination (not exposed by the function schema or
search_context_size). - Automatic retries on HTTP 429 (see Concurrency and rate limits).
- A public plugin ABI for providers.
- Query rewriting with
site:operators to emulate domain filters (possible later optimization; Phase 1 post-filters). - Cargo features per provider. All providers compile into the single binary.
Proposed solution
Implementation plan
Phrase 1 — refactor and typed contract (no configuration or public API change)
- Split
crates/agentic-server-core/src/tool/web_search.rs(807 lines) into
tool/web_search/{mod.rs, args.rs, you.rs}:mod.rsholds the handler, trait, typed result, and public output
mapping;args.rsholdsWebSearchArguments,Freshness, and the domain post-filter;you.rsholds the You.com
provider and request shaping. - Introduce
WebSearchResult,WebSearchProviderMetadata, typedWebSearchProviderResponse; the You.com provider
deserializes into them. - Add
WebSearchProviderKindwith onlyYou,WebSearchProviderConfig.provider, and
WebSearchHandler::from_config; wireToolExecutors::from_config. - Add a test that serializes the handler output for the existing mock fixture
(tests/web_search_tool_test.rs:291) and asserts it byte-for-byte; add a redacted real You.com response as a
fixture so the mapping is checked against actual upstream shape, not only the hand-written mock. - Existing unit and integration tests pass unmodified.
Phrase 2 — Brave Search provider
tool/web_search/brave.rs(~300 lines): request shaping, minimal response structs, mapping, domain post-filter
application, count clamp, freshness rendering,max_concurrent_requests() = 1.WebSearchProviderKind::Brave; file/env/generated-config plumbing inconfig_file.rsandmain.rs.- Tests (see below), docs,
CHANGELOG.md.
Roadmap (separate RFC-lite issues, same contract)
- SearXNG — keyless, self-hosted,
GET /search?format=json&categories=general,news&time_range=…. This is the
air-gapped story and the proof that the abstraction handles a provider with no credential and heterogeneous
result quality. - Tavily — native
include_domains/exclude_domains, LLM-orientedcontent; news is a separate
topic=newsrequest, which exercises the "one request per query" policy differently. - DuckDuckGo is not planned: there is no official web search API, and scraping is out of scope for this project.
Testing
All provider tests run against a local Axum mock bound to 127.0.0.1:0, following spawn_mock_you_with_response
(tests/web_search_tool_test.rs:291-322). No external network access in CI. No new replay cassettes are needed.
Phrase 2 coverage:
200with mixedweb+news→ both sections mapped;sourcesderived from both.- Empty results → empty sections, no error.
401/403→ failedweb_search_call, message names the key env var, credential absent from output.429withRetry-After→ failed call, no retry (single captured request), header value in message.- Query-parameter assertions:
q,count(clamped from 50 → 20),freshness=pw,country,search_lang,
safesearch,result_filter=web,news;X-Subscription-Tokenpresent;Accept-Encodingabsent. - Domain post-filter: allowlist, blocklist, subdomain match, label-boundary negative case, uppercase host, host with
trailing dot, unparsable URL dropped. - Concurrency ceiling: reuse the
ConcurrencyTrackingProviderpattern to assertmax_active == 1for Brave under a
five-query batch even when the gateway limit is 5. - Config:
AGENTIC_WEB_SEARCH_PROVIDER=Braveparses case-insensitively; unknown value fails startup;
[web_search] providerround-trips throughFileConfig; generated file containsprovider = "you". - Mock hygiene: use
try_send(or hold the receiver for the test's lifetime) instead of
tx.send(..).await.unwrap()inside handlers (tests/web_search_tool_test.rs:311), to avoid the class of hang fixed
inCHANGELOG.md:51.
Gates: cargo test, cargo clippy --all-targets -- -D warnings (pedantic is warn in Cargo.toml and is
promoted by -D warnings), cargo fmt -- --check, pre-commit run --all-files.
Additional context
Open questions for maintainers
page_agevspublished_at. Keepingpage_agemakes PR 1 byte-identical for You.com;published_atis
provider-neutral. We proposepublished_atand would like a decision before PR 1.- Default base URL for You.com. Adding
provider.default_base_url()for You.com means a config that fails
today (YOU_API_KEYset,YOU_API_BASE_URLunset) starts working. Acceptable? If so, which host is canonical —
README.md:219sayshttps://api.ydc-index.io,docs/deploying/kubernetes.md:420sayshttps://ydc-index.io. - Base URL override for non-You providers: a generic
AGENTIC_WEB_SEARCH_BASE_URL, or per-provider
BRAVE_API_BASE_URLmirroringYOU_API_BASE_URL? We lean generic, withYOU_API_BASE_URLretained as-is. - Location of
WebSearchProviderKind:config.rs(proposed, alongsideSqliteTempStore) or
tool/web_search/mod.rswith a re-export? - Should the provider name be surfaced to the model in
metadata[](proposed) or kept out of the model-facing
output?
References
- PR #85 —
feat: add web search gateway tool(commitbf5fe8b). docs/design/tool-framework.md— tool ownership model;web_searchis gateway-owned.docs/design/messages-gateway-tool-classification.md— Claude CodeWebSearchaliasing intoweb_search.crates/agentic-server-core/tests/cassettes/README.md— recorder workflow (no new cassettes required here).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with crates/agentic-server-core/src/tool/web_search.rs, its referenced tests/web_search_tool_test.rs, and the implementation plan. Review the five open questions before proposing changes, then use the existing mock fixture and listed cargo test, clippy, and fmt gates to validate the behavior-preserving refactor and provider configuration.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- backend, backend-api-design
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100