omp packages

@xprnvd/pi-web-access0.46.0

Web search, URL fetching, GitHub repo cloning, PDF extraction, YouTube video understanding, and local video analysis for Pi coding agent. Supports OpenAI, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Crawl4AI, Jina, SE

GitHub Actions Published 7 hours ago MIT ~251 downloads/mo

Untested on omp

This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.

Did it work for you?

Result

One report per person. A new report replaces your old one.

Reports 0

No reports yet.

Readme

pi-web-access

Pi Web Access

Web search, content extraction, and video understanding for Pi agent. OpenAI/Codex search, zero-config Exa search, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, AnySearch, XCrawl, Valyu, xAI/Grok, Mistral, Bright Data SERP, SerpBase, SerpApi, Linkup, Serper, explicit-only Serply, SearchApi, self-hosted SearXNG, degoog metasearch, self-hosted Crawl4AI extraction, keyless DuckDuckGo, keyless Keenable, optional browser-cookie Gemini Web, Kimi Code Plan search, or bring your own API keys.

npm version License: MIT Platform

https://github.com/user-attachments/assets/cac6a17a-1eeb-4dde-9818-cdf85d8ea98f

What's new in 0.45

https://github.com/user-attachments/assets/d9296f9b-9769-4ca0-8c27-ef1a766afcdc

The agent can now pull specific values off a page, like a price or a rate limit, instead of reading the whole thing. Numbers and text come back with the line they were found on, and anything the model isn't sure of is marked as a guess or as not on the page. Code mode scripts get those values as typed data, so one script can search and compare numbers across the top results. Search can also move on to the next provider when one comes back empty or off-topic, and the web tools take about 60% fewer tokens on every request. See the changelog for everything in this release.

Why Pi Web Access

Zero Config — Works out of the box with Exa MCP (no API key needed). If you're signed into Pi with a Codex subscription, OpenAI web search can reuse that auth. An active Kimi Code Plan signed in through /login kimi-coding enables explicit Kimi search without a separate Open Platform key. Add API keys or endpoints for OpenAI, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, Valyu, SerpBase, SerpApi, Serper, explicit-only Serply, SearchApi, Exa, Perplexity, Gemini API, or Mistral for more control; configure a self-hosted SearXNG endpoint or a degoog metasearch instance (public https://degoog.org by default) for private search; or add a Tabstack key for cited research answers; or opt into browser-cookie access for Gemini Web.

Video Understanding — Point it at a YouTube video or local screen recording and ask questions about what's on screen. Full transcripts, visual descriptions, and frame extraction at exact timestamps.

Smart Fallbacks — Every capability has a fallback chain. Search tries configured SearXNG first for local/private search. When the active Pi model uses a ChatGPT subscription (openai-codex, or openai with Sign in with ChatGPT), it then tries subscription-backed OpenAI search. Otherwise it tries Exa before OpenAI, then Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, Perplexity, Gemini API, and Gemini Web when browser cookies are enabled. YouTube tries Gemini Web when enabled, then API, then Perplexity. Blocked pages try configured self-hosted Firecrawl or Crawl4AI first. Third-party hosted page fetchers require explicit fetchRouting.allowRemoteHostedProviders opt-in for remote HTTP(S) targets.

GitHub Cloning — GitHub URLs are cloned locally instead of scraped. The agent gets real file contents and a local path to explore, not rendered HTML.

Other Agents — The search and fetch tools also run as a local MCP server for Claude Code, Codex, Cursor, and other MCP clients. See Use from other agents (MCP).

Install

pi install npm:pi-web-access

Works immediately with no API keys — Exa MCP provides zero-config search. If Pi has Codex auth from /login, OpenAI search can also work without a separate key. For more providers or direct API access, add keys to ~/.pi/agent/web-search.json:

{
  "openaiApiKey": "sk-...",
  "braveApiKey": "BSA_...",
  "exaApiKey": "exa-...",
  "tinyfishApiKey": "sk-tinyfish-...",
  "search1apiApiKey": "...",
  "searchinfinityApiKey": "...",
  "queritApiKey": "...",
  "jinaApiKey": "jina_...",
  "bochaApiKey": "sk-...",
  "perplexityApiKey": "pplx-...",
  "geminiApiKey": "AIza...",
  "mistralApiKey": "..."
}

In auto mode (default), web_search tries a configured SearXNG endpoint first for local/private search. When the active Pi model uses a ChatGPT subscription (openai-codex, or openai with Sign in with ChatGPT), it then tries subscription-backed OpenAI search. Otherwise it tries Exa (direct API if keyed, MCP if not) before OpenAI, then Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Perplexity, Gemini API, and Gemini Web when browser-cookie access is enabled. Exa handles search; curator summary drafts are generated separately by the configured Pi summary model, defaulting to Claude Haiku, Codex Luna, Codex Terra, Gemini 3.6 Flash, GPT-5 mini, then DeepSeek V4 Flash when available. Slow summary drafts fall back to a deterministic result summary after a bounded deadline.

For a third-party Responses-compatible gateway, set openaiResponsesUrl to its full Responses endpoint, or opt into reusing the selected Pi provider's base URL as described below. Without either opt-in, Pi credentials with a custom baseUrl are refused before sending a request, and availability checks mark OpenAI unavailable without blocking other providers. The auth-resolved base URL takes precedence over the model's; gateway Responses/web_search support is not inferred. An explicit endpoint overrides this guard, including an explicit official endpoint. Existing Codex subscription endpoint selection is preserved when provider URL reuse is not active.

Reuse the selected Pi provider URL

Set openaiUseProviderBaseUrl: true to derive the search endpoint from the Pi provider whose credentials were selected. This avoids maintaining the same gateway address in both models.json and web-search.json. The default is false.

{
  "openaiSearchProviders": ["my-openai-provider"],
  "openaiUseProviderBaseUrl": true
}

If openaiResponsesUrl is supplied, this switch is ignored and the existing explicit-endpoint/auth rules apply. Remove that field to use the provider address. Otherwise the switch must be a boolean. The selected provider's auth-resolved baseUrl takes precedence over its model's baseUrl; credentials and URL are resolved together for each search. Candidates with missing or invalid resolved base URLs are skipped in configured order, keeping each candidate's credentials bound to its own URL. Reload Pi after changing its provider configuration.

Only absolute HTTP(S) URLs are accepted. Preserve the origin, port, prefix, and query parameters, and complete the Responses path as follows:

Provider base URL Derived Responses endpoint
https://gateway.example.com https://gateway.example.com/v1/responses
https://gateway.example.com/v1 https://gateway.example.com/v1/responses
https://gateway.example.com/team/v1/ https://gateway.example.com/team/v1/responses
https://gateway.example.com/v1/responses https://gateway.example.com/v1/responses

Codex auth with the official https://chatgpt.com/backend-api base uses /backend-api/codex/responses. When reusing a custom provider URL with Codex credentials, retain its destination and required account headers rather than redirecting to the official host. If openaiUseAlphaSearch is also true, replace the derived /responses suffix with /alpha/search.

If no candidate has usable Pi credentials and a valid provider base URL, this mode fails closed and marks OpenAI unavailable; it does not fall back to the official endpoint or a standalone API key. API-key-only configurations can use openaiResponsesUrl instead. This switch applies to independent OpenAI provider selection; searchRouting.useCurrentModel retains its existing official-endpoint selection and eligibility rules.

Azure OpenAI web search (Bing grounding)

