pi-web-providers 2.3.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +33 -0
  2. package/dist/index.js +341 -85
  3. package/package.json +12 -5
package/README.md CHANGED
@@ -13,6 +13,9 @@ off entirely.
13
13
 
14
14
  ## ✨ Features
15
15
 
16
+ - **Multiple providers**: Claude, Cloudflare, Codex, Exa, Firecrawl,
17
+ Gemini, Linkup, OpenAI, Perplexity, Parallel, Serper,
18
+ [Tavily](https://tavily.com), Valyu
16
19
  - **Provider-aware tool options**: pi only exposes the provider settings that
17
20
  actually apply to the backend you selected, so tool calls are easier to
18
21
  discover and harder to get wrong
@@ -61,6 +64,7 @@ Each tool can be routed to any compatible provider:
61
64
  | **OpenAI** | ✔ | | ✔ | ✔ | `OPENAI_API_KEY` |
62
65
  | **Parallel** | ✔ | ✔ | | | `PARALLEL_API_KEY` |
63
66
  | **Perplexity** | ✔ | | ✔ | ✔ | `PERPLEXITY_API_KEY` |
67
+ | **Serper** | ✔ | | | | `SERPER_API_KEY` |
64
68
  | **Tavily** | ✔ | ✔ | | | `TAVILY_API_KEY` |
65
69
  | **Valyu** | ✔ | ✔ | ✔ | ✔ | `VALYU_API_KEY` |
66
70
 
@@ -377,6 +381,35 @@ call.
377
381
 
378
382
  </details>
379
383
 
384
+ <details>
385
+ <summary><strong>Serper</strong></summary>
386
+
387
+ - API: Serper HTTP API
388
+ - Supports `web_search` via Serper's Google search endpoint
389
+ - Good fit for fast, straightforward Google-style organic search results
390
+ - Exposes search options `gl`, `hl`, `location`, `page`, and `autocorrect`
391
+ - Preserves rich metadata from Serper responses, including ranking position,
392
+ sitelinks, attributes, and top-level response context such as
393
+ `knowledgeGraph`, `answerBox`, `peopleAlsoAsk`, and `relatedSearches`
394
+ - Optional `baseUrl` overrides are supported for proxies and testing
395
+
396
+ Minimal config:
397
+
398
+ ```json
399
+ {
400
+ "tools": {
401
+ "search": "serper"
402
+ },
403
+ "providers": {
404
+ "serper": {
405
+ "apiKey": "SERPER_API_KEY"
406
+ }
407
+ }
408
+ }
409
+ ```
410
+
411
+ </details>
412
+
380
413
  <details>
381
414
  <summary><strong>Tavily</strong></summary>
382
415
 
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  visibleWidth,
24
24
  wrapTextWithAnsi
25
25
  } from "@mariozechner/pi-tui";
26
- import { Type as Type15 } from "@sinclair/typebox";
26
+ import { Type as Type16 } from "typebox";
27
27
 
28
28
  // src/config.ts
29
29
  import { execSync } from "node:child_process";
@@ -44,6 +44,7 @@ var PROVIDER_TOOLS_BY_ID = {
44
44
  openai: ["search", "answer", "research"],
45
45
  parallel: ["search", "contents"],
46
46
  perplexity: ["search", "answer", "research"],
47
+ serper: ["search"],
47
48
  tavily: ["search", "contents"],
48
49
  valyu: ["search", "contents", "answer", "research"]
49
50
  };
@@ -90,6 +91,7 @@ var PROVIDER_IDS = [
90
91
  "openai",
91
92
  "parallel",
92
93
  "perplexity",
94
+ "serper",
93
95
  "tavily",
94
96
  "valyu"
95
97
  ];
