pi-better-web-search
A better provider-native web_search tool for pi: raw results, automatic provider fallback, URL analysis, and native DeepSeek Search/Fetch.
Package details
Install pi-better-web-search from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:pi-better-web-search- Package
pi-better-web-search- Version
0.1.9- Published
- Nov 2, 2026
- Downloads
- 889/mo · 514/wk
- Author
- zhexusun10
- License
- MIT
- Types
- extension
- Size
- 208.7 KB
- Dependencies
- 0 dependencies · 3 peers
Pi manifest JSON
{
"image": "https://raw.githubusercontent.com/zhexusun10/pi-better-web-search/main/assets/architecture.svg",
"extensions": [
"./extensions/index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-better-web-search
One tool. Every provider. Raw results.
A pi extension that adds exactly one model-callable tool:
web_search. It uses provider-native search APIs, analyzes URLs when a usable
provider supports it, automatically falls back across authenticated providers,
and returns provider data as-is.
简体中文 · Architecture · Providers · Testing · Source
The npm registry may lag GitHub releases. If the version you want is not yet published to npm, install the tagged GitHub release below instead.
pi install npm:pi-better-web-search
Then start a new pi process, or run /reload in an existing session.
GitHub and local installations also work:
pi install git:github.com/zhexusun10/pi-better-web-search@v0.1.9
pi install ./path/to/pi-better-web-search
Do not load this package together with
pi-web-search. Both register a tool namedweb_search, and the second registration replaces the first.
Requirements: pi 1.3+ and Node.js 22.19+.
What it fixes
pi-better-web-search started as an extraction of the web_search tool from
pi-web-search, then changed the parts that made it unreliable:
- One tool, not two. There is no separate
url_contexttool. URL analysis is the optionalurlsfield onweb_search. - Correct exposure. The tool is hidden when the current model has no compatible search interface and no compatible provider is authenticated. A login or API key for any supported provider is enough to expose it.
- Automatic fallback. If the current model cannot search, or a provider call fails, the extension tries other authenticated provider interfaces until one succeeds.
- Remembered interfaces. The successful search and fetch providers are cached under pi's agent directory. The next call uses the remembered interface when the current model cannot search.
- Fresh discovery. Adding or removing a provider, login, API key, or available model changes the cache fingerprint, so old preferences are not blindly reused.
- Dynamic URL field.
urlsis exposed only when at least one usable provider supports URL analysis.urlscan be used alone or withquery. - Raw results. Provider data is returned in a JSON wrapper without deduplication, URL normalization, junk filtering, re-ranking, summarization, citation-marker insertion, or appended Sources sections. A final character guard applies structured truncation only to oversized output.
- Correct semantics. The tool description is exactly
Search the web or analyze specified URLs.The schema makes clear thaturlsdoes not requirequery. - No system prompt changes. The extension does not add prompt snippets, guidelines, loadout hooks, commands, providers, MCP servers, or other tools.
- Native DeepSeek Search/Fetch. DeepSeek uses the documented
/v1/tools/searchand/v1/tools/fetchendpoints with Bearer auth.
Tool semantics
The runtime schema is logically:
{
query?: string;
urls?: string[];
}
querysearches the web. Outer whitespace is trimmed; search operators are otherwise passed through unchanged. If DeepSeek is selected and the query exceeds 128 Unicode characters, the tool returns an error with the actual length and limit, asking the model to shorten the query and retry. It does not send the query or silently truncate it. Other providers may accept longer queries.urlsanalyzes the listed pages. URLs can be used alone or together withquery. A query plus URLs performs a search and then analyzes the URLs.- At most 20 URLs may be supplied.
- At least one non-empty
queryor one valid URL is required. This rule is enforced at runtime rather than with a top-levelanyOf, for compatibility with all model providers. - The
urlsfield is hidden when no usable interface supports URL analysis. In that configurationqueryis required. - Search and URL analysis may use different providers in the same call.
Invalid input with no usable query or URL, a missing URL-capable provider for
supplied URLs, or a call where every search/URL-only provider fails returns a
tool error (isError: true) instead of silently dropping work. Individual
invalid URLs alongside valid work are reported in the fetched entries.
Provider support
pi-better-web-search natively supports 10 distinct provider classes (plus Anthropic Messages), automatically detecting authenticated credentials and routing each call through native provider protocols:
| # | Provider Class | Web Search | URL Analysis | Native Protocol & Endpoint |
|---|---|---|---|---|
| 1 | DeepSeek | Yes | Yes | Native REST /v1/tools/search & /v1/tools/fetch |
| 2 | Google Gemini | Yes | Yes | google-generative-ai SSE google_search grounding & url_context |
| 3 | Google Antigravity | Yes | Yes | Antigravity enterprise Vertex endpoint & wrapper body |
| 4 | Ollama Cloud | Yes | Yes | Hosted REST /api/web_search & /api/web_fetch (ollama.com) |
| 5 | OpenAI | Yes | No | Official Responses API web_search tool (openai-responses) |
| 6 | Azure OpenAI | Yes | No | Azure Responses API (azure-openai-responses) |
| 7 | OpenAI Codex | Yes | No | Specialized /codex/responses with Codex account tokens & headers |
| 8 | GitHub Copilot | Yes | No | Responses routing with Copilot auth tokens & proxy endpoints |
| 9 | OpenCode | Yes | No | Responses routing with Pi session tracking headers |
| 10 | xAI (Grok) | Yes | No | Grok dialect Responses API (web_search_call.action.sources) |
| * | Anthropic Messages | Yes | No | Messages API web_search_20260209 tool & Claude Code OAuth |
URL analysis is only exposed when at least one usable interface supports it.
For example, a current OpenAI model with only OpenAI credentials gets a
query-only schema, while the same session with DeepSeek, Ollama Cloud, or Google
credentials also gets urls.
Provider detection follows the authenticated model's provider/API:
deepseekprovider (DeepSeek native search/fetch)- Antigravity or Google Generative AI (Google Gemini & Google Antigravity)
- Ollama Cloud (
ollama-cloudprovider or exactollama.comhost) - xAI with
openai-responses(xAI Grok) - OpenAI-compatible Responses APIs (OpenAI, Azure OpenAI, OpenAI Codex, GitHub Copilot, OpenCode)
- Anthropic Messages (Claude)
See Providers for request shapes, auth quirks, and URL support per family.
Automatic selection and fallback
For a search:
- The current conversation model is tried first when its provider family supports web search and neither the provider nor that model is cooling down after a recent failure.
- A remembered successful provider from the preference cache is tried next when the cache fingerprint is current.
- The remaining provider families are tried in this fixed order: DeepSeek → Ollama → Google → OpenAI → xAI → Anthropic.
- Within a family, brand order comes first: DeepSeek models, then OpenAI,
then Claude, then Gemini/Gemma, then other gateway brands. For brands
without named tiers, variants order
flash → base → pro; within a variant, newer versions come first. GPT and Claude keep version-first ordering, with their capability tier as a tiebreaker: OpenAI tiers areluna → terra → sol → astraand Claude tiers arehaiku → sonnet → opus → fable. Ties are deterministic. - Alternate providers are considered before additional models of an already represented provider; siblings are then tried round-robin across families. A provider-wide failure skips that provider, while a model-specific failure (including a model-specific 403) can fall back to a sibling.
The search limit is 12 actual attempts; skipped candidates do not consume it.
URL fetching follows the same idea with a separate limit of 6 actual attempts.
Model ranking applies only to supported APIs: for example, the built-in
opencode-go DeepSeek models use openai-completions, which this extension
cannot search through and does not reinterpret as Responses.
The current model is normally tried first when it supports search, unless its provider or model is in a failure cooldown. The cache does not replace a supported current model; it mainly lets an unsupported current model use a remembered compatible provider immediately on later calls.
If a provider errors, the extension records the attempt and moves to the next
interface. An overlong query rejected by DeepSeek instead returns immediately
without trying another provider or updating its health state. Aborts stop immediately. Each attempt has a timeout (60 seconds by
default, configurable with PI_BETTER_WEB_SEARCH_TIMEOUT_MS). If every attempt
fails, the tool returns an error whose message contains the provider errors.
Provider/model routing remains in details.attempts. For a URL-only call, the
error also reports the per-URL failures. Provider error bodies and model-facing
error text are bounded to prevent an oversized error from filling the context.
Provider-wide failures (auth, balance, network, timeout, malformed request) and
model-specific failures are remembered for the life of the extension process.
On later calls the failed provider or model is moved behind healthy candidates
for search and fetch independently. The cooldown starts at 60 seconds, doubles
when a subsequent attempt after expiry fails, and is capped at 10 minutes.
Concurrent failures during the same cooldown do not extend it; a successful
search or a fetch with at least one successful URL and no provider transport
error clears it. Set
PI_BETTER_WEB_SEARCH_FAILURE_COOLDOWN_MS=0 to disable this behavior. Calls
already in flight may still try the same candidate at the same time.
Cache fingerprint invalidation
The preference cache is stamped with a fingerprint of all authenticated chat
models known to pi, derived from sorted provider/model@baseUrl values. Adding
or removing a provider, login, API key, or available model changes the
fingerprint. A changed fingerprint means:
- the old cached provider is not used as the first choice;
- provider discovery starts again from the current model and family priority;
- the cache is rewritten only after the next successful search/fetch;
- stale, unverified preferences are not carried into the new fingerprint.
A cache entry is only a hint. Credentials are resolved again on every call, and the referenced provider must still be available and support the operation.
Cache
Default path:
~/.pi/agent/better-web-search.json
The path respects pi's PI_CODING_AGENT_DIR setting. Override it directly with:
export PI_BETTER_WEB_SEARCH_CACHE=/path/to/better-web-search.json
Example contents:
{
"version": 1,
"fingerprint": "0123456789abcdef0123456789abcdef",
"search": { "provider": "deepseek", "model": "deepseek-chat" },
"fetch": { "provider": "ollama-cloud", "model": "gpt-oss:120b" }
}
Only provider and model identifiers plus the fingerprint are stored. API keys, OAuth tokens, and other credentials are never written to this file.
To clear the remembered preferences, delete the file:
rm ~/.pi/agent/better-web-search.json
A missing, unreadable, or corrupt cache is treated as empty and never breaks a tool call.
Raw provider results
Provider data is returned in a JSON wrapper without cleanup. A search looks like this:
{
"query": "the query",
"search": {
"answer": "provider text when available",
"results": "the provider's raw results",
"citations": "the provider's raw citations",
"searchQueries": ["provider query"],
"groundingMetadata": "raw provider metadata"
},
"fetched": [
{
"url": "https://example.com",
"data": "the provider's raw fetch payload"
}
]
}
- Duplicate URLs are not deduplicated.
- Tracking parameters and junk-looking URLs are not normalized or filtered.
- Results are not re-ranked or summarized.
- Citation markers are not inserted, and no "Sources" or "Additional results" sections are appended.
- Redirects are not resolved and sources are not derived.
- Unknown provider fields are preserved when the result fits the size guard.
A final output-size guard limits model-facing results and errors to 96,000 Unicode code points by default. Normal-sized results retain their original fields and formatting. If indentation alone exceeds the limit, compact JSON is returned without dropping data or setting the truncation flag.
When even compact JSON is too large, the guard allocates space across sibling
fields and array entries, redistributing unused space. Recognized link and
identity fields (url, uri, href, link, title, name, id, and common
aliases) stay complete; if they cannot fit, the associated entry or branch is
omitted instead. Standalone URLs and entries in link/citation arrays also stay
whole. When a text cut would split a recognizable embedded URL token (such as
https://, mailto:, or www.), the incomplete token is omitted. Relative
links or bare domains in unrecognized fields cannot always be identified. Other long strings may be shortened, and fields or trailing array
entries omitted; results are not re-ranked or summarized.
The JSON portion remains valid, followed by a single [truncated] line. No
output-size truncation metadata or per-string markers are added to the JSON.
Because of the trailing line, the complete text is not itself valid JSON.
details.truncated is also set to true when data is actually removed.
Change the limit before starting pi:
export PI_BETTER_WEB_SEARCH_MAX_CHARS=100000
In PowerShell, use $env:PI_BETTER_WEB_SEARCH_MAX_CHARS = "100000". The limit
is in code points, not tokens, and does not adapt to the current model's
context window. The marker counts toward the limit. If truncation is needed and
the limit is 11–13 characters, only [truncated] is returned; below 11
characters the marker does not fit and only a minimal JSON value is returned.
details.truncated remains true in both cases. There is no continuation or
pagination cache: retry individual URLs or raise the limit if more content is
needed.
Exposure rules
web_search is exposed when either:
- the current conversation model supports a provider-native search interface; or
- at least one authenticated available model maps to a supported provider family.
For an unsupported current model with no compatible login or API key, the tool
stays hidden (removed from the active tool set). The extension never calls an
unauthenticated provider. When the set of available providers or credentials
changes, the extension re-evaluates and re-registers the same web_search name
with the appropriate schema variant.
DeepSeek native Search/Fetch API
This package supports DeepSeek's native search and fetch APIs. The older project's Anthropic-compatible DeepSeek chat route is replaced by this native API; DeepSeek models are routed to it even when their underlying chat API is Anthropic Messages compatible.
POST https://api.deepseek.com/v1/tools/search
POST https://api.deepseek.com/v1/tools/fetch
Authorization: Bearer <DeepSeek API Key>
Content-Type: application/json
Search uses query, count: 10, and need_content: true. DeepSeek limits
query to 128 Unicode characters. Longer queries return an error before any
DeepSeek request is sent; the model should shorten the query and retry. For
example: Search query is too long (129 Unicode characters). The maximum is 128 Unicode characters. Shorten the query and retry. The raw results array
is returned unchanged,
including rerank_score, title, url,
snippet, summary, content, site_name, site_icon, date, authority,
and any unknown fields.
Fetch sends { "url": "..." } and preserves the raw { title, content }
response. URLs must be at most 8192 characters, use HTTP/HTTPS, include a host,
and omit credentials.
DeepSeek errors use the OpenAI-compatible error shape. Search errors and global
fetch errors switch providers; 403 and 404 fetch failures are reported
against the individual URL so the orchestrator can retry only that URL elsewhere.
See Providers for the full request, response, and error details.
Security and privacy
- Credentials are resolved through pi's model registry on each call. The extension does not store API keys or OAuth tokens.
- The cache stores only provider/model identifiers and a fingerprint.
- Search queries and requested URLs are sent to the selected provider's API.
- The extension has no telemetry, analytics, or third-party service of its own.
- A call may use an authenticated provider other than the current conversation model, which can incur usage on that provider.
- Provider error text and tool results can contain request identifiers or page content. Redact them before sharing logs or session exports.
See SECURITY.md for reporting vulnerabilities.
Verified
- Unit and integration tests covering exposure, schema variants, provider request shapes, SSE parsing, raw-result preservation, fallback, cache invalidation, URL handling, truncation, and abort behavior.
- Real DeepSeek Search/Fetch run with a real API key: 10 raw search results, summaries and content, valid fetch payloads for normal pages, and documented errors for 404 and invalid URLs.
- Real pi process end-to-end run: a mock unsupported current model plus
DeepSeek credentials exposed
web_search, called it, auto-switched to the native DeepSeek API, returned 10 raw results, and answered with the tool result attached. - Other provider families are covered by unit/integration tests with mocked requests and streams; live credentials for every provider are not required.
The opt-in live DeepSeek test is reproducible from a repository checkout:
DEEPSEEK_API_KEY=... npm run test:live:deepseek
Compatibility
- pi 1.3+. The extension uses the current extension tool and model registry APIs.
- Node.js 22.19+. Pi loads the TypeScript extension directly; there is no compile step.
- Pi provides
@earendil-works/pi-coding-agent,@earendil-works/pi-ai, andtypeboxas host packages. They are declared as peer dependencies and are not bundled. - Upstream provider API changes may require adapter updates. Unknown provider fields are preserved rather than rewritten.
Development
npm ci --ignore-scripts
npm run check
npm test
npm run verify:package
npm run build
npm run build runs the checks and tests, verifies the npm file list, and packs
dist/pi-better-web-search-0.1.9.tgz. Nothing is published automatically.
The test suite uses plain .mjs files that import the .ts sources directly.
It mocks globalThis.fetch, uses a temporary cache file, and exercises
exposure, fallback, cache invalidation, raw-result preservation, truncation,
URL handling, and provider switching.
See Testing for live-provider tests and the real pi end-to-end procedure. See Publishing for the release checklist.
Contributing
Use English for issues, pull requests, and primary documentation. Chinese translations are welcome. Read CONTRIBUTING.md before changing provider behavior, exposure rules, or output handling.
The extension keeps one invariant above all others: trust the provider's result. Do not add cleanup that rewrites, filters, re-ranks, summarizes, or reconstructs provider data.
License
MIT. An independent community package, not an official Pi product.
The web_search extraction is derived from the MIT-licensed
pi-web-search project. The
repository structure and documentation conventions are inspired by
pi-trim.