Azure OpenAI supports the Responses API's built-in web_search tool, backed by Grounding with Bing Search / Bing Custom Search, through the openai search provider.

For a standalone Azure API key, add this to web-search.json:

{
  "provider": "openai",
  "openaiSearchProviders": [],
  "openaiResponsesUrl": "https://YOUR-RESOURCE.openai.azure.com/openai/v1/responses",
  "openaiApiKey": "$AZURE_OPENAI_API_KEY",
  "openaiSearchModel": "YOUR-DEPLOYMENT-NAME"
}

The empty provider list prevents an existing Pi OpenAI/Codex login from taking precedence over the Azure key. Without it, a Codex login keeps searching through ChatGPT Codex and never sends its token to Azure, while a Pi OpenAI login's credential is sent to the Azure endpoint in place of the Azure key. Set AZURE_OPENAI_API_KEY in the environment; do not put the key itself in shared configuration. An explicitly configured Microsoft Entra JWT access token is also supported and is sent as Bearer auth; raw Azure API keys use the api-key header. The extension does not fetch or renew Entra tokens.

To reuse a configured Pi Azure provider instead, set openaiSearchProviders to its provider ID and openaiUseProviderBaseUrl: true, with no openaiResponsesUrl. Its base URL must already end in /openai/v1 or /openai/v1/responses. Use openaiSearchModel to specify the Azure deployment name if it differs from the selected model ID. Do not enable openaiUseAlphaSearch: Azure does not implement the Codex standalone search protocol.

Current-model routing (searchRouting.useCurrentModel: true, with no top-level provider) also accepts openai-responses and azure-openai-responses models on HTTPS Azure resource endpoints under *.openai.azure.com, *.cognitiveservices.azure.com, or *.services.ai.azure.com. Their configured base URL must use /openai/v1; the model ID is sent as the deployment name, including names without a gpt- prefix. Other gateway and sovereign-cloud endpoints can use the explicit Responses endpoint configuration, but are not automatically recognized as Azure for authentication or current-model eligibility.

Costs and privacy: Bing grounding incurs search charges in addition to model usage. Microsoft states that grounding data flows outside your compliance and geographic boundary and is not covered by the Microsoft Data Protection Addendum. Confirm that this is appropriate before sending sensitive queries. See Microsoft's Azure web search documentation.

After reloading Pi, verify with a public, non-sensitive query. A successful hosted search must contain a web_search_call and return an answer or sources; an ordinary model-only response is rejected.

Optional OpenAI standalone search

Set openaiUseAlphaSearch: true to use the independent Codex alpha/search protocol instead of Responses-hosted web_search. The default is false; omitting the flag keeps existing Responses behavior. Non-boolean values are rejected.

{
  "openaiUseAlphaSearch": true,
  "openaiResponsesUrl": "https://gateway.example.com/v1/responses",
  "openaiSearchProviders": ["my-openai-provider"]
}

The selected Responses endpoint must end in /responses (an optional trailing slash is accepted). Its path suffix becomes /alpha/search, preserving the host, port, prefix, and query parameters. For example, /v1/responses becomes /v1/alpha/search. By default, Codex subscription auth selects https://chatgpt.com/backend-api/codex/alpha/search; with provider URL reuse enabled, it follows the resolved provider endpoint instead. Existing credential selection, search-model overrides, custom-base-URL safeguards when URL reuse is disabled, and official current-model eligibility rules still apply. This flag does not make an arbitrary gateway support standalone search.

Standalone requests use id, model, and commands.search_query, not a Responses tool call. Plaintext output and structured text_result sources are returned without another model-generated summary. Encrypted-only responses are rejected. numResults defaults to 5 and caps the deduplicated source list at up to 20; recency maps to 1/7/30/365 days, and positive domain filters map to domains. Excluded domains (-example.com) are explicitly unsupported in this mode rather than silently ignored.

Missing endpoint responses (HTTP 404/405/501) and excluded-domain requests follow the existing unsupported fallback policy. Other HTTP errors, cancellation, proxy transport, and the 60-second request deadline retain their existing behavior. There is no automatic retry through Responses. To permit another provider, configure searchRouting.fallbackOn accordingly; explicit provider: "openai" remains strict. Reload Pi after changing configuration.

To route automatic searches through the active Pi model, configure an ordered route without a top-level provider:

{
  "searchRouting": {
    "providers": ["openai", "tavily"],
    "useCurrentModel": true,
    "fallbackOn": ["unsupported", "transient", "quota", "network", "invalid-response"]
  }
}

With useCurrentModel: true, the automatic openai step uses Hosted web_search (or standalone search when openaiUseAlphaSearch is enabled) when the active model is a GPT model backed by an official OpenAI Responses endpoint: openai/openai-responses on HTTPS api.openai.com, or openai-codex/openai-codex-responses on the official ChatGPT Codex endpoint. Azure v1 Responses models are also eligible as described above, using their model ID as the deployment name. Third-party gateways and other models continue to the next route entry. A tool-level provider or top-level provider remains an explicit override; provider: "openai" keeps the existing independent OpenAI/Codex search-model behavior.

For sandboxed or corporate networks where only the proxy can resolve hostnames, set ssrf.trustEnvProxy to true to skip local DNS preflight for proxied hostnames:

{
  "ssrf": {
    "trustEnvProxy": true
  }
}

This is an opt-in DNS-preflight adjustment, not proxy transport configuration. It applies to requests sent through the proxy set in web-search.json (HTTP(S), socks5h, or socks4a, including a per-call proxy with the same value). When no proxy is configured, it applies to hostnames covered by HTTP_PROXY, HTTPS_PROXY, or ALL_PROXY, for sandboxes that route outbound traffic through those variables themselves; the extension does not route requests through environment proxies. A per-call proxy that differs from the configured one still gets local DNS validation. NO_PROXY hosts still undergo DNS validation, and localhost or literal private IP targets remain blocked.

Optional dependencies for video frame extraction:

brew install ffmpeg   # frame extraction, video thumbnails, local video duration
brew install yt-dlp   # YouTube stream URLs for frame extraction

Without these, video content analysis (transcripts, visual descriptions via Gemini) still works. The binaries are only needed for extracting individual frames as images.

System dependency for curator browser launch on Linux:

Package Purpose Install
xdg-utils Opens the curator UI in your default browser Debian/Ubuntu: sudo apt install -y xdg-utils · Fedora/RHEL: sudo dnf install -y xdg-utils · Arch: sudo pacman -S xdg-utils

Without xdg-utils, the curator URL is printed to the tool output so you can copy it into a browser.

Requires Pi v0.37.3+.

Quick Start

// Search the web
web_search({ query: "TypeScript best practices 2025" })

// Fetch a page
fetch_content({ url: "https://docs.example.com/guide" })

// Clone a GitHub repo
fetch_content({ url: "https://github.com/owner/repo" })

// Understand a YouTube video
fetch_content({ url: "https://youtube.com/watch?v=abc", prompt: "What libraries are shown?" })

// Analyze a screen recording
fetch_content({ url: "/path/to/recording.mp4", prompt: "What error appears on screen?" })

Tools

web_search

