If your pipeline prints an outlet name next to a citation, no grounded-search API is giving you that name. You are deriving it from the URL domain, whether you meant to or not. I went into a provider reassessment believing one of the major grounding APIs supplied per-article outlet names. None of them do. Here is what the response schemas actually contain, verified against recorded API responses rather than docs alone.
Context: a two-phase news pipeline. Phase A calls a search-capable model and keeps its prose plus citation metadata; Phase B extracts {headline, why_it_matters, source_name} and renders [source_name](url) publicly. When Phase B has no outlet in its input it invents one, so the metadata question is load-bearing.
Gemini's web.title is a bare domain, not a headline and not a display name
The widespread assumption is that Google's grounding index hands back recognizable outlets. It hands back domain strings. Real groundingChunks from gemini-3.1-pro-preview via generativelanguage v1alpha, tools=[{'googleSearch': {}}], nine chunks, web keys exactly ['title', 'uri']:
mdcounties.org wikipedia.org bnnbloomberg.ca
americandeposits.com fool.com nbcpalmsprings.com
smartsolar.com.tr metatradingclub.com post-gazette.comGoogle's own docs agree: the Vertex example shows "title": "weatherbug.com", and the newer Interactions-API url_citation shows "title": "aljazeera.com". Two implications. First, web.title is informationally identical to urlparse(url).netloc, so switching to Gemini to "get outlet names" is a no-op. Second, note the actual domains: a grounding index surfacing smartsolar.com.tr and metatradingclub.com for macroeconomic queries is not a source-quality upgrade over anything.
Beware a second trap here: with several chunks from one outlet, web.title cannot discriminate between them at all, so story-to-chunk matching must use the resolved URL slug.
Anthropic returns far more than it cites, and litellm already exposes it
Anthropic's web_search_20250305 puts search results and citations in different places, and reading only the citations throws away most of the fetch. From one recorded /v1/messages response with four searches:
| Where | Count | Distinct domains |
|---|---|---|
web_search_tool_result blocks | 31 results | 23 |
citations on text blocks | 11 | 7 |
The result pool was overwhelmingly primary sources (federalreserve.gov, bls.gov, cnbc.com, cnn.com, forbes.com, fortune.com, nerdwallet.com); the model cited a narrow slice of it. That reframes the usual complaint. When your citations land on a low-quality aggregator, the index may have offered better pages and the model simply did not cite them.
Field shapes, which differ between the two:
# web_search_tool_result -> content[] entries
{'type', 'url', 'title', 'encrypted_content', 'page_age'}
# citations on a text block
{'type', 'url', 'title', 'encrypted_index', 'cited_text'}page_age exists only on search results, as "June 17, 2026" or "1 week ago" or null. If you fetch pages yourself just to date them, you may already have a date for free.
On litellm 1.93.0 both surfaces are available without touching the raw SDK:
psf = resp.choices[0].message.provider_specific_fields
citations = psf['citations'] # nested: one list per text block, flatten it
results = psf['web_search_results'] # the full web_search_tool_result blocksSee litellm/llms/anthropic/chat/transformation.py (collection near line 2009, assignment near 2238) and litellm/llms/anthropic/chat/handler.py (streaming accumulation near 858).
Domain filtering is incompatible across the three vendors
This is the actual lever on source quality, and no two vendors agree on it:
| Provider | Allowlist | Blocklist | Both in one request |
|---|---|---|---|
Anthropic web_search | allowed_domains | blocked_domains | No, HTTP 400 |
Gemini google_search | none | exclude_domains | n/a, no allowlist exists |
OpenAI Responses web_search | filters.allowed_domains (100 max) | filters.blocked_domains (100 max) | Yes |
Anthropic, verbatim: "Provide allowed_domains or blocked_domains, not both. If a request includes both, the API returns a 400 error." Also watch for error code request_too_large, which the docs say fires "typically because of a long domain filter list" - your allowlist has an undocumented size ceiling.
This bites when your blocklist is dynamic (a growing table of banned domains) and your allowlist is static (a curated set of trusted outlets). On Anthropic you must pick one and enforce the other yourself after the fact. OpenAI's own example passes both inside one filters object.
The OpenAI path has its own trap: "domain filtering is only available in the Responses API with the web_search tool" - not Chat Completions search models, not web_search_preview. litellm has a Responses-to-Chat-Completions bridge that derives web_search_options from a Responses web_search tool (litellm/responses/litellm_completion_transformation/transformation.py, around lines 170-208 and 1264-1322). That derivation produces the Chat Completions shape, which cannot carry filters. So a request that looks like it has an allowlist can quietly lose it in translation. Verify the outgoing body, not the call site.
Brave News is the one option with a real publisher field
If you actually need a display name rather than a domain, GET|POST https://api.search.brave.com/res/v1/news/search carries one:
{"results": [{
"title": "...", "url": "...", "description": "...",
"age": "2 hours ago", "page_age": "2026-01-15T14:30:00",
"meta_url": {"netloc": "news.example.com", "hostname": "..."},
"profile": {"name": "Example Outlet", "long_name": "..."}
}]}profile.name is the publisher. Note it is profile.name, not a top-level source field - several third-party write-ups get this wrong. page_age is an ISO datetime rather than Anthropic's freeform string. There is no include_domains parameter, but inline Goggles give you an allowlist with no registration step: $discard\n$site=reuters.com\n$site=apnews.com, blocklist form $discard,site=example.com, up to three goggles per request. Third-party reporting holds that Brave backs Anthropic's web_search, which would mean querying Brave directly buys you metadata and controls over the same index rather than a different one; Anthropic has never confirmed a backend in its docs.
The takeaway for pipeline design
Separate two problems that look like one. Naming is "the label next to the link reads like a domain" and is fixed by a domain-to-display-name map or by a provider field like profile.name; it never requires changing search provider. Precision is "the cited page should not have been cited at all" and is only fixed at search time with an allowlist, which is exactly the control that varies most across vendors. Conflating them sends you on a provider migration that cannot fix the symptom you actually see.
One dead end worth naming: the Bing Web Search API is not an escape hatch. It was retired on 2025-08-11 with instances decommissioned, and the Azure AI Agents "Grounding with Bing Search" replacement is not a standalone search API.