webseek 0.2.2 → 0.4.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/README.md CHANGED
@@ -19,6 +19,38 @@ The Gemini provider supports two backends that share the same request/response
19
19
  shape: the **Gemini Developer API** (`gemini-api`, default) and **Vertex AI
20
20
  express mode** (`vertex-express`).
21
21
 
22
+ ### Usage and cost
23
+
24
+ Every search reports what it consumed and an estimated cost in USD. The text
25
+ output ends with a line such as:
26
+
27
+ ```
28
+ Usage: 1,100 tokens (1,000 in, 100 out), 1 search, ~$0.0180
29
+ ```
30
+
31
+ JSON output (and the MCP tool result) carries the same data as `usage` and
32
+ `cost`, plus the `model` that served the search:
33
+
34
+ ```jsonc
35
+ {
36
+ "model": "gpt-5.5",
37
+ "usage": { "inputTokens": 1000, "outputTokens": 100, "totalTokens": 1100, "searchCalls": 1 },
38
+ "cost": { "totalUsd": 0.018, "tokensUsd": 0.008, "searchUsd": 0.01 },
39
+ }
40
+ ```
41
+
42
+ - `usage.inputTokens` includes cached tokens (`usage.cachedInputTokens`) and the
43
+ search content the model read; `usage.outputTokens` includes reasoning/thinking
44
+ tokens. SERP providers (`google`) report no tokens.
45
+ - `usage.searchCalls` counts OpenAI `web_search` calls, Gemini grounding queries,
46
+ or Google Custom Search requests (one per page of 10 results).
47
+ - `cost` is computed from built-in list prices: token rates for the model plus
48
+ the provider's search fee (OpenAI $10 per 1k calls, Gemini 3.x $14 per 1k
49
+ queries, Gemini 2.5 $35 per 1k grounded prompts, Google Custom Search $5 per 1k
50
+ queries). It is a list-price estimate, not your bill: free allowances,
51
+ discounts and long-context rates are **not** applied. `cost` is omitted when the model has no known price
52
+ or the provider reported no token counts.
53
+
22
54
  ## Install
23
55
 
24
56
  ```bash
@@ -105,6 +137,24 @@ CommonJS consumers can `require` it the same way:
105
137
  const { runSearch } = require("webseek");
106
138
  ```
107
139
 