Search the web via OpenAI, Brave, Parallel, TinyFish, Search1API, Searchinfinity, Querit, Tavily, Firecrawl, Jina, SERPdive, Kagi, Bocha, Ollama, AnySearch, XCrawl, Valyu, xAI, Mistral, Bright Data SERP, SerpBase, SerpApi, Serper, Serply, SearchApi, You.com, keyless Keenable, Ceramic, DeepSeek, Linkup, a 9router gateway, the Cloudflare Web Search API, explicit-only Tabstack Research, self-hosted SearXNG, explicit-only degoog, keyless DuckDuckGo, Exa, Perplexity AI, Gemini, or Kimi. The default none workflow makes no summary-generation model call and opens no curator/browser: it returns bounded raw results, identifies the provider used for each query, and stores the full results for retrieval by responseId. The selected search provider may itself be model-backed.

web_search({ query: "rust async programming" })
web_search({ queries: ["query 1", "query 2"] })
web_search({ query: "latest news", numResults: 10, recencyFilter: "week" })
web_search({ query: "...", domainFilter: ["github.com"] })
web_search({ query: "...", provider: "openai" })
web_search({ query: "...", provider: "openai", model: "gpt-6-luna" })
web_search({ query: "...", provider: "kimi" })
web_search({ query: "...", provider: "mistral" })
web_search({ query: "...", provider: "all" })
web_search({ query: "...", provider: "brave,exa" })
web_search({ query: "...", includeContent: true })
web_search({ queries: ["query 1", "query 2"], workflow: "none" })
web_search({ queries: ["query 1", "query 2"], workflow: "summary-review" })
web_search({ queries: ["query 1", "query 2"], workflow: "auto-summary" })
Parameter Description
query / queries Single query or batch of queries
numResults Results per query (default: 5, max: 20)
recencyFilter day, week, month, or year
domainFilter Limit to domains (prefix with - to exclude)
category Exa only: restrict results to a category such as publication (research papers), news, or company; other providers ignore it. Each Exa source lists its published date and authors when Exa returns them, and publication results also list the citation count and DOI. Without an API key, if Exa's filtered search is unavailable, the category is added to the query instead
provider Omit it or pass "auto" to use the configured provider (auto-selects when no provider or routing is configured). "all" searches every eligible provider at once; All providers lists the explicit-only providers it skips. Otherwise one name (openai, brave, parallel, parallel-mcp, tinyfish, search1api, searchinfinity, querit, tavily, you, firecrawl, jina, serpdive, kagi, bocha, ollama, anysearch, xcrawl, valyu, xai, mistral, brightdata, serpbase, serpapi, serper, serply, searchapi, baizhi, zai, keenable, ceramic, deepseek, linkup, ninerouter, cloudflare, tabstack, searxng, degoog, duckduckgo, exa, perplexity, gemini, or kimi), or several comma-separated names such as "brave,exa" to search them at once. An unknown name fails before any request with an error that lists the valid names
includeContent Fetch full page content from sources in background
model Model ID for this call only, used instead of openaiSearchModel, xaiSearchModel, mistralSearchModel, or Gemini's searchModel. Requires provider set to exactly one of openai, xai, mistral, or gemini. A failure is reported as an error rather than retried with another model or provider, and Gemini uses only its API, never browser cookies. Listed only when webSearch.allowedProviders allows one of those providers; not available with workflow: "summary-review" or over MCP
workflow none (skip curator; fresh-install default), summary-review (open curator and auto-generate a summary draft), or auto-summary (generate a summary without opening the curator)
proxy http(s) or socks proxy URL for every request in this call; "" forces a direct connection. HTTP(S)_PROXY env vars are ignored. See Proxy

Batch searches run up to three queries concurrently. Provider routing and fallback within each query remain sequential. Brave requests share an in-process queue and honor the rate-limit buckets returned in X-RateLimit-*; exhausted short windows delay the next request with a small safety margin, while long quota resets fail fast and keep the advertised cooldown active for later queued calls. HTTP 429 responses are retried once after the advertised Retry-After or rate-limit reset. Brave's 30-second search deadline covers queueing, cooldown waits, and both request attempts. The queue coordinates one Pi process; separate Pi processes still rely on Brave's 429 response.

fetch_content

Fetch URL(s) as readable markdown, exact textual HTTP bodies, direct images, or page-grounded answers. Automatically detects and handles GitHub repos, GitHub PRs and issues, YouTube videos, PDFs, local video files, images, and regular web pages.

fetch_content({ url: "https://example.com/article" })
fetch_content({ urls: ["url1", "url2", "url3"] })
fetch_content({ url: "https://github.com/owner/repo" })
fetch_content({ url: "https://github.com/owner/repo/pull/123#discussion_r456" })
fetch_content({ url: "https://youtube.com/watch?v=abc", prompt: "What libraries are shown?" })
fetch_content({ url: "/path/to/recording.mp4", prompt: "What error appears on screen?" })
fetch_content({ url: "https://youtube.com/watch?v=abc", timestamp: "23:41-25:00", frames: 4 })
fetch_content({ url: "https://example.com/api", mode: "raw" })
fetch_content({ url: "https://example.com/guide", mode: "answer", prompt: "What are the installation steps?" })
fetch_content({ url: "https://example.com/pricing", mode: "extract", fields: { pro_monthly: "number: Pro plan monthly price", trial_days: "integer" } })
fetch_content({ url: "https://example.com/account", auth: "work", mode: "raw" })
fetch_content({ url: "https://example.com/diagram.png" })
Parameter Description
url / urls Single URL/path or multiple URLs
prompt Question for video analysis, or the page-local question required by mode: "answer"
mode readable (default), raw for exact textual HTTP bodies, answer for a grounded answer from fetched content, or extract for typed values (off unless fetch.allowedModes lists it)
fields For mode: "extract": field name → "type" or "type: what to find", such as "date: release date of the latest stable version"
answerModel Optional provider/model-id override for answer mode; defaults to the configured fetch.answerProvider + fetch.answerModel pair, or the current enabled Pi model when no pair is configured
timestamp Extract frame(s): single ("1:23:45", "23:41", or seconds "85") or range ("23:41-25:00", 6 evenly spaced frames by default; useful when you only know roughly where something happens). Needs ffmpeg, plus yt-dlp for YouTube
frames Number of frames (1-12): spread across a timestamp range, 5 seconds apart from a single timestamp, or across the whole video when used alone
model Gemini model for video and YouTube analysis (default: config, else gemini-3.6-flash)
forceClone Clone GitHub repos that exceed the 350MB size threshold
auth authFetch profile name for local browser-cookie fetching, or true when exactly one profile exists. See Blocked pages
proxy Proxy URL for this fetch; "" forces a direct connection. See Proxy
redirects "same-origin" refuses redirects that change the scheme, hostname, or port. See fetchContent.redirects

For a standing answer-mode model, set both fetch.answerProvider and fetch.answerModel in web-search.json; a per-call answerModel takes precedence. Configured answer defaults are opt-in and can send fetched page text to a different provider/model, which may change privacy and cost behavior.