@@ -289,6 +291,29 @@ function normalizeProvider(providerId, raw, source) {
289
291
  case "linkup":
290
292
  case "parallel":
291
293
  case "perplexity":
294
+ return parseProviderWithShape(
295
+ raw,
296
+ source,
297
+ providerId,
298
+ {
299
+ apiKey: readOptionalString,
300
+ baseUrl: readOptionalString,
301
+ options: readOptionalObject,
302
+ settings: parseOptionalExecutionSettings
303
+ }
304
+ );
305
+ case "serper":
306
+ return parseProviderWithShape(raw, source, providerId, {
307
+ apiKey: readOptionalString,
308
+ baseUrl: readOptionalString,
309
+ options: (value, innerSource, field) => parseOptionalCapabilityOptions(
310
+ value,
311
+ innerSource,
312
+ field,
313
+ ["search"]
314
+ ),
315
+ settings: parseOptionalExecutionSettings
316
+ });
292
317
  case "tavily":
293
318
  return parseProviderWithShape(raw, source, providerId, {
294
319
  apiKey: readOptionalString,
@@ -1154,7 +1179,7 @@ function parseOptionalNonNegativeIntegerOption(options, key) {
1154
1179
  }
1155
1180
 
1156
1181
  // src/options.ts
1157
- import { Type } from "@sinclair/typebox";
1182
+ import { Type } from "typebox";
1158
1183
  var runtimeOptionFields = {
1159
1184
  requestTimeoutMs: Type.Optional(
1160
1185
  Type.Integer({
@@ -1249,7 +1274,7 @@ import { createHash } from "node:crypto";
1249
1274
  // src/providers/claude.ts
1250
1275
  import { existsSync } from "node:fs";
1251
1276
  import { query } from "@anthropic-ai/claude-agent-sdk";
1252
- import { Type as Type3 } from "@sinclair/typebox";
1277
+ import { Type as Type3 } from "typebox";
1253
1278
 
1254
1279
  // src/provider-plans.ts
1255
1280
  function createProviderPlan({
@@ -1347,7 +1372,7 @@ function asPlanConfig(config) {
1347
1372
  }
1348
1373
 
1349
1374
  // src/providers/schema.ts
1350
- import { Type as Type2 } from "@sinclair/typebox";
1375
+ import { Type as Type2 } from "typebox";
1351
1376
  function literalUnion(values, options) {
1352
1377
  return Type2.Union(
1353
1378
  values.map((value) => Type2.Literal(value)),
@@ -1723,7 +1748,7 @@ function readString(value, key) {
1723
1748
  }
1724
1749
 
1725
1750
  // src/providers/cloudflare.ts
1726
- import { Type as Type4 } from "@sinclair/typebox";
1751
+ import { Type as Type4 } from "typebox";
1727
1752
  import CloudflareClient from "cloudflare";
1728
1753
  var cloudflareContentsOptionsSchema = Type4.Object(
1729
1754
  {
@@ -1863,7 +1888,7 @@ function buildRequestOptions(context) {
1863
1888
 
1864
1889
  // src/providers/codex.ts
1865
1890
  import { Codex as CodexClient } from "@openai/codex-sdk";
1866
- import { Type as Type5 } from "@sinclair/typebox";
1891
+ import { Type as Type5 } from "typebox";
1867
1892
  var codexOutputSchema = Type5.Object({
1868
1893
  sources: Type5.Array(
1869
1894
  Type5.Object({
@@ -2468,7 +2493,7 @@ function isJsonObject2(value) {
2468
2493
  }
2469
2494
 
2470
2495
  // src/providers/exa.ts
2471
- import { Type as Type6 } from "@sinclair/typebox";
2496
+ import { Type as Type6 } from "typebox";
2472
2497
  import { Exa as ExaClient } from "exa-js";
2473
2498
  var exaSearchOptionsSchema = Type6.Object(
2474
2499
  {
@@ -2766,7 +2791,7 @@ function createClient2(config) {
2766
2791
 
2767
2792
  // src/providers/firecrawl.ts
2768
2793
  import FirecrawlClient from "@mendable/firecrawl-js";
2769
- import { Type as Type7 } from "@sinclair/typebox";
2794
+ import { Type as Type7 } from "typebox";
2770
2795
  var firecrawlSearchOptionsSchema = Type7.Object(
2771
2796
  {
2772
2797
  lang: Type7.Optional(
@@ -3061,7 +3086,7 @@ function readString2(value) {
3061
3086
 
3062
3087
  // src/providers/gemini.ts
3063
3088
  import { GoogleGenAI } from "@google/genai";
3064
- import { Type as Type8 } from "@sinclair/typebox";
3089
+ import { Type as Type8 } from "typebox";
3065
3090
  var DEFAULT_SEARCH_MODEL = "gemini-2.5-flash";
3066
3091
  var DEFAULT_ANSWER_MODEL = "gemini-2.5-flash";
3067
3092
  var DEFAULT_RESEARCH_AGENT = "deep-research-pro-preview-12-2025";
@@ -3844,7 +3869,7 @@ function readNonEmptyString3(value) {
3844
3869
  }
3845
3870
 
3846
3871
  // src/providers/linkup.ts
3847
- import { Type as Type9 } from "@sinclair/typebox";
3872
+ import { Type as Type9 } from "typebox";
3848
3873
  import {
3849
3874
  LinkupClient
3850
3875
  } from "linkup-sdk";
@@ -4097,7 +4122,7 @@ function toDate(value, name) {
4097
4122
  }
4098
4123
 
4099
4124
  // src/providers/openai.ts
4100
- import { Type as Type10 } from "@sinclair/typebox";
4125
+ import { Type as Type10 } from "typebox";
4101
4126
  import OpenAI from "openai";
4102
4127
  var DEFAULT_SEARCH_MODEL2 = "gpt-4.1";
4103
4128
  var DEFAULT_ANSWER_MODEL2 = "gpt-4.1";
@@ -4613,7 +4638,7 @@ function readInteger(value) {
4613
4638
  }
4614
4639
 
4615
4640
  // src/providers/parallel.ts
4616
- import { Type as Type11 } from "@sinclair/typebox";
4641
+ import { Type as Type11 } from "typebox";
4617
4642
  import ParallelClient from "parallel-web";
4618
4643
  var parallelSearchOptionsSchema = Type11.Object(
4619
4644
  {
@@ -4794,7 +4819,7 @@ function buildRequestOptions3(context) {
4794
4819
 
4795
4820
  // src/providers/perplexity.ts
4796
4821
  import PerplexityClient from "@perplexity-ai/perplexity_ai";
4797
- import { Type as Type12 } from "@sinclair/typebox";
4822
+ import { Type as Type12 } from "typebox";
4798
4823
  var DEFAULT_ANSWER_MODEL3 = "sonar";
4799
4824
  var DEFAULT_RESEARCH_MODEL2 = "sonar-deep-research";
4800
4825
  var perplexitySearchOptionsSchema = Type12.Object(
@@ -5107,56 +5132,287 @@ function buildRequestOptions4(context) {
5107
5132
  return context.signal ? { signal: context.signal } : void 0;
5108
5133
  }
5109
5134
 
5135
+ // src/providers/serper.ts
5136
+ import { Type as Type13 } from "typebox";
5137
+ var DEFAULT_BASE_URL = "https://google.serper.dev";
5138
+ var serperSearchOptionsSchema = Type13.Object(
5139
+ {
5140
+ gl: Type13.Optional(
5141
+ Type13.String({
5142
+ description: "Country code hint for Google results (for example 'us')."
5143
+ })
5144
+ ),
5145
+ hl: Type13.Optional(
5146
+ Type13.String({
5147
+ description: "Language code hint for Google results (for example 'en')."
5148
+ })
5149
+ ),
5150
+ location: Type13.Optional(
5151
+ Type13.String({
5152
+ description: "Geographic location hint for Google results."
5153
+ })
5154
+ ),
5155
+ page: Type13.Optional(
5156
+ Type13.Integer({
5157
+ minimum: 1,
5158
+ description: "1-based results page to request from Serper."
5159
+ })
5160
+ ),
5161
+ autocorrect: Type13.Optional(
5162
+ Type13.Boolean({
5163
+ description: "Enable or disable Serper query autocorrection."
5164
+ })
5165
+ )
5166
+ },
5167
+ { description: "Serper search options." }
5168
+ );
5169
+ var serperAdapter = {
5170
+ id: "serper",
5171
+ label: "Serper",
5172
+ docsUrl: "https://serper.dev/",
5173
+ tools: ["search"],
5174
+ getToolOptionsSchema(capability) {
5175
+ switch (capability) {
5176
+ case "search":
5177
+ return serperSearchOptionsSchema;
5178
+ default:
5179
+ return void 0;
5180
+ }
5181
+ },
5182
+ createTemplate() {
5183
+ return {
5184
+ apiKey: "SERPER_API_KEY",
5185
+ options: {}
5186
+ };
5187
+ },
5188
+ getConfigForCapability(capability, config) {
5189
+ switch (capability) {
5190
+ case "search":
5191
+ return {
5192
+ apiKey: config.apiKey,
5193
+ baseUrl: config.baseUrl,
5194
+ options: config.options?.search,
5195
+ settings: config.settings
5196
+ };
5197
+ default:
5198
+ return config;
5199
+ }
5200
+ },
5201
+ getCapabilityStatus(config) {
5202
+ return getApiKeyStatus(config?.apiKey);
5203
+ },
5204
+ buildPlan(request, config) {
5205
+ return buildProviderPlan({
5206
+ request,
5207
+ config,
5208
+ providerId: serperAdapter.id,
5209
+ providerLabel: serperAdapter.label,
5210
+ handlers: {
5211
+ search: {
5212
+ execute: (searchRequest, providerConfig, context) => serperAdapter.search(
5213
+ searchRequest.query,
5214
+ searchRequest.maxResults,
5215
+ providerConfig,
5216
+ context,
5217
+ searchRequest.options
5218
+ )
5219
+ }
5220
+ }
5221
+ });
5222
+ },
5223
+ async search(query2, maxResults, config, context, options) {
5224
+ const apiKey = resolveConfigValue(config.apiKey);
5225
+ if (!apiKey) {
5226
+ throw new Error("is missing an API key");
5227
+ }
5228
+ const defaults = stripLocalExecutionOptions(asJsonObject(config.options?.search)) ?? {};
5229
+ const runtimeOptions = stripLocalExecutionOptions(asJsonObject(options));
5230
+ const {
5231
+ q: _ignoredQuery,
5232
+ num: _ignoredNum,
5233
+ ...providerOptions
5234
+ } = {
5235
+ ...defaults,
5236
+ ...runtimeOptions ?? {}
5237
+ };
5238
+ const response = await fetch(joinUrl(resolveConfigValue(config.baseUrl)), {
5239
+ method: "POST",
5240
+ headers: {
5241
+ "content-type": "application/json",
5242
+ "x-api-key": apiKey
5243
+ },
5244
+ body: JSON.stringify({
5245
+ q: query2,
5246
+ num: clampMaxResults(maxResults),
5247
+ ...providerOptions
5248
+ }),
5249
+ signal: context.signal
5250
+ });
5251
+ if (!response.ok) {
5252
+ throw new Error(await buildHttpError(response));
5253
+ }
5254
+ const payload = await response.json();
5255
+ const responseRecord = asRecord3(payload) ?? {};
5256
+ const organic = asArray(responseRecord.organic) ?? [];
5257
+ const searchContext = buildSearchContext(responseRecord);
5258
+ return {
5259
+ provider: serperAdapter.id,
5260
+ results: organic.map((entry) => toSearchResult3(entry, searchContext)).filter(
5261
+ (result) => result !== null
5262
+ ).slice(0, clampMaxResults(maxResults))
5263
+ };
5264
+ }
5265
+ };
5266
+ function joinUrl(baseUrl) {
5267
+ const base = (baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
5268
+ return `${base}/search`;
5269
+ }
5270
+ function clampMaxResults(value) {
5271
+ return Math.max(1, Math.min(20, Math.trunc(value || 0)));
5272
+ }
5273
+ async function buildHttpError(response) {
5274
+ const detail = await readErrorDetail(response);
5275
+ const status = `${response.status}${response.statusText ? ` ${response.statusText}` : ""}`;
5276
+ return detail ? `Serper API request failed (${status}): ${detail}` : `Serper API request failed (${status}).`;
5277
+ }
5278
+ async function readErrorDetail(response) {
5279
+ const text = (await response.text()).trim();
5280
+ if (!text) {
5281
+ return void 0;
5282
+ }
5283
+ try {
5284
+ const parsed = JSON.parse(text);
5285
+ const record = asRecord3(parsed);
5286
+ const detail = readString4(record?.message) ?? readString4(record?.error) ?? readString4(record?.detail);
5287
+ if (detail) {
5288
+ return detail;
5289
+ }
5290
+ return JSON.stringify(parsed);
5291
+ } catch {
5292
+ return text;
5293
+ }
5294
+ }
5295
+ function toSearchResult3(entry, searchContext) {
5296
+ const record = asRecord3(entry);
5297
+ if (!record) {
5298
+ return null;
5299
+ }
5300
+ const url = readString4(record.link) ?? "";
5301
+ const title = readString4(record.title) || url || "Untitled";
5302
+ const snippet = trimSnippet(
5303
+ readString4(record.snippet) ?? readString4(record.richSnippet) ?? readString4(record.date) ?? ""
5304
+ );
5305
+ const metadata = omitUndefined({
5306
+ source: "organic",
5307
+ position: readNumber(record.position),
5308
+ date: readString4(record.date),
5309
+ attributes: asRecord3(record.attributes),
5310
+ sitelinks: asArray(record.sitelinks),
5311
+ rating: readNumber(record.rating),
5312
+ ratingCount: readNumber(record.ratingCount),
5313
+ cid: readString4(record.cid),
5314
+ ...extractExtraMetadata(record, ["title", "link", "snippet"]),
5315
+ ...searchContext ? { searchContext } : {}
5316
+ });
5317
+ return {
5318
+ title,
5319
+ url,
5320
+ snippet,
5321
+ ...Object.keys(metadata).length > 0 ? { metadata } : {}
5322
+ };
5323
+ }
5324
+ function buildSearchContext(response) {
5325
+ const context = omitUndefined({
5326
+ searchParameters: asRecord3(response.searchParameters),
5327
+ searchInformation: asRecord3(response.searchInformation),
5328
+ credits: readNumber(response.credits),
5329
+ answerBox: asRecord3(response.answerBox),
5330
+ knowledgeGraph: asRecord3(response.knowledgeGraph),
5331
+ peopleAlsoAsk: asArray(response.peopleAlsoAsk),
5332
+ relatedSearches: asArray(response.relatedSearches),
5333
+ topStories: asArray(response.topStories),
5334
+ news: asArray(response.news),
5335
+ images: asArray(response.images),
5336
+ videos: asArray(response.videos),
5337
+ places: asArray(response.places)
5338
+ });
5339
+ return Object.keys(context).length > 0 ? context : void 0;
5340
+ }
5341
+ function extractExtraMetadata(record, ignoredKeys) {
5342
+ return Object.fromEntries(
5343
+ Object.entries(record).filter(
5344
+ ([key, value]) => !ignoredKeys.includes(key) && value !== void 0
5345
+ )
5346
+ );
5347
+ }
5348
+ function omitUndefined(value) {
5349
+ return Object.fromEntries(
5350
+ Object.entries(value).filter(([, entry]) => entry !== void 0)
5351
+ );
5352
+ }
5353
+ function asRecord3(value) {
5354
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
5355
+ }
5356
+ function asArray(value) {
5357
+ return Array.isArray(value) ? value : void 0;
5358
+ }
5359
+ function readString4(value) {
5360
+ return typeof value === "string" ? value : void 0;
5361
+ }
5362
+ function readNumber(value) {
5363
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
5364
+ }
5365
+
5110
5366
  // src/providers/tavily.ts
5111
- import { Type as Type13 } from "@sinclair/typebox";
5367
+ import { Type as Type14 } from "typebox";
5112
5368
  import {
5113
5369
  tavily
5114
5370
  } from "@tavily/core";
5115
- var tavilySearchOptionsSchema = Type13.Object(
5371
+ var tavilySearchOptionsSchema = Type14.Object(
5116
5372
  {
5117
- topic: Type13.Optional(
5373
+ topic: Type14.Optional(
5118
5374
  literalUnion(["general", "news", "finance"], {
5119
5375
  description: "Category of the search query."
5120
5376
  })
5121
5377
  ),
5122
- searchDepth: Type13.Optional(
5378
+ searchDepth: Type14.Optional(
5123
5379
  literalUnion(["basic", "advanced"], {
5124
5380
  description: "Depth of the search. 'advanced' is slower but more thorough."
5125
5381
  })
5126
5382
  ),
5127
- timeRange: Type13.Optional(
5128
- Type13.String({ description: "Named time range filter." })
5383
+ timeRange: Type14.Optional(
5384
+ Type14.String({ description: "Named time range filter." })
5129
5385
  ),
5130
- country: Type13.Optional(
5131
- Type13.String({ description: "Country hint for search results." })
5386
+ country: Type14.Optional(
5387
+ Type14.String({ description: "Country hint for search results." })
5132
5388
  ),
5133
- exactMatch: Type13.Optional(
5134
- Type13.Boolean({ description: "Prefer exact matches." })
5389
+ exactMatch: Type14.Optional(
5390
+ Type14.Boolean({ description: "Prefer exact matches." })
5135
5391
  ),
5136
- includeAnswer: Type13.Optional(
5137
- Type13.Boolean({ description: "Include a short AI-generated answer." })
5392
+ includeAnswer: Type14.Optional(
5393
+ Type14.Boolean({ description: "Include a short AI-generated answer." })
5138
5394
  ),
5139
- includeRawContent: Type13.Optional(
5140
- Type13.Boolean({ description: "Include raw page content in results." })
5395
+ includeRawContent: Type14.Optional(
5396
+ Type14.Boolean({ description: "Include raw page content in results." })
5141
5397
  ),
5142
- includeImages: Type13.Optional(
5143
- Type13.Boolean({ description: "Include related images." })
5398
+ includeImages: Type14.Optional(
5399
+ Type14.Boolean({ description: "Include related images." })
5144
5400
  ),
5145
- includeFavicon: Type13.Optional(
5146
- Type13.Boolean({ description: "Include favicon URLs." })
5401
+ includeFavicon: Type14.Optional(
5402
+ Type14.Boolean({ description: "Include favicon URLs." })
5147
5403
  ),
5148
- includeDomains: Type13.Optional(
5149
- Type13.Array(Type13.String(), {
5404
+ includeDomains: Type14.Optional(
5405
+ Type14.Array(Type14.String(), {
5150
5406
  description: "Restrict results to these domains."
5151
5407
  })
5152
5408
  ),
5153
- excludeDomains: Type13.Optional(
5154
- Type13.Array(Type13.String(), {
5409
+ excludeDomains: Type14.Optional(
5410
+ Type14.Array(Type14.String(), {
5155
5411
  description: "Exclude these domains from results."
5156
5412
  })
5157
5413
  ),
5158
- days: Type13.Optional(
5159
- Type13.Integer({
5414
+ days: Type14.Optional(
5415
+ Type14.Integer({
5160
5416
  minimum: 1,
5161
5417
  description: "Limit results to the last N days."
5162
5418
  })
@@ -5164,27 +5420,27 @@ var tavilySearchOptionsSchema = Type13.Object(
5164
5420
  },
5165
5421
  { description: "Tavily search options." }
5166
5422
  );
5167
- var tavilyExtractOptionsSchema = Type13.Object(
5423
+ var tavilyExtractOptionsSchema = Type14.Object(
5168
5424
  {
5169
- extractDepth: Type13.Optional(
5170
- Type13.String({ description: "Depth setting for extraction." })
5425
+ extractDepth: Type14.Optional(
5426
+ Type14.String({ description: "Depth setting for extraction." })
5171
5427
  ),
5172
- format: Type13.Optional(
5428
+ format: Type14.Optional(
5173
5429
  literalUnion(["markdown", "text"], {
5174
5430
  description: "Output format for extracted content."
5175
5431
  })
5176
5432
  ),
5177
- includeImages: Type13.Optional(
5178
- Type13.Boolean({ description: "Include extracted images." })
5433
+ includeImages: Type14.Optional(
5434
+ Type14.Boolean({ description: "Include extracted images." })
5179
5435
  ),
5180
- query: Type13.Optional(
5181
- Type13.String({ description: "Optional query to focus extraction." })
5436
+ query: Type14.Optional(
5437
+ Type14.String({ description: "Optional query to focus extraction." })
5182
5438
  ),
5183
- chunksPerSource: Type13.Optional(
5184
- Type13.Integer({ minimum: 1, description: "Maximum chunks per source." })
5439
+ chunksPerSource: Type14.Optional(
5440
+ Type14.Integer({ minimum: 1, description: "Maximum chunks per source." })
5185
5441
  ),
5186
- includeFavicon: Type13.Optional(
5187
- Type13.Boolean({ description: "Include favicon URLs." })
5442
+ includeFavicon: Type14.Optional(
5443
+ Type14.Boolean({ description: "Include favicon URLs." })
5188
5444
  )
5189
5445
  },
5190
5446
  { description: "Tavily extract options." }
@@ -5358,48 +5614,48 @@ function buildExtractMetadata(response, result) {
5358
5614
  }
5359
5615
 
5360
5616
  // src/providers/valyu.ts
5361
- import { Type as Type14 } from "@sinclair/typebox";
5617
+ import { Type as Type15 } from "typebox";
5362
5618
  import { Valyu as ValyuClient } from "valyu-js";
5363
- var valyuSearchOptionsSchema = Type14.Object(
5619
+ var valyuSearchOptionsSchema = Type15.Object(
5364
5620
  {
5365
- searchType: Type14.Optional(
5621
+ searchType: Type15.Optional(
5366
5622
  literalUnion(["all", "web", "proprietary", "news"], {
5367
5623
  description: "Valyu search type."
5368
5624
  })
5369
5625
  ),
5370
- responseLength: Type14.Optional(
5626
+ responseLength: Type15.Optional(
5371
5627
  literalUnion(["short", "medium", "large", "max"], {
5372
5628
  description: "Response length."
5373
5629
  })
5374
5630
  ),
5375
- countryCode: Type14.Optional(
5376
- Type14.String({ description: "Country code to scope search results." })
5631
+ countryCode: Type15.Optional(
5632
+ Type15.String({ description: "Country code to scope search results." })
5377
5633
  )
5378
5634
  },
5379
5635
  { description: "Valyu search options." }
5380
5636
  );
5381
- var valyuAnswerOptionsSchema = Type14.Object(
5637
+ var valyuAnswerOptionsSchema = Type15.Object(
5382
5638
  {
5383
- responseLength: Type14.Optional(
5639
+ responseLength: Type15.Optional(
5384
5640
  literalUnion(["short", "medium", "large", "max"], {
5385
5641
  description: "Response length for answers."
5386
5642
  })
5387
5643
  ),
5388
- countryCode: Type14.Optional(
5389
- Type14.String({ description: "Country code to scope answer results." })
5644
+ countryCode: Type15.Optional(
5645
+ Type15.String({ description: "Country code to scope answer results." })
5390
5646
  )
5391
5647
  },
5392
5648
  { description: "Valyu answer options." }
5393
5649
  );
5394
- var valyuResearchOptionsSchema = Type14.Object(
5650
+ var valyuResearchOptionsSchema = Type15.Object(
5395
5651
  {
5396
- responseLength: Type14.Optional(
5652
+ responseLength: Type15.Optional(
5397
5653
  literalUnion(["short", "medium", "large", "max"], {
5398
5654
  description: "Response length for research."
5399
5655
  })
5400
5656
  ),
5401
- countryCode: Type14.Optional(
5402
- Type14.String({ description: "Country code to scope research results." })
5657
+ countryCode: Type15.Optional(
5658
+ Type15.String({ description: "Country code to scope research results." })
5403
5659
  )
5404
5660
  },
5405
5661
  { description: "Valyu research options." }
@@ -5684,6 +5940,7 @@ var ADAPTERS_BY_ID = {
5684
5940
  openai: openaiAdapter,
5685
5941
  parallel: parallelAdapter,
5686
5942
  perplexity: perplexityAdapter,
5943
+ serper: serperAdapter,
5687
5944
  tavily: tavilyAdapter,
5688
5945
  valyu: valyuAdapter
5689
5946
  };
@@ -6780,7 +7037,7 @@ var PROVIDER_CONFIG_MANIFESTS = {
6780
7037
  "deep-reasoning",
6781
7038
  "deep-max"
6782
7039
  ],
6783
- getValue: (config) => readString4(getExaSearchOptions(config)?.type) ?? "default",
7040
+ getValue: (config) => readString5(getExaSearchOptions(config)?.type) ?? "default",
6784
7041
  setValue: (config, value) => {
6785
7042
  const options = ensureExaSearchOptions(config);
6786
7043
  if (value === "default") {
@@ -7004,7 +7261,7 @@ var PROVIDER_CONFIG_MANIFESTS = {
7004
7261
  label: "Search mode",
7005
7262
  help: "Parallel search mode. 'default' uses the SDK default.",
7006
7263
  values: ["default", "agentic", "one-shot"],
7007
- getValue: (config) => readString4(getParallelOptions(config)?.search?.mode) ?? "default",
7264
+ getValue: (config) => readString5(getParallelOptions(config)?.search?.mode) ?? "default",
7008
7265
  setValue: (config, value) => {
7009
7266
  const options = ensureParallelOptions(config);
7010
7267
  options.search = asJsonObject2(options.search) ?? {};
@@ -7058,6 +7315,9 @@ var PROVIDER_CONFIG_MANIFESTS = {
7058
7315
  })
7059
7316
  ]
7060
7317
  },
7318
+ serper: {
7319
+ settings: [apiKeySetting(), baseUrlSetting()]
7320
+ },
7061
7321
  tavily: {
7062
7322
  settings: [apiKeySetting(), baseUrlSetting()]
7063
7323
  },
@@ -7070,7 +7330,7 @@ var PROVIDER_CONFIG_MANIFESTS = {
7070
7330
  label: "Search type",
7071
7331
  help: "Valyu search type. 'default' uses the SDK default.",
7072
7332
  values: ["default", "all", "web", "proprietary", "news"],
7073
- getValue: (config) => readString4(getValyuCapabilityOptions(config, "search")?.searchType) ?? "default",
7333
+ getValue: (config) => readString5(getValyuCapabilityOptions(config, "search")?.searchType) ?? "default",
7074
7334
  setValue: (config, value) => {
7075
7335
  const options = ensureValyuCapabilityOptions(config, "search");
7076
7336
  if (value === "default") {
@@ -7086,7 +7346,7 @@ var PROVIDER_CONFIG_MANIFESTS = {
7086
7346
  label: "Search response length",
7087
7347
  help: "Valyu search response length. 'default' uses the SDK default.",
7088
7348
  values: ["default", "short", "medium", "large", "max"],
7089
- getValue: (config) => readString4(
7349
+ getValue: (config) => readString5(
7090
7350
  getValyuCapabilityOptions(config, "search")?.responseLength
7091
7351
  ) ?? "default",
7092
7352
  setValue: (config, value) => {
@@ -7098,7 +7358,7 @@ var PROVIDER_CONFIG_MANIFESTS = {
7098
7358
  label: "Answer response length",
7099
7359
  help: "Valyu answer response length. 'default' uses the SDK default.",
7100
7360
  values: ["default", "short", "medium", "large", "max"],
7101
- getValue: (config) => readString4(
7361
+ getValue: (config) => readString5(
7102
7362
  getValyuCapabilityOptions(config, "answer")?.responseLength
7103
7363
  ) ?? "default",
7104
7364
  setValue: (config, value) => {
@@ -7110,7 +7370,7 @@ var PROVIDER_CONFIG_MANIFESTS = {
7110
7370
  label: "Research response length",
7111
7371
  help: "Valyu research response length. 'default' uses the SDK default.",
7112
7372
  values: ["default", "short", "medium", "large", "max"],
7113
- getValue: (config) => readString4(
7373
+ getValue: (config) => readString5(
7114
7374
  getValyuCapabilityOptions(config, "research")?.responseLength
7115
7375
  ) ?? "default",
7116
7376
  setValue: (config, value) => {
@@ -7202,7 +7462,7 @@ function getIntegerString(value) {
7202
7462
  function getBooleanValue(value) {
7203
7463
  return typeof value === "boolean" ? String(value) : "default";
7204
7464
  }
7205
- function readString4(value) {
7465
+ function readString5(value) {
7206
7466
  return typeof value === "string" ? value : void 0;
7207
7467
  }
7208
7468
  function asJsonObject2(value) {
@@ -7528,10 +7788,6 @@ function webProvidersExtension(pi) {
7528
7788
  { addAvailable: true }
7529
7789
  );
7530
7790
  });
7531
- pi.on("session_switch", async (_event, ctx) => {
7532
- latestWidgetContext = ctx;
7533
- updateWebResearchWidget(ctx);
7534
- });
7535
7791
  pi.on("before_agent_start", async (_event, ctx) => {
7536
7792
  latestWidgetContext = ctx;
7537
7793
  await cleanupContentStore();
@@ -7577,14 +7833,14 @@ function registerWebSearchTool(pi, providerIds) {
7577
7833
  promptGuidelines: [
7578
7834
  "Batch related searches when grouped comparison matters; use separate sibling web_search calls when independent results should surface as soon as they are ready."
7579
7835
  ],
7580
- parameters: Type15.Object({
7581
- queries: Type15.Array(Type15.String({ minLength: 1 }), {
7836
+ parameters: Type16.Object({
7837
+ queries: Type16.Array(Type16.String({ minLength: 1 }), {
7582
7838
  minItems: 1,
7583
7839
  maxItems: MAX_SEARCH_QUERIES,
7584
7840
  description: `One or more search queries to run in one call (max ${MAX_SEARCH_QUERIES})`
7585
7841
  }),
7586
- maxResults: Type15.Optional(
7587
- Type15.Integer({
7842
+ maxResults: Type16.Optional(
7843
+ Type16.Integer({
7588
7844
  minimum: 1,
7589
7845
  maximum: MAX_ALLOWED_RESULTS,
7590
7846
  description: `Maximum number of results to return (default: ${DEFAULT_MAX_RESULTS})`
@@ -7633,8 +7889,8 @@ function registerWebContentsTool(pi, providerIds) {
7633
7889
  name: "web_contents",
7634
7890
  label: "Web Contents",
7635
7891
  description: "Read and extract the main contents of one or more web pages. Batch related pages together, or use separate sibling calls when each page can be acted on independently.",
7636
- parameters: Type15.Object({
7637
- urls: Type15.Array(Type15.String({ minLength: 1 }), {
7892
+ parameters: Type16.Object({
7893
+ urls: Type16.Array(Type16.String({ minLength: 1 }), {
7638
7894
  minItems: 1,
7639
7895
  description: "One or more URLs to extract"
7640
7896
  }),
@@ -7684,8 +7940,8 @@ function registerWebAnswerTool(pi, providerIds) {
7684
7940
  name: "web_answer",
7685
7941
  label: "Web Answer",
7686
7942
  description: `Answer one or more questions using web-grounded evidence (up to ${MAX_SEARCH_QUERIES} per call).`,
7687
- parameters: Type15.Object({
7688
- queries: Type15.Array(Type15.String({ minLength: 1 }), {
7943
+ parameters: Type16.Object({
7944
+ queries: Type16.Array(Type16.String({ minLength: 1 }), {
7689
7945
  minItems: 1,
7690
7946
  maxItems: MAX_SEARCH_QUERIES,
7691
7947
  description: `One or more questions to answer in one call (max ${MAX_SEARCH_QUERIES})`
@@ -7739,8 +7995,8 @@ function registerWebResearchTool(pi, webResearchLifecycle, providerIds) {
7739
7995
  name: "web_research",
7740
7996
  label: "Web Research",
7741
7997
  description: "Start a long-running web research job. Returns immediately with a dispatch notice; the final report is saved to a file and posted later as a custom message.",
7742
- parameters: Type15.Object({
7743
- input: Type15.String({ description: "Research brief or question" }),
7998
+ parameters: Type16.Object({
7999
+ input: Type16.String({ description: "Research brief or question" }),
7744
8000
  ...optionalField(
7745
8001
  "options",
7746
8002
  buildStructuredOptionsSchema("research", selectedProviderId)
@@ -7883,7 +8139,7 @@ function optionalField(name, schema) {
7883
8139
  function buildStructuredOptionsSchema(capability, providerId) {
7884
8140
  const providerSchema = resolveProviderOptionsSchema(capability, providerId);
7885
8141
  const schema = buildToolOptionsSchema(capability, providerSchema);
7886
- return schema ? Type15.Optional(schema) : void 0;
8142
+ return schema ? Type16.Optional(schema) : void 0;
7887
8143
  }
7888
8144
  function resolveProviderOptionsSchema(capability, providerId) {
7889
8145
  if (!providerId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-web-providers",
3
- "version": "2.3.0",
3
+ "version": "2.4.1",
4
4
  "description": "Configurable web access extension for pi with per-tool provider routing and explicit provider option schemas for search, contents, answers, and research.",
5
5
  "type": "module",
6
6
  "files": [
@@ -30,6 +30,7 @@
30
30
  "openai",
31
31
  "parallel",
32
32
  "perplexity",
33
+ "serper",
33
34
  "tavily",
34
35
  "valyu"
35
36
  ],
@@ -51,7 +52,7 @@
51
52
  ]
52
53
  },
53
54
  "scripts": {
54
- "build": "rm -rf dist && esbuild src/index.ts --bundle --format=esm --platform=node --outfile=dist/index.js --external:@mariozechner/pi-coding-agent --external:@mariozechner/pi-ai --external:@mariozechner/pi-tui --external:@sinclair/typebox --external:@anthropic-ai/claude-agent-sdk --external:@google/genai --external:@mendable/firecrawl-js --external:@openai/codex-sdk --external:@perplexity-ai/perplexity_ai --external:@tavily/core --external:cloudflare --external:exa-js --external:linkup-sdk --external:openai --external:parallel-web --external:valyu-js",
55
+ "build": "rm -rf dist && esbuild src/index.ts --bundle --format=esm --platform=node --outfile=dist/index.js --external:@mariozechner/pi-coding-agent --external:@mariozechner/pi-ai --external:@mariozechner/pi-tui --external:typebox --external:@anthropic-ai/claude-agent-sdk --external:@google/genai --external:@mendable/firecrawl-js --external:@openai/codex-sdk --external:@perplexity-ai/perplexity_ai --external:@tavily/core --external:cloudflare --external:exa-js --external:linkup-sdk --external:openai --external:parallel-web --external:valyu-js",
55
56
  "prepare": "npm run build",
56
57
  "prepack": "npm run build",
57
58
  "check": "tsc --noEmit",
@@ -77,12 +78,18 @@
77
78
  },
78
79
  "devDependencies": {
79
80
  "@biomejs/biome": "^2.4.10",
80
- "@mariozechner/pi-ai": "^0.64.0",
81
- "@mariozechner/pi-coding-agent": "^0.64.0",
82
- "@sinclair/typebox": "^0.34.49",
81
+ "@mariozechner/pi-ai": "^0.69.0",
82
+ "@mariozechner/pi-coding-agent": "^0.69.0",
83
+ "@mariozechner/pi-tui": "^0.69.0",
84
+ "typebox": "^1.1.31",
83
85
  "@types/node": "^25.5.0",
84
86
  "esbuild": "^0.27.4",
85
87
  "typescript": "^6.0.2",
86
88
  "vitest": "^4.1.2"
89
+ },
90
+ "peerDependencies": {
91
+ "@mariozechner/pi-coding-agent": "*",
92
+ "@mariozechner/pi-tui": "*",
93
+ "typebox": "*"
87
94
  }
88
95
  }