140
+ Each result carries `usage` and, when it can be priced, an estimated `cost` (see
141
+ [Usage and cost](#usage-and-cost)). To price usage yourself — for example to sum
142
+ several searches — call `estimateCost` with the provider, the model and a
143
+ `usage` object; it returns `undefined` when the model has no known price:
144
+
145
+ ```ts
146
+ import { estimateCost } from "webseek";
147
+
148
+ const cost = estimateCost({
149
+ provider: "openai",
150
+ model: "gpt-5.5",
151
+ usage: { inputTokens: 1000, outputTokens: 100, searchCalls: 1 },
152
+ });
153
+ console.log(cost?.totalUsd.toFixed(3)); // "0.018"
154
+ ```
155
+
156
+ Prices are list prices built into the package and change with its releases.
157
+
108
158
  To embed the `web_search` tool into your own MCP server, use the
109
159
  `createWebSearchTool` factory exported from the same entry point.
110
160
 
@@ -139,6 +189,7 @@ src/
139
189
  mcp/ MCP server + the web_search tool
140
190
  lib/ runSearch — the shared core called by both CLI and MCP
141
191
  providers/ per-provider implementations (openai, google-cse, gemini)
192
+ pricing/ list-price table and cost estimation
142
193
  config/ credential + base-URL resolution from env
143
194
  output/ text / JSON formatting
144
195
  utils/ logger, error formatter
@@ -159,11 +210,16 @@ Publishing to npm is automated. To cut a new version:
159
210
  1. Run the `draft-release` skill — it bumps the version on a `release/vX.Y.Z`
160
211
  branch, opens a PR, and creates a **draft** GitHub release.
161
212
  2. Review and merge the release PR into `main`.
162
- 3. Open the draft release on GitHub and click **Publish release**.
163
213
 
164
- Publishing the release triggers
214
+ Merging the release PR triggers
165
215
  [`.github/workflows/publish.yml`](.github/workflows/publish.yml), which verifies
166
- the tag matches `package.json` and runs `pnpm publish`. Authentication uses npm
167
- **trusted publishing** (OIDC) — no token secret is required, and provenance is
168
- generated automatically. Configure the trusted publisher for the package once on
169
- npmjs.com, pointing it at this repository's `publish.yml` workflow.
216
+ the tag matches `package.json`, pushes the `vX.Y.Z` tag, runs `pnpm publish`, and
217
+ flips the draft GitHub release public — no manual **Publish release** click is
218
+ needed. Authentication uses npm **trusted publishing** (OIDC) — no token secret
219
+ is required, and provenance is generated automatically. Configure the trusted
220
+ publisher for the package once on npmjs.com, pointing it at this repository's
221
+ `publish.yml` workflow.
222
+
223
+ To run the whole flow in one shot, use the `goal-release` skill — it runs the
224
+ `draft-release` skill, waits for the release PR's CI to pass, merges it with the
225
+ `merge-pr` skill, and waits for the `Publish` workflow to finish.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const require_tools = require("../tools-Ci1EDrCT.cjs");
2
+ const require_tools = require("../tools-40nYFbvw.cjs");
3
3
  let node_fs = require("node:fs");
4
4
  let node_path = require("node:path");
5
5
  let commander = require("commander");
@@ -102,7 +102,10 @@ function formatJson(result) {
102
102
  results: result.results,
103
103
  answer: result.answer,
104
104
  citations: result.citations,
105
- searchQueries: result.searchQueries
105
+ searchQueries: result.searchQueries,
106
+ model: result.model,
107
+ usage: result.usage,
108
+ cost: result.cost
106
109
  };
107
110
  if (result.raw !== void 0) payload.raw = result.raw;
108
111
  return JSON.stringify(payload, null, 2);
@@ -128,8 +131,29 @@ function formatText(result) {
128
131
  lines.push("");
129
132
  }
130
133
  if (result.searchQueries.length > 0) lines.push(`Searches: ${result.searchQueries.join(" | ")}`);
134
+ if (result.usage) lines.push(formatUsage({
135
+ usage: result.usage,
136
+ cost: result.cost
137
+ }));
131
138
  return lines.join("\n").trimEnd();
132
139
  }
140
+ function formatUsage(params) {
141
+ const { usage, cost } = params;
142
+ const parts = [];
143
+ if (usage.inputTokens !== void 0 || usage.outputTokens !== void 0) {
144
+ const input = usage.inputTokens ?? 0;
145
+ const output = usage.outputTokens ?? 0;
146
+ const total = usage.totalTokens ?? input + output;
147
+ parts.push(`${total.toLocaleString("en-US")} tokens (${input.toLocaleString("en-US")} in, ${output.toLocaleString("en-US")} out)`);
148
+ }
149
+ parts.push(`${usage.searchCalls} search${usage.searchCalls === 1 ? "" : "es"}`);
150
+ parts.push(cost === void 0 ? "cost unknown" : `~${formatUsd(cost.totalUsd)}`);
151
+ return `Usage: ${parts.join(", ")}`;
152
+ }
153
+ function formatUsd(value) {
154
+ if (value > 0 && value < 5e-5) return "<$0.0001";
155
+ return `$${value.toFixed(4)}`;
156
+ }
133
157
  //#endregion
134
158
  //#region src/cli/commands/search.ts
135
159
  /**
@@ -152,10 +176,11 @@ function toSearchRequest(params) {
152
176
  };
153
177
  }
154
178
  async function runSearchCommand(params) {
155
- const result = await require_tools.runSearch(toSearchRequest({
179
+ const request = toSearchRequest({
156
180
  queryParts: params.queryParts,
157
181
  options: params.options
158
- }));
182
+ });
183
+ const result = await require_tools.runSearch(request);
159
184
  params.logger.result(formatResult({
160
185
  result,
161
186
  json: Boolean(params.options.json)
@@ -1 +1 @@
1
- export { };
1
+ export {}
@@ -1 +1 @@
1
- export { };
1
+ export {}
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as coerceGeminiBackend, c as runSearch, d as formatError, l as WebseekError, o as coerceMaxResults, s as coerceProvider, t as createWebSearchTool, u as errorExitCode } from "../tools-DBF9nCIs.mjs";
2
+ import { a as coerceGeminiBackend, c as runSearch, d as errorExitCode, f as formatError, o as coerceMaxResults, s as coerceProvider, t as createWebSearchTool, u as WebseekError } from "../tools-B4VbXLvm.mjs";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
5
  import { Command } from "commander";
@@ -102,7 +102,10 @@ function formatJson(result) {
102
102
  results: result.results,
103
103
  answer: result.answer,
104
104
  citations: result.citations,
105
- searchQueries: result.searchQueries
105
+ searchQueries: result.searchQueries,
106
+ model: result.model,
107
+ usage: result.usage,
108
+ cost: result.cost
106
109
  };
107
110
  if (result.raw !== void 0) payload.raw = result.raw;
108
111
  return JSON.stringify(payload, null, 2);
@@ -128,8 +131,29 @@ function formatText(result) {
128
131
  lines.push("");
129
132
  }
130
133
  if (result.searchQueries.length > 0) lines.push(`Searches: ${result.searchQueries.join(" | ")}`);
134
+ if (result.usage) lines.push(formatUsage({
135
+ usage: result.usage,
136
+ cost: result.cost
137
+ }));
131
138
  return lines.join("\n").trimEnd();
132
139
  }
140
+ function formatUsage(params) {
141
+ const { usage, cost } = params;
142
+ const parts = [];
143
+ if (usage.inputTokens !== void 0 || usage.outputTokens !== void 0) {
144
+ const input = usage.inputTokens ?? 0;
145
+ const output = usage.outputTokens ?? 0;
146
+ const total = usage.totalTokens ?? input + output;
147
+ parts.push(`${total.toLocaleString("en-US")} tokens (${input.toLocaleString("en-US")} in, ${output.toLocaleString("en-US")} out)`);
148
+ }
149
+ parts.push(`${usage.searchCalls} search${usage.searchCalls === 1 ? "" : "es"}`);
150
+ parts.push(cost === void 0 ? "cost unknown" : `~${formatUsd(cost.totalUsd)}`);
151
+ return `Usage: ${parts.join(", ")}`;
152
+ }
153
+ function formatUsd(value) {
154
+ if (value > 0 && value < 5e-5) return "<$0.0001";
155
+ return `$${value.toFixed(4)}`;
156
+ }
133
157
  //#endregion
134
158
  //#region src/cli/commands/search.ts
135
159
  /**
@@ -152,10 +176,11 @@ function toSearchRequest(params) {
152
176
  };
153
177
  }
154
178
  async function runSearchCommand(params) {
155
- const result = await runSearch(toSearchRequest({
179
+ const request = toSearchRequest({
156
180
  queryParts: params.queryParts,
157
181
  options: params.options
158
- }));
182
+ });
183
+ const result = await runSearch(request);
159
184
  params.logger.result(formatResult({
160
185
  result,
161
186
  json: Boolean(params.options.json)
package/dist/index.cjs CHANGED
@@ -1,10 +1,11 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_tools = require("./tools-Ci1EDrCT.cjs");
2
+ const require_tools = require("./tools-40nYFbvw.cjs");
3
3
  exports.GEMINI_BACKENDS = require_tools.GEMINI_BACKENDS;
4
4
  exports.PROVIDER_NAMES = require_tools.PROVIDER_NAMES;
5
5
  exports.WebseekError = require_tools.WebseekError;
6
6
  exports.createWebSearchTool = require_tools.createWebSearchTool;
7
7
  exports.errorExitCode = require_tools.errorExitCode;
8
+ exports.estimateCost = require_tools.estimateCost;
8
9
  exports.formatError = require_tools.formatError;
9
10
  exports.runSearch = require_tools.runSearch;
10
11
  exports.webSearchInputShape = require_tools.webSearchInputShape;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { z } from "zod";
2
-
3
2
  //#region src/config/env.d.ts
4
3
  /**
5
4
  * Resolves provider credentials and configuration from environment variables.
@@ -42,6 +41,24 @@ interface Citation {
42
41
  startIndex?: number;
43
42
  endIndex?: number;
44
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
+ }
45
62
  /** The normalized result shape returned by every provider. */
46
63
  interface NormalizedSearchResult {
47
64
  provider: ProviderName;
@@ -54,6 +71,12 @@ interface NormalizedSearchResult {
54
71
  citations: Citation[];
55
72
  /** Queries the provider actually ran (grounded providers). */
56
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;
57
80
  /** The provider's raw response, included only when requested. */
58
81
  raw?: unknown;
59
82
  }
@@ -76,8 +99,8 @@ interface SearchProvider {
76
99
  }
77
100
  //#endregion
78
101
  //#region src/lib/search.d.ts
79
- declare const PROVIDER_NAMES: readonly ["openai", "google", "gemini"];
80
- 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"];
81
104
  interface RunSearchParams {
82
105
  provider: ProviderName;
83
106
  query: string;
@@ -88,7 +111,28 @@ interface RunSearchParams {
88
111
  env?: Env;
89
112
  fetchImpl?: typeof fetch;
90
113
  }
91
- 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;
92
136
  //#endregion
93
137
  //#region src/utils/error.d.ts
94
138
  /**
@@ -105,7 +149,7 @@ interface WebseekErrorOptions {
105
149
  message: string;
106
150
  cause?: unknown;
107
151
  }
108
- declare class WebseekError extends Error {
152
+ export declare class WebseekError extends Error {
109
153
  readonly code: WebseekErrorCode;
110
154
  constructor(options: WebseekErrorOptions);
111
155
  }
@@ -113,17 +157,17 @@ declare class WebseekError extends Error {
113
157
  * Map a thrown value to a process exit code: `2` for usage mistakes, `1` for
114
158
  * any other failure.
115
159
  */
116
- declare function errorExitCode(error: unknown): number;
160
+ export declare function errorExitCode(error: unknown): number;
117
161
  /** Render any thrown value into a single-line, user-facing message. */
118
- declare function formatError(error: unknown): string;
162
+ export declare function formatError(error: unknown): string;
119
163
  //#endregion
120
164
  //#region src/mcp/tools.d.ts
121
- declare const webSearchInputShape: {
165
+ export declare const webSearchInputShape: {
122
166
  query: z.ZodString;
123
167
  provider: z.ZodEnum<{
124
- openai: "openai";
125
- google: "google";
126
168
  gemini: "gemini";
169
+ google: "google";
170
+ openai: "openai";
127
171
  }>;
128
172
  maxResults: z.ZodOptional<z.ZodNumber>;
129
173
  model: z.ZodOptional<z.ZodString>;
@@ -136,9 +180,9 @@ declare const webSearchInputShape: {
136
180
  declare const webSearchArgsSchema: z.ZodObject<{
137
181
  query: z.ZodString;
138
182
  provider: z.ZodEnum<{
139
- openai: "openai";
140
- google: "google";
141
183
  gemini: "gemini";
184
+ google: "google";
185
+ openai: "openai";
142
186
  }>;
143
187
  maxResults: z.ZodOptional<z.ZodNumber>;
144
188
  model: z.ZodOptional<z.ZodString>;
@@ -172,6 +216,6 @@ interface WebSearchTool {
172
216
  };
173
217
  handler: (args: WebSearchArgs) => Promise<ToolResult>;
174
218
  }
175
- declare function createWebSearchTool(params?: CreateWebSearchToolParams): WebSearchTool;
219
+ export declare function createWebSearchTool(params?: CreateWebSearchToolParams): WebSearchTool;
176
220
  //#endregion
177
- 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 };
221
+ export type { Citation, CreateWebSearchToolParams, Env, EstimateCostParams, GeminiBackend, NormalizedSearchResult, ProviderName, RunSearchParams, SearchCost, SearchParams, SearchProvider, SearchResultItem, SearchUsage, ToolResult, WebSearchArgs, WebSearchTool, WebseekErrorCode, WebseekErrorOptions };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { z } from "zod";
2
-
3
2
  //#region src/config/env.d.ts
4
3
  /**
5
4
  * Resolves provider credentials and configuration from environment variables.
@@ -42,6 +41,24 @@ interface Citation {
42
41
  startIndex?: number;
43
42
  endIndex?: number;
44
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
+ }
45
62
  /** The normalized result shape returned by every provider. */
46
63
  interface NormalizedSearchResult {
47
64
  provider: ProviderName;
@@ -54,6 +71,12 @@ interface NormalizedSearchResult {
54
71
  citations: Citation[];
55
72
  /** Queries the provider actually ran (grounded providers). */
56
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;
57
80
  /** The provider's raw response, included only when requested. */
58
81
  raw?: unknown;
59
82
  }
@@ -76,8 +99,8 @@ interface SearchProvider {
76
99
  }
77
100
  //#endregion
78
101
  //#region src/lib/search.d.ts
79
- declare const PROVIDER_NAMES: readonly ["openai", "google", "gemini"];
80
- 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"];
81
104
  interface RunSearchParams {
82
105
  provider: ProviderName;
83
106
  query: string;
@@ -88,7 +111,28 @@ interface RunSearchParams {
88
111
  env?: Env;
89
112
  fetchImpl?: typeof fetch;
90
113
  }
91
- 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;
92
136
  //#endregion
93
137
  //#region src/utils/error.d.ts
94
138
  /**
@@ -105,7 +149,7 @@ interface WebseekErrorOptions {
105
149
  message: string;
106
150
  cause?: unknown;
107
151
  }
108
- declare class WebseekError extends Error {
152
+ export declare class WebseekError extends Error {
109
153
  readonly code: WebseekErrorCode;
110
154
  constructor(options: WebseekErrorOptions);
111
155
  }
@@ -113,17 +157,17 @@ declare class WebseekError extends Error {
113
157
  * Map a thrown value to a process exit code: `2` for usage mistakes, `1` for
114
158
  * any other failure.
115
159
  */
116
- declare function errorExitCode(error: unknown): number;
160
+ export declare function errorExitCode(error: unknown): number;
117
161
  /** Render any thrown value into a single-line, user-facing message. */
118
- declare function formatError(error: unknown): string;
162
+ export declare function formatError(error: unknown): string;
119
163
  //#endregion
120
164
  //#region src/mcp/tools.d.ts
121
- declare const webSearchInputShape: {
165
+ export declare const webSearchInputShape: {
122
166
  query: z.ZodString;
123
167
  provider: z.ZodEnum<{
124
- openai: "openai";
125
- google: "google";
126
168
  gemini: "gemini";
169
+ google: "google";
170
+ openai: "openai";
127
171
  }>;
128
172
  maxResults: z.ZodOptional<z.ZodNumber>;
129
173
  model: z.ZodOptional<z.ZodString>;
@@ -136,9 +180,9 @@ declare const webSearchInputShape: {
136
180
  declare const webSearchArgsSchema: z.ZodObject<{
137
181
  query: z.ZodString;
138
182
  provider: z.ZodEnum<{
139
- openai: "openai";
140
- google: "google";
141
183
  gemini: "gemini";
184
+ google: "google";
185
+ openai: "openai";
142
186
  }>;
143
187
  maxResults: z.ZodOptional<z.ZodNumber>;
144
188
  model: z.ZodOptional<z.ZodString>;
@@ -172,6 +216,6 @@ interface WebSearchTool {
172
216
  };
173
217
  handler: (args: WebSearchArgs) => Promise<ToolResult>;
174
218
  }
175
- declare function createWebSearchTool(params?: CreateWebSearchToolParams): WebSearchTool;
219
+ export declare function createWebSearchTool(params?: CreateWebSearchToolParams): WebSearchTool;
176
220
  //#endregion
177
- 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 };
221
+ 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-B4VbXLvm.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 = 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) => {
@@ -595,6 +856,12 @@ Object.defineProperty(exports, "errorExitCode", {
595
856
  return errorExitCode;
596
857
  }
597
858
  });
859
+ Object.defineProperty(exports, "estimateCost", {
860
+ enumerable: true,
861
+ get: function() {
862
+ return estimateCost;
863
+ }
864
+ });
598
865
  Object.defineProperty(exports, "formatError", {
599
866
  enumerable: true,
600
867
  get: function() {
@@ -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) => {
@@ -547,4 +808,4 @@ function createWebSearchTool(params = {}) {
547
808
  };
548
809
  }
549
810
  //#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 };
811
+ 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webseek",
3
- "version": "0.2.2",
3
+ "version": "0.4.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,23 +54,24 @@
54
54
  "access": "public"
55
55
  },
56
56
  "devDependencies": {
57
- "@openrouter/sdk": "0.13.7",
58
- "@secretlint/secretlint-rule-preset-recommend": "13.0.2",
57
+ "@openrouter/sdk": "1.3.21",
58
+ "@secretlint/secretlint-rule-preset-recommend": "13.0.5",
59
59
  "@tsconfig/node24": "^24.0.4",
60
60
  "@types/node": "^26.0.0",
61
- "cspell": "10.0.1",
62
- "lint-staged": "17.0.7",
63
- "oxfmt": "0.55.0",
64
- "oxlint": "1.70.0",
65
- "repomix": "1.14.1",
66
- "resend": "6.14.0",
67
- "rulesync": "^8.31.0",
68
- "secretlint": "13.0.2",
69
- "simple-git-hooks": "2.13.1",
70
- "tsdown": "^0.22.3",
61
+ "cspell": "10.3.3",
62
+ "lint-staged": "17.5.1",
63
+ "oxfmt": "0.70.0",
64
+ "oxlint": "1.85.0",
65
+ "repomix": "1.18.1",
66
+ "resend": "6.28.1",
67
+ "rulesync": "^17.0.0",
68
+ "secretlint": "13.0.5",
69
+ "simple-git-hooks": "2.14.0",
70
+ "smol-toml": "1.9.0",
71
+ "tsdown": "^0.23.0",
71
72
  "tsx": "^4.22.4",
72
- "typescript": "^6.0.3",
73
- "vitest": "^4.1.9"
73
+ "typescript": "^7.0.2",
74
+ "vitest": "^5.0.0"
74
75
  },
75
76
  "dependencies": {
76
77
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -94,8 +95,9 @@
94
95
  "test:e2e": "vitest run --config vitest.e2e.config.ts --silent=false",
95
96
  "sync:skill-docs": "tsx scripts/sync-skill-docs.ts",
96
97
  "check:sync-skill-docs": "tsx scripts/check-skill-docs-sync.ts",
98
+ "check:codex-config": "tsx scripts/check-codex-config.ts",
97
99
  "cicheck:code": "pnpm run check && pnpm run test",
98
- "cicheck:content": "pnpm run check:sync-skill-docs && pnpm run cspell && pnpm run secretlint",
100
+ "cicheck:content": "pnpm run check:sync-skill-docs && pnpm run check:codex-config && pnpm run cspell && pnpm run secretlint",
99
101
  "cicheck": "pnpm run cicheck:code && pnpm run cicheck:content",
100
102
  "generate": "rulesync generate",
101
103
  "gitignore": "rulesync gitignore"