Extract mode reads named values off each page with Cloudflare's Clairic model, on your own Cloudflare account: add "extract" to fetch.allowedModes and set up Pi's cloudflare-workers-ai provider (CLOUDFLARE_API_KEY and CLOUDFLARE_ACCOUNT_ID, or Pi's /login). Each field comes back as a typed value, an uncertain guess, or not on the page. The page text is sent to Cloudflare, including a private GitHub repository your gh login can read; auth is refused. Pi only. docs/field-extract.md covers AI Gateway routing, field types, limits, and measurements.

Thanks to @linuxtextadventurer for PR #328.

get_search_content

Retrieve stored content from previous searches or fetches. Search provider answers and every result remain available in full and can be paged with offset and limit or searched with findText. Fetched URL content is stored in full in a private web-search-cache directory under the Pi config directory, not in the session JSONL. This includes fetch_content answer mode, which stores the original page content. The cache has a one-hour lifetime and fixed limits of 128 entries and 128 MiB; when either limit is reached, the oldest entries are removed first. Concurrent Pi processes share this cache and its limits. To give a process its own cache without moving web-search.json, set PI_WEB_ACCESS_CACHE_ROOT to a directory; the cache then lives in web-search-cache inside it, and a session resumed without the same value cannot read content cached there. On macOS and Linux the cache directory and files are kept at permissions 0700 and 0600, respectively. Use findText to locate bounded matching passages without paging through a large page, or use offset and limit to retrieve slices intentionally.

get_search_content({ responseId: "abc123", urlIndex: 0 })
get_search_content({ responseId: "abc123", url: "https://...", offset: 30000 })
get_search_content({ responseId: "abc123", query: "original query" })
get_search_content({ responseId: "abc123", queryIndex: 0, offset: 30000 })
get_search_content({ responseId: "abc123", urlIndex: 0, findText: "installation" })
get_search_content({ responseId: "abc123", urlIndex: 0, findText: ["timeout", "retry"], findMode: "fuzzy" })

Pass the responseId from a web_search, fetch_content, or source_check result. Choose the stored item with query or queryIndex for a search, or url or urlIndex for a fetched page. offset (default 0) and limit page through it.

findMode supports exact, case-insensitive (default), and fuzzy. Finder output is capped at 20,000 characters with match counts and nearby context. Fitting responses retain their existing format and document order. Overflow responses identify queries as Q1, Q2, and so on, list each full query once, and reserve a representative excerpt for each query whose discovered match span fits the output budget. Queries with oversized spans are listed as having no representative excerpt. findText cannot be combined with offset or limit. The default limit and maximum permitted limit use maxInlineContentChars; search-page continuation guidance shares that overall output budget and may reduce the returned stored-content characters.

source_check

Gather evidence for a claim and return a machine-readable artifact with exact passage citations for manual semantic review. Search results are deduplicated and capped at 20 sources; fetchContent fetches at most 5 pages, while stored and retrieved content remains subject to the configured maxInlineContentChars offset/limit bounds. The result lists each source's passages under it, so the agent can weigh the claim without another call: up to 4,000 characters in all, each passage cut at 400, with the rest in the artifact. Passages from fetched pages are the sentences that share the most words with the claim, then with the search snippet; short acronyms and numbers such as API, TLS or 429 count when the page writes them the same way.

source_check({ claim: "The API supports streaming responses" })
source_check({
  claim: "The API supports streaming responses",
  queries: ["API streaming responses documentation", "API streaming limitations"],
  fetchContent: true,
  domainFilter: ["docs.example.com", "-old.example.com"]
})
Parameter Description
claim The assertion to check
queries Search queries to run (default: the claim; up to 8)
numResults Results per query (default: 5, max: 20)
fetchContent Fetch the top 5 pages so passages come with exact offsets in the page
recencyFilter day, week, month, or year
domainFilter Limit to domains (prefix with - to exclude)
provider Same as web_search: "auto", "all", one name, or a list such as "brave,exa"
proxy http(s) or socks proxy URL for every request in this call; "" forces a direct connection. See Proxy

The artifact preserves the supported, contradicted, unclear, or missing-evidence claim status schema, source quality hints, SHA-256 content hashes, and passage IDs with exact source offsets. It does not judge the claim: retrieved passages produce unclear for manual review, while no passages produce missing-evidence. Search and fetch errors remain in the artifact instead of being silently discarded. Artifacts are stored with the session and retrieved through get_search_content using the returned responseId; paged artifact responses are JSON slices, so request the next offset when needed.

In codemode scripts

Scripts run by Pi's codemode tool (Pi 1.0 or later) get data instead of the text the model sees. await tools.web_search(...) resolves to { responseId, fetchId, queries }, where each query has its answer, error, provider, and results (title, url, snippet). await tools.fetch_content(...) resolves to { responseId, urls }, where each URL has its full content, not limited by maxInlineContentChars, and its error. Failed searches and fetches still resolve to this data, with the reason in error; invalid arguments reject. A script's web_search uses workflow: "none" unless it passes a workflow, so a configured curator does not open, and with includeContent it waits for the pages, readable through fetchId, instead of fetching them in the background. A script that passes workflow: "summary-review" gets the curator and the background fetch, as the model does.

With extract mode allowed, each fetched URL also has an extraction: fields maps each name to { status, value, confidence } (status is "extracted", "uncertain" with a guess instead of a value, or "null" for not on the page), or error says why extraction failed for that page. To search and read a value off the top results:

const search = await tools.web_search({ query: "Acme Pro plan price" });
const urls = search.queries[0].results.slice(0, 3).map((result) => result.url);
const page = await tools.fetch_content({ urls, mode: "extract", fields: { pro_monthly: "number: Pro plan price per month" } });
return page.urls.map(({ url, extraction }) => ({ url, price: extraction?.fields?.pro_monthly }));

Use from other agents (MCP)

Other agents, such as Claude Code, Codex, Cursor, or Executor, can use web_search, fetch_content, get_search_content, and source_check through a local stdio MCP server. It needs Node.js 22.19 or later and no Pi install:

npx -y pi-web-access

Add it to an mcpServers config (Claude Code, Cursor, and similar clients; Codex takes the same command, args, and env in its config.toml):

{
  "mcpServers": {
    "pi-web-access": {
      "command": "npx",
      "args": ["-y", "pi-web-access"],
      "env": { "BRAVE_API_KEY": "BSA_..." }
    }
  }
}
  • The server reads the same web-search.json and provider environment variables as the extension. It lists only the tools enabled there, under their default names; toolNames renames do not apply.
  • Pi-only features are not available: the curator and summaries (a configured workflow is ignored), Kimi search, OpenAI search through ChatGPT sign-in or the current Pi model, fetch_content answer and extract modes and video prompts or frames, and direct image fetches. Explicit requests for them return a tool error rather than a fallback, and automatic provider selection only uses providers that work outside Pi.
  • includeContent waits for the page fetch before web_search returns.
  • The server keeps the 50 most recent results in memory for get_search_content; they are gone when it exits. Restart the server to pick up config changes.
  • Install with npm's default settings or --legacy-peer-deps. --omit=peer leaves out zod, which the MCP SDK needs.

Capabilities

GitHub repos

GitHub URLs are cloned locally instead of scraped. The agent gets real file contents and a local path to explore with read and bash. Root URLs return the repo tree + README, /tree/ paths return directory listings, /blob/ paths return file contents.

Repos over 350MB get a lightweight API-based view instead of a full clone (override with forceClone: true). Commit SHA URLs are handled via the API. Clones are cached for the session and wiped on session change. Private repos require the gh CLI. Set githubClone.enabled to false to skip this GitHub-specific clone/API handling; fetch_content remains available, so the URL can continue through the normal HTTP extraction path.

Pull request and issue URLs are rendered as one priority-ordered markdown document instead of scraped HTML. PR views include status, body, checks when gh supports them, review verdicts, linked references, files, commits, conversation comments, review thread comments, truncation markers, and escalation commands. Issue views include state, metadata, body, linked closing PRs when available, and comments. Comment anchors such as #issuecomment-... and #discussion_r... are forced inline. The full rendered document is stored for get_search_content offsets and findText.

fetch_content uses gh pr view or gh issue view first with prompts disabled. It retries with a smaller field set when an older gh does not know a requested field. Public unauthenticated REST is used as a bounded fallback under the same fetchContent.domainPolicy and SSRF rules; REST cannot include checks. Set githubPrIssue.enabled to false to skip PR/issue specialization and keep normal HTTP extraction.

YouTube videos

YouTube URLs are processed via Gemini for full video understanding — visual descriptions, transcripts with timestamps, and chapter markers. Pass a prompt to ask specific questions about the video. Results include the video thumbnail so the agent gets visual context alongside the transcript.

Fallback: Gemini Web when browser cookies are enabled → Gemini API → Perplexity (text summary only). Handles all URL formats: /watch?v=, youtu.be/, /shorts/, /live/, /embed/, /v/.

Local video files

Pass a file path (/, ./, ../, or file:// prefix) to analyze video content via Gemini. Supports MP4, MOV, WebM, AVI, and other common formats up to 50MB for Gemini analysis. Pass a prompt to ask about specific content. If ffmpeg is installed, a thumbnail frame is included alongside the analysis. Timestamp/frame extraction uses ffmpeg directly and can still operate on larger local files.

Fallback: Gemini API (Files API upload) → Gemini Web when browser cookies are enabled.

Video frame extraction

Use timestamp and/or frames on any YouTube URL or local video file to extract visual frames as images.

fetch_content({ url: "...", timestamp: "23:41" })                       // single frame
fetch_content({ url: "...", timestamp: "23:41-25:00" })                 // range, 6 frames
fetch_content({ url: "...", timestamp: "23:41-25:00", frames: 3 })      // range, custom count
fetch_content({ url: "...", timestamp: "23:41", frames: 5 })            // 5 frames at 5s intervals
fetch_content({ url: "...", frames: 6 })                                // sample whole video

Requires ffmpeg (and yt-dlp for YouTube). Timestamps accept H:MM:SS, MM:SS, or bare seconds.

PDFs

With mode: "answer", the answer model receives the extracted PDF Markdown. In readable mode the result is the Markdown itself, preceded by the path of the saved file, and long PDFs are cut at maxInlineContentChars like any page. In readable, answer and extract modes the Markdown is stored, so get_search_content can page through it.

PDF URLs are converted to Markdown and also saved under the temporary pi-web-pdf directory by default, so an agent that can read local files can read specific sections. Four engines are available, selected with pdf.provider ("auto" is the default):

Provider Engine Trade-offs
endpoint Self-hosted parser endpoint (POST the PDF, get Markdown) Layout-aware conversion (marker and similar pipelines) on hardware you run; no key, no per-page cost; requires pdf.endpointUrl
datalab Datalab hosted conversion (Marker) Deterministic layout-aware output — tables, multi-column reading order, headings, math; accurate mode handles scanned pages; may return a parse_quality_score; requires a Datalab key, billed per page with a free monthly credit
gemini Gemini API (vision LLM) Best on scanned/complex pages; LLM transcription can occasionally drift or truncate; requires a Gemini key
unpdf Local pdf.js text extraction Free, offline, no key; flattened text only — no layout, no tables, no OCR

auto order: self-hosted endpoint (when pdf.endpointUrl is configured) → Datalab (when a key is configured) → Gemini (when a key is configured) → local unpdf. The endpoint runs first because it is local and deterministic. If its request fails — parser down, engine error, timeout — the chain continues to Datalab, then Gemini, then unpdf, automatically. Setting pdf.provider to endpoint, gemini, datalab, or unpdf pins that engine and skips the other remote tiers (an explicit engine still falls back to unpdf when it errors, except for credential/config errors and caller cancellation). No Datalab key means the datalab tier is simply skipped — behavior is unchanged for existing users.

Self-hosted parser endpoint. pdf.endpointUrl receives a multipart/form-data POST with a file field (plus engine when pdf.endpointEngine is set, and max_pages) and answers { "markdown": "…" }, or { "job_id": "…" }, which is polled at jobs/{id} next to the configured URL until done, error, or pdf.endpointTimeoutMs (default 120s, capped at 300s). The URL passes the same SSRF guard as every other fetch, so a LAN host needs its range in ssrf.allowRanges (loopback is allowed); PDF_ENDPOINT_URL overrides pdf.endpointUrl. For a LAN parser: { "ssrf": { "allowRanges": ["192.168.50.0/24"] }, "pdf": { "endpointUrl": "http://192.168.50.48:8070/parse", "endpointEngine": "marker" } }.

Why Datalab. The hosted converter uses a dedicated extraction engine (Marker) intended to retain document structure such as tables, multi-column reading order, headings, links, and math, where local unpdf extraction only yields flattened text. It is deterministic rather than LLM-based. Completed responses may include a parse_quality_score (0–5) for optional quality gating. Pricing is per processed page: fast / balanced $4 / 1,000 pages; accurate $10 / 1,000 pages. The free tier gives a $10 monthly credit (personal email; $20 with a work email) at 25 requests/minute — roughly 2,500 pages/month free in fast mode or 1,000 in accurate mode. Processing defaults to the US region. EU data residency uses 1.25× usage; opt in with DATALAB_PROCESSING_LOCATION=eu.

Configure Datalab via the web-search config:

{
  "datalabApiKey": "$DATALAB_API_KEY",
  "pdf": {
    "maxSizeMB": 20,
    "maxPages": 100,
    "provider": "auto",      // "auto" | "endpoint" | "gemini" | "datalab" | "unpdf"
    "datalabMode": "balanced", // "fast" | "balanced" | "accurate"
    "datalabTimeoutMs": 120000
  }
}

Env vars: DATALAB_API_KEY (or datalabApiKey in config), DATALAB_PROCESSING_LOCATION (us default; eu enables EU data residency at 1.25× usage), DATALAB_MODE (fast / balanced / accurate), and DATALAB_API_BASE (custom gateway). pdf.datalabMode overrides DATALAB_MODE. The default datalabTimeoutMs is 120s and is capped at 300s.

Privacy note: like the Gemini tier, the PDF bytes are sent to the Datalab cloud for conversion. Files are uploaded to the selected region's storage and deleted best-effort after conversion.

Blocked pages

Raw and direct-image HTTP requests use the same SSRF validation, hostname domain policy, redirect checks, timeout, and 5MB streamed response bound as normal extraction. Raw mode returns textual bodies even for non-2xx responses and exposes the HTTP status in tool details; it does not run readability or hosted extraction fallbacks.

fetch_content can opt into local browser-cookie auth with auth: "profile", or auth: true when exactly one authFetch profile exists. Configure profiles in ~/.pi/agent/web-search.json, for example { "authFetch": { "social": ["x.com", "instagram.com"], "work": { "hosts": ["docs.company.com"], "chromeProfile": "Profile 2", "cache": "off" } } }. Auth fetch uses only the local direct HTTP path, requires HTTPS, allows only configured hosts and their subdomains, refuses cross-origin redirects, and never sends cookies or authenticated content to hosted extraction providers. Browser cookie extraction remains opt-in through allowBrowserCookies: true or PI_ALLOW_BROWSER_COOKIES=1.

Proxy (proxy)

web_search, source_check, and fetch_content all accept an optional proxy string (e.g. "http://mcr:4444"). When provided, every outbound HTTP(S) request is routed through curl instead of Node's built-in fetch — this works around Node fetch ignoring HTTP(S)_PROXY env vars and undici ProxyAgent failing the TLS handshake against several common HTTP proxies (ERR_SSL_WRONG_VERSION_NUMBER).

An empty string ("") forces a direct connection even when a config-level proxy is set. Omitting the parameter falls back to the global proxy in ~/.pi/agent/web-search.json.

The config-level proxy applies only to requests made by this extension's tools. Other traffic in the host process — such as the coding agent's own model API calls — is never routed through it, so a configured proxy that is temporarily down cannot break unrelated requests.

// ~/.pi/agent/web-search.json — global proxy for all tools
{
  "proxy": "http://mcr:4444"
}

Localhost, 127.0.0.1, [::1], and any host matching the NO_PROXY environment variable are never proxied.

When Readability fails or returns only a cookie notice, the extension can retry configured Firecrawl extraction, configured Crawl4AI extraction, Jina Reader (handles JS rendering server-side, no API key needed), TinyFish, Search1API, Querit, Kagi Extract, Ollama Web Fetch, Parallel, Bright Data Web Unlocker, DeepSeek Fetch, Exa contents, Tabstack Extract, Gemini URL Context API, and Gemini Web extraction when browser cookies are enabled. Configure fetchRouting.providers to change the order or set of fetch_content providers. Supported values are http, firecrawl, crawl4ai, jina, tinyfish, search1api, querit, kagi, ollama, parallel, parallel-mcp, brightdata, gemini, deepseek, exa, and tabstack; when absent, the default order is unchanged. parallel-mcp, deepseek, exa, and tabstack are not in the default fetch order and must be listed explicitly. exa reads the page with Exa's POST /contents endpoint using the same exaApiKey/EXA_API_KEY as Exa search (it is skipped without a key, and Exa bills each page); a page Exa cannot read is reported as an Exa error rather than an empty page. For remote HTTP(S) targets, third-party hosted providers are disabled unless fetchRouting.allowRemoteHostedProviders is true, because hosted services perform their own fetch and can see a different redirect chain than the local safety gate. Firecrawl and Crawl4AI stay available as configured self-hosted extraction services. Firecrawl requests are cache-only by default and require an explicit fresh-scrape opt-in before the Firecrawl server can fetch target URLs. Bright Data Web Unlocker runs last of the remote scraping providers, ahead of only the Gemini fallbacks, because it is billed per request against a paid account; it is skipped unless both a key and an unblocker zone are configured. It applies no minimum-length check, so any non-empty body it returns — including a short consent or paywall stub — is the final answer for that URL and the Gemini fallbacks are not tried. Handles SPAs, JS-heavy pages, and anti-bot protections transparently. Also parses Next.js RSC flight data when present. HTML extraction also surfaces registered discovery relations (service-desc, service-doc, service-meta, api-catalog, describedby) from the HTTP Link header and matching link/a[rel] markup. Readable or rendered content remains primary; on an empty shell, the normal extraction fallbacks run before declared links are returned on their own.

How It Works

web_search(query)
  → configured searchRouting (optionally active-model-aware OpenAI Hosted Search) → automatic provider chain

fetch_content(url)
  → Video file?  Gemini API (Files API) → Gemini Web (if browser cookies enabled)
  → GitHub URL?  Clone repo, return file contents + local path
  → YouTube URL? Gemini Web (if browser cookies enabled) → Gemini API → Perplexity
  → HTTP fetch (asks for markdown first; raw mode does not) → PDF? Self-hosted endpoint → Datalab → Gemini API → local text extraction, save to temp pi-web-pdf
               → HTML? Readability (+ declared Link/rel discovery) → RSC parser → Firecrawl → Crawl4AI (each if configured) → third-party hosted fallbacks only when fetchRouting.allowRemoteHostedProviders is enabled
               → Text/JSON/Markdown? Return directly

Commands

/websearch

Open the search curator directly. Runs searches and lets you review, add, select results, and approve a summary before it is sent back to the agent — no LLM round-trip needed.

/websearch                                               # empty page, type your own searches
/websearch react hooks, next.js caching                  # pre-fill with comma-separated queries

Results get injected into the conversation when you approve the summary or click "Send selected results without summary". On timeout, the curator auto-submits and falls back to a deterministic summary if no approved draft is present.

In summary review, Approve + auto-summary remaining searches for this prompt approves the current draft and makes later searches that inherit summary-review use auto-summary until the run settles. Explicit workflows still win; the choice stays in memory and does not affect open curator windows.

/curator

Toggle or configure the curator workflow at runtime.

/curator                    # toggle on/off
/curator on                 # enable curator (summary-review)
/curator off                # disable curator (raw results only)
/curator summary-review     # explicit workflow

Persists to ~/.pi/agent/web-search.json and takes effect on the next web_search call. When disabled, web_search returns raw results without opening the curator window.

/search

Browse stored search results interactively. Lists all results from the current session with their response IDs for easy retrieval.

/google-account

Show the active Google account currently authenticated for Gemini Web. If cookie extraction fails, it reports sanitized attempted browser/profile entries and whether the failure was missing required cookies, password-store access, decryption, SQLite, or profile lookup.

Activity Monitor

Toggle with Ctrl+Shift+W to see live request/response activity:

─── Web Search Activity ────────────────────────────────────
  API  "typescript best practices"     200    2.1s ✓
  GET  docs.example.com/article        200    0.8s ✓
  GET  blog.example.com/post           404    0.3s ✗
────────────────────────────────────────────────────────────

Configuration

Config defaults to ~/.pi/agent/web-search.json when neither PI_CODING_AGENT_DIR nor XDG_CONFIG_HOME is set. PI_CODING_AGENT_DIR takes precedence when set; with XDG_CONFIG_HOME, an existing XDG_CONFIG_HOME/pi/web-search.json is preferred, an existing legacy ~/.pi/web-search.json remains usable for compatibility, and the XDG path is used as the new-config target when neither file exists. When neither environment variable is set, an existing ~/.pi/agent/web-search.json is preferred, an existing legacy ~/.pi/web-search.json remains usable for compatibility, and the agent directory is used as the new-config target when neither file exists. Every field is optional.

{
  "openaiApiKey": "sk-...",
  "openaiResponsesUrl": "https://gateway.example.com/v1/responses",
  "braveApiKey": "BSA_...",
  "braveBaseUrl": "https://gateway.example.com/brave/res/v1",
  "exaApiKey": "exa-...",
  "exaBaseUrl": "https://gateway.example.com/exa",
  "parallelApiKey": "...",
  "parallelBaseUrl": "https://gateway.example.com/parallel",
  "tinyfishApiKey": "sk-tinyfish-...",
  "search1apiApiKey": "...",
  "tavilyApiKey": "tvly-...",
  "tavilyBaseUrl": "https://gateway.example.com/tavily",
  "tabstackApiKey": "$TABSTACK_API_KEY",
  "tabstackResearchMode": "fast",
  "youApiKey": "your-ydc-api-key",
  "jinaApiKey": "$JINA_API_KEY",
  "serpdiveApiKey": "sd_live_...",
  "serpdiveModel": "krill",
  "kagiApiKey": "$KAGI_API_KEY",
  "ollamaApiKey": "$OLLAMA_API_KEY",
  "valyuApiKey": "$VALYU_API_KEY",
  "keenableApiKey": "$KEENABLE_API_KEY",
  "ceramicApiKey": "$CERAMIC_API_KEY",
  "linkupApiKey": "$LINKUP_API_KEY",
  "linkupDepth": "fast",
  "deepseekApiKey": "$DEEPSEEK_API_KEY",
  "serpbaseApiKey": "$SERPBASE_API_KEY",
  "serpapiBaseUrl": "https://gateway.example.com/serpapi",
  "serpapiApiKey": "$SERPAPI_KEY",
  "serperBaseUrl": "https://gateway.example.com/serper",
  "serperApiKey": "$SERPER_API_KEY",
  "serplyApiKey": "$SERPLY_API_KEY",
  "searchapiApiKey": "$SEARCHAPI_API_KEY",
  "baizhiApiKey": "$BAIZHI_API_KEY",
  "zaiApiKey": "$ZAI_API_KEY",
  "brightdataApiKey": "$BRIGHTDATA_API_KEY",
  "brightdataSerpZone": "pi_serp",
  "xaiApiKey": "xai-...",
  "xaiSearchTools": ["web_search"],
  "mistralApiKey": "$MISTRAL_API_KEY",
  "mistralSearchModel": "mistral-small-latest",
  "mistralSearchTool": "web_search",
  "searxngBaseUrl": "https://search.example.com",
  "searxngHeaders": {
    "CF-Access-Client-Id": "xxxxxxxx.access",
    "CF-Access-Client-Secret": "xxxxxxxx"
  },
  "degoogBaseUrl": "https://degoog.example.com",
  "degoogApiKey": "$DEGOOG_API_KEY",
  "degoogEngines": ["degoog-org-official-extensions-brave-engine"],
  "firecrawlBaseUrl": "https://crawl.example.com",
  "firecrawlApiKey": "fc-...",
  "firecrawlApiVersion": "v2",
  "firecrawlFreshScrape": false,
  "firecrawlPdfMaxPages": 50,
  "crawl4aiBaseUrl": "https://crawl4ai.example.com",
  "crawl4aiApiToken": "$CRAWL4AI_API_TOKEN",
  "brightdataUnlockerZone": "pi_unlocker",
  "perplexityApiKey": "pplx-...",
  "geminiApiKey": "AIza...",
  "geminiBaseUrl": "https://my-gateway.example.com/gemini",
  "cloudflareApiKey": "...",
  "geminiAuth": "adc",
  "geminiProject": "my-gcp-project",
  "geminiLocation": "us-central1",
  "provider": "openai",
  "searchRouting": {
    "providers": ["openai", "brave", "exa"],
    "useCurrentModel": false,
    "fallbackOn": ["transient", "quota", "network", "invalid-response"]
  },
  "fetchRouting": {
    "providers": ["http", "firecrawl", "crawl4ai", "jina", "tinyfish", "search1api", "querit", "kagi", "ollama", "parallel", "brightdata", "gemini"],
    "allowRemoteHostedProviders": false
  },
  "fetch": {
    "timeout": 30,
    "defaultMode": "readable",
    "allowedModes": ["readable", "raw", "answer"],
    "answerProvider": "openai",
    "answerModel": "gpt-5.6"
  },
  "webSearch": {
    "enabled": true,
    "allowedProviders": ["openai", "brave", "exa"]
  },
  "tools": {
    "webSearch": { "enabled": true },
    "sourceCheck": { "enabled": true },
    "fetchContent": { "enabled": true },
    "getSearchContent": { "enabled": true }
  },
  "toolActivation": "auto",
  "commands": {
    "websearch": { "enabled": true },
    "curator": { "enabled": true },
    "search": { "enabled": true },
    "google-account": { "enabled": true }
  },
  "image": {
    "enabled": true
  },
  "browserCookies": {
    "browser": "helium",
    "profile": "Profile 2"
  },
  "allowBrowserCookies": false,
  "searchModel": "gemini-3.6-flash",
  "summaryModel": "anthropic/claude-haiku-4-5",
  "queryRewriteModel": "current",
  "summaryGenerationDeadlineMs": 30000,
  "summaryInstructions": "- Preserve concrete figures verbatim: prices, limits, versions, dates.\n- Name the product version each claim applies to.",
  "maxInlineContentChars": 30000,
  "workflow": "summary-review",
  "curatorTimeoutSeconds": 20,
  "curatorRemote": {
    "host": "my-box.tailnet.ts.net",
    "bind": "100.101.102.103"
  },
  "autoOpenBrowser": true,
  "githubClone": {
    "enabled": true,
    "maxRepoSizeMB": 350,
    "cloneTimeoutSeconds": 30,
    "clonePath": "/tmp/pi-github-repos"
  },
  "githubPrIssue": {
    "enabled": true
  },
  "youtube": {
    "enabled": true,
    "preferredModel": "gemini-3.6-flash"
  },
  "video": {
    "enabled": true,
    "preferredModel": "gemini-3.6-flash",
    "maxSizeMB": 50
  },
  "pdf": {
    "enabled": true,
    "maxSizeMB": 20,
    "provider": "auto"
  },
  "fetchContent": {
    "domainPolicy": {
      "allow": ["example.com"],
      "deny": ["blocked.example.com"]
    },
    "redirects": "same-origin",
    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
    "curl": "curl_chrome146"
  },
  "shortcuts": {
    "curate": "ctrl+shift+s",
    "activity": "ctrl+shift+w"
  },
  "ssrf": {
    "allowRanges": ["198.18.0.0/15"],
    "trustEnvProxy": false
  }
}

maxInlineContentChars accepts integers from 1,000 through 200,000. Missing, invalid, or smaller values use the 30,000-character default so truncation and retrieval guidance always fit.

webSearch.allowedProviders is an optional, non-empty search-provider allowlist with no duplicate entries. When omitted, every current search provider remains permitted. When present, explicit scalar and array requests for providers outside the list fail before any provider request, while auto, all, configured routing, source_check, and Curator use only listed providers. The allowlist does not make explicit-only or paid providers eligible for automatic fallback or all; they must still be named explicitly or included in searchRouting.providers. A configured provider/searchProvider or searchRouting.providers entry outside the allowlist is rejected as invalid configuration. This setting affects search only: it does not restrict fetchRouting, fetch answer models, or summary models.

summaryModel accepts an optional thinking-level suffix, such as anthropic/claude-haiku-4-5:low. Supported suffixes are off, minimal, low, medium, high, xhigh, and max. "current" uses the model the Pi session is on when the search starts, such as a local model; add a suffix ("current:low") to set a thinking level, since the session's own level is not copied. Pi enabledModels still applies. When summaryModel is set, no fallback model is tried, including for a model you pick in the curator: if the model is unavailable or fails, you get the deterministic summary with the reason it failed, not a summary from another provider. Unset, the built-in list of small models is tried in order. Any other value makes summary workflows fail with an error naming the config file.

queryRewriteModel picks the model for the curator's query rewrite and takes the same values as summaryModel. When set, only that model is used, and if it is unavailable or fails the rewrite shows an error. Unset, the rewrite tries anthropic/claude-haiku-4-5, then google/gemini-3.6-flash, then openai/gpt-5-mini.

summaryInstructions appends custom text to the Requirements section of the summary prompt used by the curator UI and auto-summary mode (e.g. "- Preserve concrete figures verbatim: prices, limits, versions, dates.\n- Name the product version each claim applies to."). The text is passed verbatim to the summary model, so use one - bullet per requirement. It is additive: the built-in guardrails (no invented sources, explicit weak/conflicting-evidence notes, the Sources section) always stay in place, and curator regeneration feedback is still appended afterwards. Missing, non-string, or blank values leave the default prompt unchanged.

All provider API-key fields (openaiApiKey, braveApiKey, parallelApiKey, tinyfishApiKey, search1apiApiKey, searchinfinityApiKey, queritApiKey, tavilyApiKey, tabstackApiKey, youApiKey, jinaApiKey, serpdiveApiKey, kagiApiKey, bochaApiKey, ollamaApiKey, valyuApiKey, serpbaseApiKey, serpapiApiKey, serperApiKey, serplyApiKey, searchapiApiKey, baizhiApiKey, zaiApiKey, keenableApiKey, ceramicApiKey, linkupApiKey, ninerouterApiKey, deepseekApiKey, degoogApiKey, anysearchApiKey, xcrawlApiKey, xaiApiKey, mistralApiKey, brightdataApiKey, firecrawlApiKey, crawl4aiApiToken, exaApiKey, perplexityApiKey, geminiApiKey, datalabApiKey, and cloudflareApiKey) accept explicit credential sources. Use $NAME or ${NAME} to read one named environment variable, or prefix a trusted local shell command with ! to resolve one value at provider request time. Escape $$ as a literal leading $ and $! as a literal leading !:

{
  "openaiApiKey": "!/absolute/path/to/secret-manager read openai",
  "braveApiKey": "${SCOPED_BRAVE_API_KEY}",
  "exaApiKey": "$$literal-key",
  "geminiApiKey": "$!literal-command"
}

This syntax applies to provider credentials only; other configuration fields are not interpolated. firecrawlApiKey, crawl4aiApiToken, kagiApiKey, ollamaApiKey, valyuApiKey, serpbaseApiKey, serpapiApiKey, serperApiKey, serplyApiKey, searchapiApiKey, baizhiApiKey, zaiApiKey, keenableApiKey, ceramicApiKey, linkupApiKey, ninerouterApiKey, deepseekApiKey, degoogApiKey, mistralApiKey, and brightdataApiKey use the same credential-source rules, while API base URLs, firecrawlApiVersion, firecrawlFreshScrape, firecrawlPdfMaxPages, crawl4aiBaseUrl, degoogBaseUrl, degoogEngines, brightdataSerpZone, brightdataUnlockerZone, and zaiEndpoint are literal config values.

A command source is not run while the extension loads or registers tools. Each selected provider request runs it again with a five-second timeout, a 16 KiB output limit, a minimized environment, and a one-line non-empty stdout requirement. Command text and stderr are omitted from errors. On Windows the command runs in Git Bash (or bash.exe on PATH), like Pi's own !command config values, so POSIX syntax such as $HOME works; it falls back to cmd.exe when no bash is found. These commands are trusted local configuration, not a same-user process isolation boundary; use absolute executable paths and protect the config file. OP_SESSION_* and OP_SERVICE_ACCOUNT_TOKEN, when present in Pi's environment, are forwarded to trusted resolver commands so 1Password CLI sessions and service accounts can be reused without storing their credentials in config. For example, "braveApiKey": "!/absolute/path/to/op read 'op://Automation/Brave/credential'" resolves that item after you replace the executable placeholder with your trusted installation's absolute path, but the op:// argument is not an authorization boundary: every configured resolver command that receives the token can exercise all vault and item permissions granted to its service account. Prefer a narrowly scoped service account. An explicit source overrides legacy provider environment variables and fails that provider locally rather than falling back with a stale credential. Direct Google Gemini API requests send the resolved key only in the x-goog-api-key header, never in the URL.

mistralSearchModel defaults to mistral-small-latest and must be a non-empty string. mistralSearchTool defaults to web_search and accepts only web_search or the opt-in web_search_premium tool.

These providers accept an API base URL for compatible gateways. Environment variables take precedence over config; path prefixes are preserved and trailing slashes are removed. Ollama, DeepSeek, and Tabstack read PI_OLLAMA_BASE_URL, PI_DEEPSEEK_BASE_URL, and PI_TABSTACK_BASE_URL, because other tools commonly set the bare OLLAMA_BASE_URL (a local Ollama server), DEEPSEEK_BASE_URL (an OpenAI-compatible /v1 root), and TABSTACK_BASE_URL (the Tabstack SDKs' /v1 root); the bare names are ignored.

Config Environment variable Appended paths
braveBaseUrl BRAVE_BASE_URL /web/search
exaBaseUrl EXA_BASE_URL /search, /contents
tavilyBaseUrl TAVILY_BASE_URL /search
anysearchBaseUrl ANYSEARCH_BASE_URL /v1/search
bochaBaseUrl BOCHA_BASE_URL /v1/web-search
brightdataBaseUrl BRIGHTDATA_BASE_URL /request (SERP and Web Unlocker)
ceramicBaseUrl CERAMIC_BASE_URL /search
deepseekBaseUrl PI_DEEPSEEK_BASE_URL /v1/tools/search, /v1/tools/fetch
jinaSearchBaseUrl JINA_SEARCH_BASE_URL /{encoded query}
kagiBaseUrl KAGI_BASE_URL /api/v1/search, /api/v1/extract
keenableBaseUrl KEENABLE_BASE_URL /v1/search, /v1/search/public
linkupBaseUrl LINKUP_BASE_URL /v1/search
mistralSearchBaseUrl MISTRAL_SEARCH_BASE_URL /v1/conversations
ollamaBaseUrl PI_OLLAMA_BASE_URL /api/web_search, /api/web_fetch
parallelBaseUrl PARALLEL_BASE_URL /v1/search, /v1/extract
perplexityBaseUrl PERPLEXITY_BASE_URL /search, /chat/completions
queritBaseUrl QUERIT_BASE_URL /v1/search, /v1/contents
search1apiBaseUrl SEARCH1API_BASE_URL /search, /crawl
searchapiBaseUrl SEARCHAPI_BASE_URL /api/v1/search
searchinfinityBaseUrl SEARCHINFINITY_BASE_URL /search_api/web_search
serpapiBaseUrl SERPAPI_BASE_URL /search.json
serpbaseBaseUrl SERPBASE_BASE_URL /google/search
serpdiveBaseUrl SERPDIVE_BASE_URL /v1/search
serperBaseUrl SERPER_BASE_URL /search
serplyBaseUrl SERPLY_BASE_URL /v1/search
tabstackBaseUrl PI_TABSTACK_BASE_URL /v1/research, /v1/extract/markdown
valyuBaseUrl VALYU_BASE_URL /v1/search
xcrawlBaseUrl XCRAWL_BASE_URL /v1/serp
youBaseUrl YOU_BASE_URL /v1/search

Defaults remain the providers' official API roots. Overrides require HTTPS, except HTTP is allowed on loopback hosts (localhost, 127.0.0.0/8, and ::1). URLs with credentials, query parameters, or fragments are rejected before a request is sent; invalid overrides never fall back to an official endpoint. Cross-origin redirects remove credential headers and the api_key query parameter used by SerpApi and SerpBase. exaBaseUrl applies only to keyed direct API calls; zero-config Exa MCP search continues to use Exa's hosted MCP endpoint. parallelBaseUrl does not change the separate Parallel MCP provider.

To use a plain-HTTP gateway on another host, such as a Kubernetes Service ("braveBaseUrl": "http://search-gateway:8080/brave/res/v1"), set "allowHttpBaseUrls": true in web-search.json or PI_WEB_ACCESS_ALLOW_HTTP_BASE_URLS=true. The environment variable wins over config and accepts true, false, 1, or 0; any other value, or a non-boolean config value, is a configuration error. The switch defaults to false and lets every base URL above use http://. HTTP sends API keys and request data in plaintext, so enable it only for gateways