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.
package/dist/index.d.mts CHANGED
@@ -41,6 +41,24 @@ interface Citation {
41
41
  startIndex?: number;
42
42
  endIndex?: number;
43
43
  }
44
+ /** What a search consumed. Token counts are absent for SERP providers. */
45
+ interface SearchUsage {
46
+ /** Prompt tokens, including cached ones and fetched search content. */
47
+ inputTokens?: number;
48
+ /** The part of `inputTokens` served from the prompt cache. */
49
+ cachedInputTokens?: number;
50
+ /** Generated tokens, including reasoning/thinking tokens. */
51
+ outputTokens?: number;
52
+ totalTokens?: number;
53
+ /** Searches the provider ran (web search tool calls, grounding queries, or SERP requests). */
54
+ searchCalls: number;
55
+ }
56
+ /** Estimated list-price cost of a search in USD (free tiers not applied). */
57
+ interface SearchCost {
58
+ totalUsd: number;
59
+ tokensUsd: number;
60
+ searchUsd: number;
61
+ }
44
62
  /** The normalized result shape returned by every provider. */
45
63
  interface NormalizedSearchResult {
46
64
  provider: ProviderName;
@@ -53,6 +71,12 @@ interface NormalizedSearchResult {
53
71
  citations: Citation[];
54
72
  /** Queries the provider actually ran (grounded providers). */
55
73
  searchQueries: string[];
74
+ /** The model that served the search (grounded providers). */
75
+ model?: string;
76
+ /** Tokens and searches consumed (always set by the built-in providers). */
77
+ usage?: SearchUsage;
78
+ /** Estimated cost; absent when the model's price is unknown. */
79
+ cost?: SearchCost;
56
80
  /** The provider's raw response, included only when requested. */
57
81
  raw?: unknown;
58
82
  }
@@ -75,8 +99,8 @@ interface SearchProvider {
75
99
  }
76
100
  //#endregion
77
101
  //#region src/lib/search.d.ts
78
- declare const PROVIDER_NAMES: readonly ["openai", "google", "gemini"];
79
- declare const GEMINI_BACKENDS: readonly ["gemini-api", "vertex-express"];
102
+ export declare const PROVIDER_NAMES: readonly ["openai", "google", "gemini"];
103
+ export declare const GEMINI_BACKENDS: readonly ["gemini-api", "vertex-express"];
80
104
  interface RunSearchParams {
81
105
  provider: ProviderName;
82
106
  query: string;
@@ -87,7 +111,28 @@ interface RunSearchParams {
87
111
  env?: Env;
88
112
  fetchImpl?: typeof fetch;
89
113
  }
90
- declare function runSearch(params: RunSearchParams): Promise<NormalizedSearchResult>;
114
+ export declare function runSearch(params: RunSearchParams): Promise<NormalizedSearchResult>;
115
+ //#endregion
116
+ //#region src/pricing/pricing.d.ts
117
+ interface EstimateCostParams {
118
+ provider: ProviderName;
119
+ /** The model that served the search (ignored for SERP providers). */
120
+ model?: string;
121
+ /**
122
+ * Priced instead when `model` is not in the table — typically the requested
123
+ * model, since the served id may be a snapshot or variant the table lacks.
124
+ */
125
+ fallbackModel?: string;
126
+ usage: SearchUsage;
127
+ /** Price as of this moment (rates can change on a set date); defaults to now. */
128
+ now?: Date;
129
+ }
130
+ /**
131
+ * Estimate what a search cost at list price, or `undefined` when it cannot be
132
+ * priced: the model is not in this table (e.g. newer than it), or an LLM-backed
133
+ * provider reported no token counts (a search-fee-only figure would understate).
134
+ */
135
+ export declare function estimateCost(params: EstimateCostParams): SearchCost | undefined;
91
136
  //#endregion
92
137
  //#region src/utils/error.d.ts
93
138
  /**
@@ -104,7 +149,7 @@ interface WebseekErrorOptions {
104
149
  message: string;
105
150
  cause?: unknown;
106
151
  }
107
- declare class WebseekError extends Error {
152
+ export declare class WebseekError extends Error {
108
153
  readonly code: WebseekErrorCode;
109
154
  constructor(options: WebseekErrorOptions);
110
155
  }
@@ -112,12 +157,12 @@ declare class WebseekError extends Error {
112
157
  * Map a thrown value to a process exit code: `2` for usage mistakes, `1` for
113
158
  * any other failure.
114
159
  */
115
- declare function errorExitCode(error: unknown): number;
160
+ export declare function errorExitCode(error: unknown): number;
116
161
  /** Render any thrown value into a single-line, user-facing message. */
117
- declare function formatError(error: unknown): string;
162
+ export declare function formatError(error: unknown): string;
118
163
  //#endregion
119
164
  //#region src/mcp/tools.d.ts
120
- declare const webSearchInputShape: {
165
+ export declare const webSearchInputShape: {
121
166
  query: z.ZodString;
122
167
  provider: z.ZodEnum<{
123
168
  gemini: "gemini";
@@ -161,6 +206,8 @@ interface CreateWebSearchToolParams {
161
206
  env?: Env;
162
207
  /** Injectable for tests; defaults to the global fetch. */
163
208
  fetchImpl?: typeof fetch;
209
+ /** Called with every successful result before it is returned (e.g. to log usage). */
210
+ onResult?: (result: NormalizedSearchResult) => Promise<void> | void;
164
211
  }
165
212
  interface WebSearchTool {
166
213
  name: string;
@@ -171,6 +218,6 @@ interface WebSearchTool {
171
218
  };
172
219
  handler: (args: WebSearchArgs) => Promise<ToolResult>;
173
220
  }
174
- declare function createWebSearchTool(params?: CreateWebSearchToolParams): WebSearchTool;
221
+ export declare function createWebSearchTool(params?: CreateWebSearchToolParams): WebSearchTool;
175
222
  //#endregion
176
- export { type Citation, type CreateWebSearchToolParams, type Env, GEMINI_BACKENDS, type GeminiBackend, type NormalizedSearchResult, PROVIDER_NAMES, type ProviderName, type RunSearchParams, type SearchParams, type SearchProvider, type SearchResultItem, type ToolResult, type WebSearchArgs, type WebSearchTool, WebseekError, type WebseekErrorCode, type WebseekErrorOptions, createWebSearchTool, errorExitCode, formatError, runSearch, webSearchInputShape };
223
+ export type { Citation, CreateWebSearchToolParams, Env, EstimateCostParams, GeminiBackend, NormalizedSearchResult, ProviderName, RunSearchParams, SearchCost, SearchParams, SearchProvider, SearchResultItem, SearchUsage, ToolResult, WebSearchArgs, WebSearchTool, WebseekErrorCode, WebseekErrorOptions };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { c as runSearch, d as formatError, i as PROVIDER_NAMES, l as WebseekError, n as webSearchInputShape, r as GEMINI_BACKENDS, t as createWebSearchTool, u as errorExitCode } from "./tools-DBF9nCIs.mjs";
2
- export { GEMINI_BACKENDS, PROVIDER_NAMES, WebseekError, createWebSearchTool, errorExitCode, formatError, runSearch, webSearchInputShape };
1
+ import { c as runSearch, d as errorExitCode, f as formatError, i as PROVIDER_NAMES, l as estimateCost, n as webSearchInputShape, r as GEMINI_BACKENDS, t as createWebSearchTool, u as WebseekError } from "./tools-1ZllUYBx.mjs";
2
+ export { GEMINI_BACKENDS, PROVIDER_NAMES, WebseekError, createWebSearchTool, errorExitCode, estimateCost, formatError, runSearch, webSearchInputShape };
@@ -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 = z.looseObject({
151
328
  content: z.looseObject({ parts: z.array(z.looseObject({ text: z.string().optional() })).optional() }).optional(),
152
329
  groundingMetadata: groundingMetadataSchema.optional()
153
330
  });
331
+ const usageMetadataSchema = z.looseObject({
332
+ promptTokenCount: z.number().optional(),
333
+ cachedContentTokenCount: z.number().optional(),
334
+ candidatesTokenCount: z.number().optional(),
335
+ thoughtsTokenCount: z.number().optional(),
336
+ toolUsePromptTokenCount: z.number().optional(),
337
+ totalTokenCount: z.number().optional()
338
+ });
154
339
  const responseSchema$2 = z.looseObject({
155
340
  candidates: z.array(candidateSchema).optional(),
341
+ usageMetadata: usageMetadataSchema.optional(),
342
+ modelVersion: z.string().optional(),
156
343
  error: z.looseObject({ message: 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 = z.looseObject({
358
583
  content: z.array(contentSchema).optional(),
359
584
  action: z.looseObject({ query: z.string().optional() }).optional()
360
585
  });
586
+ const usageSchema = z.looseObject({
587
+ input_tokens: z.number().optional(),
588
+ input_tokens_details: z.looseObject({ cached_tokens: z.number().optional() }).nullish(),
589
+ output_tokens: z.number().optional(),
590
+ total_tokens: z.number().optional()
591
+ });
361
592
  const responseSchema = z.looseObject({
362
593
  output: z.array(outputItemSchema).optional(),
363
594
  output_text: z.string().optional(),
595
+ model: z.string().optional(),
596
+ usage: usageSchema.nullish(),
364
597
  error: z.looseObject({ message: 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)
@@ -547,4 +811,4 @@ function createWebSearchTool(params = {}) {
547
811
  };
548
812
  }
549
813
  //#endregion
550
- export { coerceGeminiBackend as a, runSearch as c, formatError as d, PROVIDER_NAMES as i, WebseekError as l, webSearchInputShape as n, coerceMaxResults as o, GEMINI_BACKENDS as r, coerceProvider as s, createWebSearchTool as t, errorExitCode as u };
814
+ export { coerceGeminiBackend as a, runSearch as c, errorExitCode as d, formatError as f, PROVIDER_NAMES as i, estimateCost as l, webSearchInputShape as n, coerceMaxResults as o, GEMINI_BACKENDS as r, coerceProvider as s, createWebSearchTool as t, WebseekError as u };