webseek 0.3.0 → 0.5.0

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.
@@ -99,6 +99,183 @@ function resolveGeminiConfig(params) {
99
99
  };
100
100
  }
101
101
  //#endregion
102
+ //#region src/pricing/pricing.ts
103
+ /** OpenAI's `web_search` tool: $10 per 1k calls; search content tokens bill at model rates. */
104
+ const OPENAI_SEARCH_PER_1K = 10;
105
+ const OPENAI_PRICES = {
106
+ "gpt-5.5": openai({
107
+ input: 5,
108
+ cachedInput: .5,
109
+ output: 30
110
+ }),
111
+ "gpt-5.4": openai({
112
+ input: 2.5,
113
+ cachedInput: .25,
114
+ output: 15
115
+ }),
116
+ "gpt-5": openai({
117
+ input: 1.25,
118
+ cachedInput: .125,
119
+ output: 10
120
+ }),
121
+ "gpt-5-mini": openai({
122
+ input: .25,
123
+ cachedInput: .025,
124
+ output: 2
125
+ }),
126
+ "gpt-5-nano": openai({
127
+ input: .05,
128
+ cachedInput: .005,
129
+ output: .4
130
+ }),
131
+ "gpt-4.1": openai({
132
+ input: 2,
133
+ cachedInput: .5,
134
+ output: 8
135
+ }),
136
+ "gpt-4o": openai({
137
+ input: 2.5,
138
+ cachedInput: 1.25,
139
+ output: 10
140
+ }),
141
+ "gpt-4o-mini": openai({
142
+ input: .15,
143
+ cachedInput: .075,
144
+ output: .6
145
+ })
146
+ };
147
+ /**
148
+ * Gemini 3.x: "$14 per 1,000 requests", read as one charge per search query the
149
+ * model ran (each `webSearchQueries` entry), not per prompt.
150
+ */
151
+ const GEMINI_3_SEARCH_PER_1K = 14;
152
+ /** Gemini 2.5 bills each grounded prompt, however many queries it ran ($35 per 1k). */
153
+ const GEMINI_2_5_SEARCH_PER_1K = 35;
154
+ const GEMINI_3_FLASH = {
155
+ rates: {
156
+ input: .75,
157
+ cachedInput: .075,
158
+ output: 3.75
159
+ },
160
+ next: {
161
+ from: "2027-01-01",
162
+ rates: {
163
+ input: 1.5,
164
+ cachedInput: .15,
165
+ output: 7.5
166
+ }
167
+ },
168
+ searchPer1k: GEMINI_3_SEARCH_PER_1K
169
+ };
170
+ const GEMINI_PRICES = {
171
+ "gemini-3.8-flash": GEMINI_3_FLASH,
172
+ "gemini-3.7-flash": GEMINI_3_FLASH,
173
+ "gemini-3.6-flash": GEMINI_3_FLASH,
174
+ "gemini-3-flash-preview": GEMINI_3_FLASH,
175
+ "gemini-3.5-flash": gemini3({
176
+ input: 1.5,
177
+ cachedInput: .15,
178
+ output: 9
179
+ }),
180
+ "gemini-3.5-flash-lite": gemini3({
181
+ input: .3,
182
+ cachedInput: .03,
183
+ output: 2.5
184
+ }),
185
+ "gemini-3.1-flash-lite": gemini3({
186
+ input: .25,
187
+ cachedInput: .025,
188
+ output: 1.5
189
+ }),
190
+ "gemini-3.1-pro-preview": gemini3({
191
+ input: 2,
192
+ cachedInput: .2,
193
+ output: 12
194
+ }),
195
+ "gemini-2.5-pro": gemini25({
196
+ input: 1.25,
197
+ cachedInput: .125,
198
+ output: 10
199
+ }),
200
+ "gemini-2.5-flash": gemini25({
201
+ input: .3,
202
+ cachedInput: .03,
203
+ output: 2.5
204
+ }),
205
+ "gemini-2.5-flash-lite": gemini25({
206
+ input: .1,
207
+ cachedInput: .01,
208
+ output: .4
209
+ })
210
+ };
211
+ /** Google Custom Search: $5 per 1k queries (each paginated request is a query). */
212
+ const GOOGLE_CSE_PER_1K = 5;
213
+ function openai(rates) {
214
+ return {
215
+ rates,
216
+ searchPer1k: OPENAI_SEARCH_PER_1K
217
+ };
218
+ }
219
+ function gemini3(rates) {
220
+ return {
221
+ rates,
222
+ searchPer1k: GEMINI_3_SEARCH_PER_1K
223
+ };
224
+ }
225
+ function gemini25(rates) {
226
+ return {
227
+ rates,
228
+ searchPer1k: GEMINI_2_5_SEARCH_PER_1K,
229
+ searchPerPrompt: true
230
+ };
231
+ }
232
+ /**
233
+ * Estimate what a search cost at list price, or `undefined` when it cannot be
234
+ * priced: the model is not in this table (e.g. newer than it), or an LLM-backed
235
+ * provider reported no token counts (a search-fee-only figure would understate).
236
+ */
237
+ function estimateCost(params) {
238
+ const { usage } = params;
239
+ if (params.provider === "google") {
240
+ const searchUsd = usage.searchCalls * GOOGLE_CSE_PER_1K / 1e3;
241
+ return {
242
+ totalUsd: searchUsd,
243
+ tokensUsd: 0,
244
+ searchUsd
245
+ };
246
+ }
247
+ const table = params.provider === "openai" ? OPENAI_PRICES : GEMINI_PRICES;
248
+ const price = [params.model, params.fallbackModel].filter((model) => model !== void 0).map((model) => lookupPrice({
249
+ table,
250
+ model
251
+ })).find((found) => found !== void 0);
252
+ if (price === void 0) return;
253
+ if (usage.inputTokens === void 0 && usage.outputTokens === void 0) return;
254
+ const rates = ratesAt({
255
+ price,
256
+ now: params.now ?? /* @__PURE__ */ new Date()
257
+ });
258
+ const inputTokens = usage.inputTokens ?? 0;
259
+ const cachedInputTokens = Math.min(usage.cachedInputTokens ?? 0, inputTokens);
260
+ const tokensUsd = ((inputTokens - cachedInputTokens) * rates.input + cachedInputTokens * rates.cachedInput + (usage.outputTokens ?? 0) * rates.output) / 1e6;
261
+ const searchUsd = (price.searchPerPrompt ? Math.min(usage.searchCalls, 1) : usage.searchCalls) * price.searchPer1k / 1e3;
262
+ return {
263
+ totalUsd: tokensUsd + searchUsd,
264
+ tokensUsd,
265
+ searchUsd
266
+ };
267
+ }
268
+ function lookupPrice(params) {
269
+ const { table } = params;
270
+ const id = params.model.replace(/^(?:.*\/)?models\//, "").replace(/-\d{4}-\d{2}-\d{2}$/, "").toLowerCase();
271
+ return Object.hasOwn(table, id) ? table[id] : void 0;
272
+ }
273
+ function ratesAt(params) {
274
+ const { price } = params;
275
+ if (price.next && params.now.toISOString().slice(0, 10) >= price.next.from) return price.next.rates;
276
+ return price.rates;
277
+ }
278
+ //#endregion
102
279
  //#region src/providers/gemini.ts
103
280
  /**
104
281
  * Gemini web search provider via "Grounding with Google Search".
@@ -151,8 +328,18 @@ const candidateSchema = zod.z.looseObject({
151
328
  content: zod.z.looseObject({ parts: zod.z.array(zod.z.looseObject({ text: zod.z.string().optional() })).optional() }).optional(),
152
329
  groundingMetadata: groundingMetadataSchema.optional()
153
330
  });
331
+ const usageMetadataSchema = zod.z.looseObject({
332
+ promptTokenCount: zod.z.number().optional(),
333
+ cachedContentTokenCount: zod.z.number().optional(),
334
+ candidatesTokenCount: zod.z.number().optional(),
335
+ thoughtsTokenCount: zod.z.number().optional(),
336
+ toolUsePromptTokenCount: zod.z.number().optional(),
337
+ totalTokenCount: zod.z.number().optional()
338
+ });
154
339
  const responseSchema$2 = zod.z.looseObject({
155
340
  candidates: zod.z.array(candidateSchema).optional(),
341
+ usageMetadata: usageMetadataSchema.optional(),
342
+ modelVersion: zod.z.string().optional(),
156
343
  error: zod.z.looseObject({ message: zod.z.string().optional() }).optional()
157
344
  });
158
345
  function createGeminiProvider(params) {
@@ -183,6 +370,11 @@ function createGeminiProvider(params) {
183
370
  body
184
371
  });
185
372
  const { answer, citations, searchQueries } = extract$1(parsed.data);
373
+ const servedModel = parsed.data.modelVersion ?? model;
374
+ const usage = toUsage$1({
375
+ usage: parsed.data.usageMetadata,
376
+ searchCalls: searchQueries.length
377
+ });
186
378
  return {
187
379
  provider: "gemini",
188
380
  query: searchParams.query,
@@ -190,6 +382,14 @@ function createGeminiProvider(params) {
190
382
  answer,
191
383
  citations,
192
384
  searchQueries,
385
+ model: servedModel,
386
+ usage,
387
+ cost: estimateCost({
388
+ provider: "gemini",
389
+ model: servedModel,
390
+ fallbackModel: model,
391
+ usage
392
+ }),
193
393
  raw: searchParams.includeRaw ? body : void 0
194
394
  };
195
395
  }
@@ -208,6 +408,23 @@ function extract$1(data) {
208
408
  searchQueries: metadata?.webSearchQueries ?? []
209
409
  };
210
410
  }
411
+ function toUsage$1(params) {
412
+ const { usage } = params;
413
+ if (usage === void 0) return { searchCalls: params.searchCalls };
414
+ const inputTokens = sumDefined([usage.promptTokenCount, usage.toolUsePromptTokenCount]);
415
+ const outputTokens = sumDefined([usage.candidatesTokenCount, usage.thoughtsTokenCount]);
416
+ return {
417
+ inputTokens,
418
+ cachedInputTokens: usage.cachedContentTokenCount,
419
+ outputTokens,
420
+ totalTokens: usage.totalTokenCount,
421
+ searchCalls: params.searchCalls
422
+ };
423
+ }
424
+ function sumDefined(values) {
425
+ const defined = values.filter((value) => value !== void 0);
426
+ return defined.length > 0 ? defined.reduce((total, value) => total + value, 0) : void 0;
427
+ }
211
428
  function toError$2(params) {
212
429
  const parsed = responseSchema$2.safeParse(params.body);
213
430
  const message = (parsed.success ? parsed.data.error?.message : void 0) ?? `Gemini grounding request failed (HTTP ${params.status}).`;
@@ -262,6 +479,7 @@ function createGoogleCseProvider(params) {
262
479
  const desired = clampDesired(searchParams.maxResults ?? MAX_PER_REQUEST);
263
480
  const items = [];
264
481
  let lastRaw;
482
+ let requests = 0;
265
483
  for (let start = 1; start <= MAX_TOTAL_RESULTS && items.length < desired; start += MAX_PER_REQUEST) {
266
484
  const num = Math.min(MAX_PER_REQUEST, desired - items.length);
267
485
  const response = await fetchImpl(buildUrl({
@@ -270,6 +488,7 @@ function createGoogleCseProvider(params) {
270
488
  start,
271
489
  num
272
490
  }));
491
+ requests += 1;
273
492
  const body = await response.json().catch(() => void 0);
274
493
  lastRaw = body;
275
494
  const parsed = responseSchema$1.safeParse(body);
@@ -281,6 +500,7 @@ function createGoogleCseProvider(params) {
281
500
  items.push(...page);
282
501
  if (page.length < num) break;
283
502
  }
503
+ const usage = { searchCalls: requests };
284
504
  return {
285
505
  provider: "google",
286
506
  query: searchParams.query,
@@ -292,6 +512,11 @@ function createGoogleCseProvider(params) {
292
512
  })),
293
513
  citations: [],
294
514
  searchQueries: [searchParams.query],
515
+ usage,
516
+ cost: estimateCost({
517
+ provider: "google",
518
+ usage
519
+ }),
295
520
  raw: searchParams.includeRaw ? lastRaw : void 0
296
521
  };
297
522
  }
@@ -358,9 +583,17 @@ const outputItemSchema = zod.z.looseObject({
358
583
  content: zod.z.array(contentSchema).optional(),
359
584
  action: zod.z.looseObject({ query: zod.z.string().optional() }).optional()
360
585
  });
586
+ const usageSchema = zod.z.looseObject({
587
+ input_tokens: zod.z.number().optional(),
588
+ input_tokens_details: zod.z.looseObject({ cached_tokens: zod.z.number().optional() }).nullish(),
589
+ output_tokens: zod.z.number().optional(),
590
+ total_tokens: zod.z.number().optional()
591
+ });
361
592
  const responseSchema = zod.z.looseObject({
362
593
  output: zod.z.array(outputItemSchema).optional(),
363
594
  output_text: zod.z.string().optional(),
595
+ model: zod.z.string().optional(),
596
+ usage: usageSchema.nullish(),
364
597
  error: zod.z.looseObject({ message: zod.z.string().optional() }).nullish()
365
598
  });
366
599
  function createOpenAIProvider(params) {
@@ -388,7 +621,12 @@ function createOpenAIProvider(params) {
388
621
  status: response.status,
389
622
  body
390
623
  });
391
- const { answer, citations, searchQueries } = extract(parsed.data);
624
+ const { answer, citations, searchQueries, searchCalls } = extract(parsed.data);
625
+ const servedModel = parsed.data.model ?? model;
626
+ const usage = toUsage({
627
+ usage: parsed.data.usage,
628
+ searchCalls
629
+ });
392
630
  return {
393
631
  provider: "openai",
394
632
  query: searchParams.query,
@@ -396,6 +634,14 @@ function createOpenAIProvider(params) {
396
634
  answer,
397
635
  citations,
398
636
  searchQueries,
637
+ model: servedModel,
638
+ usage,
639
+ cost: estimateCost({
640
+ provider: "openai",
641
+ model: servedModel,
642
+ fallbackModel: model,
643
+ usage
644
+ }),
399
645
  raw: searchParams.includeRaw ? body : void 0
400
646
  };
401
647
  }
@@ -405,8 +651,12 @@ function extract(data) {
405
651
  const textParts = [];
406
652
  const citations = [];
407
653
  const searchQueries = [];
654
+ let searchCalls = 0;
408
655
  for (const item of data.output ?? []) {
409
- if (item.type === "web_search_call" && item.action?.query) searchQueries.push(item.action.query);
656
+ if (item.type === "web_search_call") {
657
+ searchCalls += 1;
658
+ if (item.action?.query) searchQueries.push(item.action.query);
659
+ }
410
660
  for (const content of item.content ?? []) {
411
661
  if (content.type === "output_text" && content.text) textParts.push(content.text);
412
662
  for (const annotation of content.annotations ?? []) if (annotation.type === "url_citation" && annotation.url) citations.push({
@@ -420,7 +670,18 @@ function extract(data) {
420
670
  return {
421
671
  answer: textParts.length > 0 ? textParts.join("") : data.output_text ?? "",
422
672
  citations,
423
- searchQueries
673
+ searchQueries,
674
+ searchCalls
675
+ };
676
+ }
677
+ function toUsage(params) {
678
+ const { usage } = params;
679
+ return {
680
+ inputTokens: usage?.input_tokens,
681
+ cachedInputTokens: usage?.input_tokens_details?.cached_tokens,
682
+ outputTokens: usage?.output_tokens,
683
+ totalTokens: usage?.total_tokens,
684
+ searchCalls: params.searchCalls
424
685
  };
425
686
  }
426
687
  function toError(params) {
@@ -515,7 +776,7 @@ function createWebSearchTool(params = {}) {
515
776
  name: "web_search",
516
777
  config: {
517
778
  title: "Web Search",
518
- description: "Search the web using a provider's API key (OpenAI, Google Custom Search, or Gemini). Returns a normalized JSON result with SERP results and/or a grounded answer with citations.",
779
+ description: "Search the web using a provider's API key (OpenAI, Google Custom Search, or Gemini). Returns a normalized JSON result with SERP results and/or a grounded answer with citations, plus the tokens and searches used and an estimated USD cost.",
519
780
  inputSchema: webSearchInputShape
520
781
  },
521
782
  handler: async (args) => {
@@ -530,6 +791,9 @@ function createWebSearchTool(params = {}) {
530
791
  env: params.env,
531
792
  fetchImpl: params.fetchImpl
532
793
  });
794
+ try {
795
+ await params.onResult?.(result);
796
+ } catch {}
533
797
  return { content: [{
534
798
  type: "text",
535
799
  text: JSON.stringify(result, null, 2)
@@ -595,6 +859,12 @@ Object.defineProperty(exports, "errorExitCode", {
595
859
  return errorExitCode;
596
860
  }
597
861
  });
862
+ Object.defineProperty(exports, "estimateCost", {
863
+ enumerable: true,
864
+ get: function() {
865
+ return estimateCost;
866
+ }
867
+ });
598
868
  Object.defineProperty(exports, "formatError", {
599
869
  enumerable: true,
600
870
  get: function() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webseek",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Unified multi-provider web search as a CLI, MCP server, and library (OpenAI, Google Custom Search, Gemini)",
5
5
  "keywords": [
6
6
  "web-search",
@@ -54,24 +54,24 @@
54
54
  "access": "public"
55
55
  },
56
56
  "devDependencies": {
57
- "@openrouter/sdk": "1.0.0",
58
- "@secretlint/secretlint-rule-preset-recommend": "13.0.2",
57
+ "@openrouter/sdk": "1.4.12",
58
+ "@secretlint/secretlint-rule-preset-recommend": "13.0.6",
59
59
  "@tsconfig/node24": "^24.0.4",
60
60
  "@types/node": "^26.0.0",
61
- "cspell": "10.0.1",
62
- "lint-staged": "17.1.0",
63
- "oxfmt": "0.59.0",
64
- "oxlint": "1.74.0",
65
- "repomix": "1.16.1",
66
- "resend": "6.17.2",
67
- "rulesync": "^14.1.0",
68
- "secretlint": "13.0.2",
69
- "simple-git-hooks": "2.13.1",
70
- "smol-toml": "1.7.0",
71
- "tsdown": "^0.22.3",
61
+ "cspell": "10.3.6",
62
+ "lint-staged": "17.6.0",
63
+ "oxfmt": "0.71.0",
64
+ "oxlint": "1.86.0",
65
+ "repomix": "1.18.1",
66
+ "resend": "6.31.0",
67
+ "rulesync": "^24.0.0",
68
+ "secretlint": "13.0.6",
69
+ "simple-git-hooks": "2.14.0",
70
+ "smol-toml": "1.9.0",
71
+ "tsdown": "^0.23.0",
72
72
  "tsx": "^4.22.4",
73
73
  "typescript": "^7.0.2",
74
- "vitest": "^4.1.9"
74
+ "vitest": "^5.0.0"
75
75
  },
76
76
  "dependencies": {
77
77
  "@modelcontextprotocol/sdk": "^1.29.0",