# pi-web-search Unified web search and fetch for Pi. The main model only ever sees two tools — `web_search` and `web_fetch` — provider selection happens inside the extension. - `web_search(query, max_results?, site?, published_after?, published_before?, depth?)` routes each query to Tavily, Exa, or Keenable using the session's current main model (`ctx.model` + `ctx.modelRegistry.complete`), guided by per-provider rules. Results include `details.provider` / `details.routedBy` for debugging. - `web_fetch(urls, max_characters?)` always reads pages via Exa (`web_fetch_exa`), degrading silently to Keenable then Tavily. ## Providers (`clients/`) Plain `fetch` calls — no MCP adapter, no upstream extension wrappers: - `clients/tavily.ts` — `POST api.tavily.com/search|extract`, Bearer key. Requires `TAVILY_API_KEY`. - `clients/keenable.ts` — `api.keenable.ai`, keyless `/public` endpoints by default, `KEENABLE_API_KEY` raises rate limits. Adapted from `@keenable/pi-search`. - `clients/exa.ts` — JSON-RPC over `mcp.exa.ai/mcp` (Streamable HTTP): `initialize` once, cache `mcp-session-id`, `tools/call` per request, re-init on session expiry. `EXA_API_KEY` optional (`x-api-key` header). Keys are loaded from `~/.config/my-pi/search.env` by `extensions/search-config.ts` (written by `search_config.sh`); explicit environment variables win. ## Routing `router.ts` asks the current model to answer with exactly one provider name, given a prompt containing the per-provider strengths and the query's requested filters. Decisions are cached per session (LRU, 32 entries). Failure paths are deterministic: an unroutable answer or a dead provider falls back across the remaining available providers — never a heuristic choice over the model.