# 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 a `tavily` credential. - `clients/keenable.ts` — `api.keenable.ai`, keyless `/public` endpoints by default, a `keenable` credential 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. An `exa` credential is optional (`x-api-key` header). Keys live in Pi's `auth.json` as one `api_key` credential per provider (`tavily`, `exa`, `keenable`), read by `auth.ts` with upstream resolution semantics — literal values, `$VAR`/`${VAR}` interpolation, and `!command` indirection (e.g. `"!op read op://Vault/Tavily/key"`). `configure.sh search` writes them (atomically, mode 600, preserving all other entries); process environment variables are not read directly — use `"key": "$TAVILY_API_KEY"` to indirect through env. `KEENABLE_API_URL` stays env-only, or can be stored in the keenable credential's `env` map. ## 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.