@warlock.js/ai-google 5.30.0 → 5.32.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/cjs/index.cjs +273 -21
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/config.type.d.mts +4 -6
  5. package/esm/config.type.d.mts.map +1 -1
  6. package/esm/embedder.mjs.map +1 -1
  7. package/esm/gemini-image.d.mts +1 -3
  8. package/esm/gemini-image.d.mts.map +1 -1
  9. package/esm/gemini-image.mjs +1 -1
  10. package/esm/gemini-image.mjs.map +1 -1
  11. package/esm/image.d.mts +1 -3
  12. package/esm/image.d.mts.map +1 -1
  13. package/esm/image.mjs.map +1 -1
  14. package/esm/index.d.mts +3 -1
  15. package/esm/index.mjs +2 -1
  16. package/esm/known-pricing.d.mts +37 -0
  17. package/esm/known-pricing.d.mts.map +1 -0
  18. package/esm/known-pricing.mjs +42 -0
  19. package/esm/known-pricing.mjs.map +1 -0
  20. package/esm/model.mjs +110 -9
  21. package/esm/model.mjs.map +1 -1
  22. package/esm/provider-options.d.mts +8 -0
  23. package/esm/provider-options.d.mts.map +1 -0
  24. package/esm/provider-options.mjs +36 -0
  25. package/esm/provider-options.mjs.map +1 -0
  26. package/esm/sdk.d.mts +4 -5
  27. package/esm/sdk.d.mts.map +1 -1
  28. package/esm/sdk.mjs +5 -3
  29. package/esm/sdk.mjs.map +1 -1
  30. package/esm/utils/apply-google-usage.mjs +5 -0
  31. package/esm/utils/apply-google-usage.mjs.map +1 -1
  32. package/esm/utils/grounding.mjs +30 -0
  33. package/esm/utils/grounding.mjs.map +1 -0
  34. package/esm/utils/index.mjs +1 -1
  35. package/esm/utils/to-google-contents.mjs +33 -1
  36. package/esm/utils/to-google-contents.mjs.map +1 -1
  37. package/esm/utils/wrap-google-error.mjs +21 -8
  38. package/esm/utils/wrap-google-error.mjs.map +1 -1
  39. package/llms-full.txt +162 -31
  40. package/llms.txt +1 -1
  41. package/package.json +3 -3
  42. package/skills/setup-google/SKILL.md +162 -31
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: setup-google
3
- description: 'Wire @warlock.js/ai-google — new GoogleSDK({apiKey} | {vertexai, project, location}) for Gemini API + Vertex AI. generateContent / embedContent + thoughtSignature round-trip for thinking models, batched embeddings. .model({name, vision?, reasoning?, audio?, pdf?}) with cost-truth capabilities (PDF + audio input map to Gemini inlineData), .image({name, pricing?}) for image generation via ai.image — the id picks the transport: gemini-* → generateContent + responseModalities IMAGE (usage passed through; generateImages is deprecated by Google), everything else → generateImages (Imagen, per-image, zero usage) — extended thinking via options.reasoning → thinkingConfig.thinkingBudget, usage reasoningTokens (thoughtsTokenCount) / cachedTokens (cachedContentTokenCount). Triggers: `GoogleSDK`, `google.model`, `google.embedder`, `google.image`, `imagen`, `generateImages`, `gemini image`, `nano banana`, `responseModalities`, `imageConfig`, `ai.image`, `inlineData`, `pdf input`, `audio input`, `thoughtSignature`, `responseJsonSchema`, `vertexai`, `reasoning`, `thinkingConfig`, `thinkingBudget`, `thoughtsTokenCount`, `reasoningTokens`, `cachedTokens`, `promptCaching`, `cacheControl`; "use gemini", "wire Vertex AI", "gemini embeddings", "gemini thinking tool calls", "gemini 2.5 thinking budget", "gemini cached content cost", "generate images with imagen", "generate an image with gemini", "gemini image model 404 not supported for predict", "send a pdf / audio to gemini"; import `import { GoogleSDK } from "@warlock.js/ai-google"`. Skip: the ai.image verb surface — the `generate-images` topic of the `warlock-js-ai` skill; agent loop the `run-ai-agent` topic of the `warlock-js-ai` skill; provider picking the `pick-ai-provider` topic of the `warlock-js-ai` skill; embedder usage the `embed-text` topic of the `warlock-js-ai` skill; siblings `@warlock.js/ai-openai`, `@warlock.js/ai-anthropic`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-ollama`; raw `@google/genai`, `@google-cloud/vertexai`, Vercel `@ai-sdk/google`.'
3
+ description: 'Wire @warlock.js/ai-google — new GoogleSDK({apiKey} | {vertexai, project, location}) for Gemini API + Vertex AI. generateContent / embedContent + thoughtSignature round-trip for thinking models, batched embeddings. .model({name, vision?, reasoning?, audio?, pdf?}) with cost-truth capabilities (PDF + audio input map to Gemini inlineData), .image({name, pricing?}) for image generation via ai.image — the id picks the transport: gemini-* → generateContent + responseModalities IMAGE (usage passed through; generateImages is deprecated by Google), everything else → generateImages (Imagen, per-image, zero usage) — extended thinking via options.reasoning → thinkingConfig.thinkingBudget, usage reasoningTokens (thoughtsTokenCount) / cachedTokens (cachedContentTokenCount). Triggers: `providerOptions`, `GoogleCallOptions`, `countTokens`, `GoogleSDK`, `google.model`, `google.embedder`, `google.image`, `imagen`, `generateImages`, `gemini image`, `nano banana`, `responseModalities`, `imageConfig`, `ai.image`, `inlineData`, `pdf input`, `audio input`, `thoughtSignature`, `responseJsonSchema`, `ai.providerTool`, `googleSearch`, `includeThoughts`, `streamChunks`, `vertexai`, `reasoning`, `thinkingConfig`, `thinkingBudget`, `thoughtsTokenCount`, `reasoningTokens`, `cachedTokens`, `inputIncludesCache`, `GOOGLE_MODEL_PRICING`, `googlePricingFor`, `ProviderOverloadedError`, `promptCaching`, `cacheControl`; "use gemini", "wire Vertex AI", "gemini embeddings", "gemini thinking tool calls", "gemini 2.5 thinking budget", "gemini cached content cost", "generate images with imagen", "generate an image with gemini", "gemini image model 404 not supported for predict", "send a pdf / audio to gemini"; import `import { GoogleSDK } from "@warlock.js/ai-google"`. Skip: the ai.image verb surface — the `generate-images` topic of the `warlock-js-ai` skill; agent loop the `run-ai-agent` topic of the `warlock-js-ai` skill; provider picking the `pick-ai-provider` topic of the `warlock-js-ai` skill; embedder usage the `embed-text` topic of the `warlock-js-ai` skill; siblings `@warlock.js/ai-openai`, `@warlock.js/ai-anthropic`, `@warlock.js/ai-bedrock`, `@warlock.js/ai-ollama`; raw `@google/genai`, `@google-cloud/vertexai`, Vercel `@ai-sdk/google`.'
4
4
  ---
5
5
 
6
6
  # `@warlock.js/ai-google`
@@ -29,21 +29,21 @@ const vertex = new GoogleSDK({
29
29
  ## Producing a model
30
30
 
31
31
  ```ts
32
- google.model({ name: "gemini-2.5-flash" })
33
- google.model({ name: "gemini-2.5-pro", temperature: 0.2 })
34
- google.model({ name: "gemini-1.5-flash", maxTokens: 2048 })
32
+ google.model({ name: "gemini-2.5-flash" });
33
+ google.model({ name: "gemini-2.5-pro", temperature: 0.2 });
34
+ google.model({ name: "gemini-1.5-flash", maxTokens: 2048 });
35
35
  ```
36
36
 
37
37
  ## Capabilities — what's auto-set
38
38
 
39
- | Flag | Default |
40
- | --- | --- |
41
- | `structuredOutput` | `true` (via `responseMimeType` + `responseJsonSchema`) |
42
- | `vision` | Inferred from model id substring. `true` when the id contains `gemini-1.5`, `gemini-2` (covers 2.x / 2.5), `gemini-exp`, `gemini-flash`, or `gemini-pro-vision`; `false` for bare `gemini-pro` / `gemini-1.0-pro` and any unknown id. Case-insensitive, tolerates date/preview/vendor-prefix suffixes (`models/gemini-1.5-flash-001`). |
43
- | `reasoning` | `true` — every Gemini 2.5 model thinks; older families harmlessly ignore an empty budget. Set `reasoning: false` to stop forwarding `thinkingConfig` to a model that rejects it. |
44
- | `promptCaching` | Always `true` — Gemini reports cache-read hits (`cachedContentTokenCount`) via implicit caching and accepts explicit context caching. |
45
- | `audio` | Mirrors the `vision` inference (the multimodal Gemini families accept audio parts). Override with `audio: true \| false`. |
46
- | `pdf` | Mirrors the `vision` inference (the multimodal Gemini families accept PDF / document parts). Override with `pdf: true \| false`. |
39
+ | Flag | Default |
40
+ | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
41
+ | `structuredOutput` | `true` (via `responseMimeType` + `responseJsonSchema`) |
42
+ | `vision` | Inferred from model id substring. `true` when the id contains `gemini-1.5`, `gemini-2` (covers 2.x / 2.5), `gemini-exp`, `gemini-flash`, or `gemini-pro-vision`; `false` for bare `gemini-pro` / `gemini-1.0-pro` and any unknown id. Case-insensitive, tolerates date/preview/vendor-prefix suffixes (`models/gemini-1.5-flash-001`). |
43
+ | `reasoning` | `true` — every Gemini 2.5 model thinks; older families harmlessly ignore an empty budget. Set `reasoning: false` to stop forwarding `thinkingConfig` to a model that rejects it. |
44
+ | `promptCaching` | Always `true` — Gemini reports cache-read hits (`cachedContentTokenCount`) via implicit caching and accepts explicit context caching. |
45
+ | `audio` | Mirrors the `vision` inference (the multimodal Gemini families accept audio parts). Override with `audio: true \| false`. |
46
+ | `pdf` | Mirrors the `vision` inference (the multimodal Gemini families accept PDF / document parts). Override with `pdf: true \| false`. |
47
47
 
48
48
  Explicit config always wins (`google.model({ name, reasoning: false, audio: false, pdf: true })`).
49
49
 
@@ -56,6 +56,15 @@ Gemini content roles must be `"user"` or `"model"` — there is no `"system"` ro
56
56
  - Neutral `assistant` → Gemini `"model"`; `user` stays `"user"`.
57
57
  - Assistant tool calls → `"model"` content: optional leading `{ text }` + one `{ functionCall: { name, args } }` part per call (`id` deliberately omitted — see below).
58
58
  - Tool results (`role: "tool"`) → a `"user"` content with one `{ functionResponse: { name, response } }` part. `response` must be a JSON object, so a stringified-JSON tool result is parsed back to an object; a non-object result is wrapped as `{ result: <raw> }`.
59
+ - Adjacent contents with the same role merge into one, parts kept in order: parallel tool results become ONE `"user"` content with several `functionResponse` parts in call order (as `@google/genai`'s automatic function calling sends them), and an agent compaction summary merges with a same-role neighbour (user-framed summary + input; assistant summary + the next tool-call turn, text part first, each `functionCall` keeping its `thoughtSignature`). Roles therefore alternate `user`/`model`; `countTokens()` uses the same builder.
60
+
61
+ Neutral history and the `contents` the adapter sends for it:
62
+
63
+ ```text
64
+ user "look both up" → { role: "user", parts: [{ text }] }
65
+ assistant toolCalls [a, b] → { role: "model", parts: [{ functionCall: a }, { functionCall: b }] }
66
+ tool a, tool b → { role: "user", parts: [{ functionResponse: a }, { functionResponse: b }] }
67
+ ```
59
68
 
60
69
  **Gemini matches a result to its call by name, not id** — the Developer API assigns no function-call ids. The adapter sets neutral `id` = tool name so `toolCallId` is non-empty. The wire `id` is omitted from both `functionCall` and `functionResponse` (echoing an empty/synthetic id is rejected by Gemini). **Limitation:** two parallel calls to the same tool in one turn share an id — inherent to the Developer API's id-less design.
61
70
 
@@ -83,12 +92,12 @@ PDF and audio reach the wire only when the model declares the matching capabilit
83
92
 
84
93
  `google.image({ name })` returns an `ImageModelContract` for the `ai.image()` verb. **The model id picks the transport:**
85
94
 
86
- | Model id | Transport | Usage it can report |
87
- | --- | --- | --- |
95
+ | Model id | Transport | Usage it can report |
96
+ | ----------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
88
97
  | `gemini-*` (e.g. `gemini-3.1-flash-lite-image`) | `ai.models.generateContent` with `responseModalities` including `"IMAGE"` | whatever `usageMetadata` Google attaches — price with `{ input, output }` |
89
- | anything else (`imagen-*`, unknown ids) | `ai.models.generateImages` (Imagen) | none, always zero — price with `{ perImage }` |
98
+ | anything else (`imagen-*`, unknown ids) | `ai.models.generateImages` (Imagen) | none, always zero — price with `{ perImage }` |
90
99
 
91
- A `gemini-` id sent to `generateImages` comes back `404 … is not supported for predict` (observed verbatim from Google) — `generateImages` routes to `{model}:predict`. `generateContent` is the SDK's own named replacement: its runtime deprecation notice for `generateImages` says *"Please use the generateContent method with image models instead"*. Routing on the id is what lets both families go through one factory. A leading `models/` resource prefix is tolerated.
100
+ A `gemini-` id sent to `generateImages` comes back `404 … is not supported for predict` (observed verbatim from Google) — `generateImages` routes to `{model}:predict`. `generateContent` is the SDK's own named replacement: its runtime deprecation notice for `generateImages` says _"Please use the generateContent method with image models instead"_. Routing on the id is what lets both families go through one factory. A leading `models/` resource prefix is tolerated.
92
101
 
93
102
  This is **routing, not validation** — no id is refused locally. An id matching neither family takes the `generateImages` route (the pre-existing one) and fails at Google as a typed provider error, not with a local throw.
94
103
 
@@ -99,13 +108,13 @@ This is **routing, not validation** — no id is refused locally. An id matching
99
108
  ```ts
100
109
  const gemini = google.image({
101
110
  name: "gemini-3.1-flash-lite-image",
102
- pricing: { input: 0.3, output: 30 }, // token-metered
111
+ pricing: { input: 0.3, output: 30 }, // token-metered
103
112
  });
104
113
 
105
114
  const { data, error, usage } = await ai.image({
106
115
  model: gemini,
107
116
  prompt: "a red bicycle on a white background",
108
- aspectRatio: "16:9", // → config.imageConfig.aspectRatio
117
+ aspectRatio: "16:9", // → config.imageConfig.aspectRatio
109
118
  options: { imageSize: "2K", personGeneration: "ALLOW_ADULT" }, // → imageConfig passthroughs
110
119
  });
111
120
  ```
@@ -127,7 +136,7 @@ const imagen = google.image({ name: "imagen-4.0-generate-001", pricing: { perIma
127
136
  const { data, error } = await ai.image({
128
137
  model: imagen,
129
138
  prompt: "a watercolor lighthouse at dawn",
130
- aspectRatio: "3:4", // Imagen ratio (vs OpenAI's WxH `size`)
139
+ aspectRatio: "3:4", // Imagen ratio (vs OpenAI's WxH `size`)
131
140
  negativePrompt: "text, watermark",
132
141
  options: { imageSize: "2K", personGeneration: "allow_adult" }, // Imagen passthroughs
133
142
  });
@@ -135,7 +144,7 @@ const { data, error } = await ai.image({
135
144
 
136
145
  - Imagen is **per-image-metered** (price with `{ perImage }`) and returns base64 bytes — no hosted URL, and `usage` is an honest `{ input: 0, output: 0, total: 0 }`.
137
146
  - When every candidate is safety-filtered (`raiFilteredReason`), the run surfaces a typed `ContentFilterError` on `result.error`.
138
- - ⏳ **This path is on a clock.** `@google/genai` warns at runtime: *"The generateImages method is deprecated and will be removed in the next major release (not before Jan. 1 2027). Please use the generateContent method with image models instead."* This package depends on `@google/genai` `^2.17.1`, which does emit that warning. Nothing breaks today, but new work should prefer a `gemini-*` id.
147
+ - ⏳ **This path is on a clock.** `@google/genai` warns at runtime: _"The generateImages method is deprecated and will be removed in the next major release (not before Jan. 1 2027). Please use the generateContent method with image models instead."_ This package depends on `@google/genai` `^2.17.1`, which does emit that warning. Nothing breaks today, but new work should prefer a `gemini-*` id.
139
148
 
140
149
  ### Not implemented: the `interactions` API
141
150
 
@@ -147,6 +156,17 @@ The verb surface lives in the `generate-images` topic of the `warlock-js-ai` ski
147
156
 
148
157
  `model.stream()` drains `generateContentStream`. Each chunk's `.text` → `{ type: "delta" }`; `functionCall` parts (read from `candidates[0].content.parts`, not the `.functionCalls` getter — the getter drops `thoughtSignature`) are emitted as `{ type: "tool-call" }` **fully formed** (Gemini streams a complete call with parsed `args`, not partial JSON), carrying `providerMetadata.thoughtSignature` when present. Terminal `{ type: "done", finishReason, usage }` — usage from the final chunk's `usageMetadata`.
149
158
 
159
+ Opt-in chunks (`options.streamChunks`, set by `agent.stream()`): `{ type: "reasoning", delta }` per thought part (`thought: true`; sent only with `providerOptions.google.body.thinkingConfig.includeThoughts: true`) and `{ type: "usage", usage }` per chunk carrying `usageMetadata`. No `tool-args` (calls arrive whole).
160
+
161
+ **Provider-defined tools:** `ai.providerTool({ provider: "google", definition: { googleSearch: {} } })` (also `{ codeExecution: {} }`, `{ urlContext: {} }`) is appended verbatim as its own `Tool` after the function declarations; other providers' tools are skipped with a warning.
162
+
163
+ ```ts
164
+ ai.agent({
165
+ model: google.model({ name: "gemini-2.5-flash" }),
166
+ tools: [ai.providerTool({ provider: "google", definition: { googleSearch: {} } })],
167
+ });
168
+ ```
169
+
150
170
  ## Finish-reason mapping
151
171
 
152
172
  `STOP` → `stop` · `MAX_TOKENS` → `length` · `SAFETY` / `RECITATION` / `BLOCKLIST` / `PROHIBITED_CONTENT` / `MALFORMED_FUNCTION_CALL` / unknown / null → `error`. `tool_calls` is derived from function-call presence.
@@ -156,7 +176,7 @@ The verb surface lives in the `generate-images` topic of the `warlock-js-ai` ski
156
176
  ```ts
157
177
  const embedder = google.embedder({ name: "gemini-embedding-001" });
158
178
  const { vector } = await embedder.embed("Hello world");
159
- const { vectors } = await embedder.embedMany(["a", "b"]); // single batched call
179
+ const { vectors } = await embedder.embedMany(["a", "b"]); // single batched call
160
180
  ```
161
181
 
162
182
  `embedContent` accepts an array natively, so `embedMany` is **one request** (unlike Bedrock/Titan). Pass `dimensions` to forward Gemini's `outputDimensionality` truncation hint.
@@ -172,14 +192,31 @@ Wrapped into the typed `@warlock.js/ai` `AIError` hierarchy. Gemini has no machi
172
192
  - 400 with token/context phrasing → `ContextLengthExceededError`, else `InvalidRequestError`
173
193
  - 404 / any other 4xx → `InvalidRequestError`
174
194
  - 504 / `DEADLINE_EXCEEDED` / `ETIMEDOUT` / `ECONNABORTED` / `AbortError` → `ProviderTimeoutError`
195
+ - 503 / `UNAVAILABLE` / "overloaded" → `ProviderOverloadedError` (`code: "PROVIDER_OVERLOADED"`, `retryable: true`) — Gemini documents 503 as "temporarily overloaded or down" (https://ai.google.dev/gemini-api/docs/api-errors)
175
196
  - anything else (5xx and status-less) → `ProviderError`
176
197
 
198
+ Every wrapped error carries `status`, `retryable` and `billed`. Google states "If your request fails with a 400 or 500 error, you won't be charged for the tokens used" (https://ai.google.dev/gemini-api/docs/billing, checked 2026-10-17), so a 400 or 500 is `billed: false`; 401 / 403 are `false` too (never reached a billable account); everything else is `"unknown"`.
199
+
200
+ ```ts
201
+ import { ai, ProviderError, ProviderOverloadedError } from "@warlock.js/ai";
202
+ import { GoogleSDK } from "@warlock.js/ai-google";
203
+
204
+ const google = new GoogleSDK({ apiKey: process.env.GEMINI_API_KEY ?? "" });
205
+ const result = await ai.agent({ model: google.model({ name: "gemini-2.5-flash" }) }).execute("hi");
206
+
207
+ if (result.error instanceof ProviderOverloadedError) {
208
+ console.log(result.error.status, result.error.retryable); // 503, true — retry or fall back
209
+ } else if (result.error instanceof ProviderError) {
210
+ console.log(result.error.billed); // false for a 400 / 500
211
+ }
212
+ ```
213
+
177
214
  Timeout is checked first, so an aborted/`AbortError` request always classifies as `ProviderTimeoutError` regardless of status. `AIError` instances pass through unwrapped (no double-wrap).
178
215
 
179
216
  ## Token counting
180
217
 
181
218
  ```ts
182
- await google.count("some text") // approximate heuristic, offline
219
+ await google.count("some text"); // approximate heuristic, offline
183
220
  ```
184
221
 
185
222
  `count(text, model?)` accepts an optional model arg for signature parity but **ignores it** — the estimate is `approximateTokenCount` (≈ `ceil(chars / 4)`) from `@warlock.js/ai`, never a `countTokens` network call.
@@ -200,25 +237,81 @@ Gemini's `thinkingBudget` semantics (for direct `maxTokens` use): `0` disables t
200
237
 
201
238
  ## Token usage
202
239
 
203
- `complete()` / `stream()` map Gemini's `usageMetadata` into the neutral `Usage` (`{ input, output, total }`): `promptTokenCount` → `input`, `candidatesTokenCount` → `output`, `totalTokenCount` → `total` (falling back to `input + output` when absent). `cachedContentTokenCount`, when > 0, is surfaced as `usage.cachedTokens`; `thoughtsTokenCount` (a reasoning model's thinking-phase tokens), when > 0, as `usage.reasoningTokens`. Both are omitted when zero/absent. Embeddings have no usage (see above).
240
+ `complete()` / `stream()` map Gemini's `usageMetadata` into the neutral `Usage` (`{ input, output, total }`): `promptTokenCount` → `input`, `candidatesTokenCount` → `output`, `totalTokenCount` → `total` (falling back to `input + output` when absent). `cachedContentTokenCount`, when > 0, is surfaced as `usage.cachedTokens`; `thoughtsTokenCount` (a reasoning model's thinking-phase tokens), when > 0, as `usage.reasoningTokens`. Both are omitted when zero/absent. `promptTokenCount` "includes the number of tokens in the cached content" (https://ai.google.dev/api/generate-content#UsageMetadata), so the usage is marked `inputIncludesCache: true` and cost bills cached tokens out of `input` once. When the model is priced, `usage.cost` is attached on `complete()` and the streaming `done` chunk. Embeddings have no usage (see above).
204
241
 
205
242
  ## Pricing
206
243
 
207
- Optional USD pricing flows in two ways, resolved per `model()` call:
244
+ Resolution per `model()` call: per-model `pricing` > SDK-level registry entry > built-in `GOOGLE_MODEL_PRICING` > `undefined` (no cost computed). The resolved `ModelPricing` is attached to the produced model, and each response's `usage.cost` is computed from it.
208
245
 
209
246
  ```ts
210
- // SDK-level registry, keyed by model name:
247
+ import { totalCost } from "@warlock.js/ai";
248
+ import { GOOGLE_MODEL_PRICING, GoogleSDK, googlePricingFor } from "@warlock.js/ai-google";
249
+
211
250
  const google = new GoogleSDK({
212
- apiKey,
213
- pricing: { "gemini-2.5-flash": { input: 0.3, output: 2.5 } },
251
+ apiKey: process.env.GEMINI_API_KEY ?? "",
252
+ pricing: { "gemini-2.5-flash": { input: 0.3, output: 2.5, cachedInput: 0.03 } }, // text-input rate
214
253
  });
215
254
 
216
- google.model({ name: "gemini-2.5-flash" }); // → inherits the registry entry
217
- google.model({ name: "gemini-2.5-flash", pricing: {...} }); // → per-model pricing wins
218
- google.model({ name: "gemini-2.5-pro" }); // → no entry → pricing undefined (no cost computed)
255
+ google.model({ name: "gemini-2.5-flash" }); // → the registry entry
256
+ google.model({ name: "gemini-3.5-flash-lite" }).pricing; // → built-in { input: 0.3, output: 2.5, cachedInput: 0.03 }
257
+ google.model({ name: "gemini-2.5-pro" }).pricing; // → undefined (prompt-length tiers; pass pricing yourself)
258
+ googlePricingFor("gemini-3.5-flash-lite"); // the same built-in entry
259
+ Object.keys(GOOGLE_MODEL_PRICING); // the priced ids
260
+
261
+ const { usage } = await google
262
+ .model({ name: "gemini-3.5-flash-lite" })
263
+ .complete([{ role: "user", content: "hi" }]);
264
+ console.log(totalCost(usage.cost));
219
265
  ```
220
266
 
221
- Resolution order: per-model `pricing` > SDK-level registry entry > `undefined`. The resolved `ModelPricing` is attached to the produced model so the agent runtime can compute cost from usage.
267
+ `GOOGLE_MODEL_PRICING` comes from https://ai.google.dev/gemini-api/docs/pricing (paid tier, Standard), fetched 2026-10-17. Only models with a single flat token price are listed. Deliberately unpriced: the Pro models (priced by prompt length, split at 200k prompt tokens), models whose audio input bills higher than text (`Usage` does not split input by modality — price them with the text rate yourself only if you send no audio), and models with a scheduled price change. The hourly context-cache storage charge is not a per-token rate and is never included.
268
+
269
+ ## Provider config options and input-token counts
270
+
271
+ Native body fields merge after neutral options. When both values are plain
272
+ objects, their immediate keys merge and caller-provided keys win; untouched
273
+ neutral fields remain. Arrays, scalars and `null` replace the field. This merge
274
+ is one level deep: a nested object explicitly supplied by the caller replaces
275
+ that nested value.
276
+
277
+ `providerOptions.google.body` adds native `GenerateContentConfig` fields,
278
+ merged after neutral generation options for both completion and streaming:
279
+
280
+ ```ts
281
+ const options = { providerOptions: { google: { body: { topP: 0.8, topK: 4 } } } };
282
+ await model.complete(messages, options);
283
+ ```
284
+
285
+ The key stays `google` for custom provider labels. Other provider keys are
286
+ ignored. `GoogleCallOptions` is exported. `systemInstruction`, `tools`,
287
+ `abortSignal` and `httpOptions` are protected and throw `InvalidRequestError`;
288
+ use neutral messages/tools/signal and SDK transport configuration for them.
289
+
290
+ `model.countTokens(messages, options?)` counts the converted contents, system
291
+ instructions and tools on both the Gemini Developer API (API key) and Vertex AI.
292
+ Developer API counting sends the full `generateContentRequest`, including tool
293
+ configuration and generation settings, as documented in the
294
+ [Gemini token-counting reference](https://ai.google.dev/api/tokens). It uses the
295
+ existing SDK transport, preserving API-key auth, custom fetch, base URL/proxy
296
+ settings, headers, retries and cancellation. Vertex retains its typed SDK path.
297
+
298
+ Cached-content references still throw `InvalidRequestError` with
299
+ `reason: "count-unsupported"`. Use `countInputTokens` for budget preflight: it
300
+ returns an estimate for this explicit limitation; authentication, network,
301
+ rate-limit and ordinary validation errors still propagate. `sdk.count(text)`
302
+ remains a separate offline estimate.
303
+
304
+ ```ts
305
+ import { countInputTokens } from "@warlock.js/ai";
306
+
307
+ const messages = [
308
+ { role: "system" as const, content: "Be concise." },
309
+ { role: "user" as const, content: "Hello" },
310
+ ];
311
+ const tools = [{ name: "lookup", description: "Look up data", execute: async () => "ok" }];
312
+ const { tokens, source } = await countInputTokens(model, messages, { tools });
313
+ // source is "native" for ordinary Developer API and Vertex requests, including tools.
314
+ ```
222
315
 
223
316
  ## When NOT to use this skill
224
317
 
@@ -230,3 +323,41 @@ Resolution order: per-model `pricing` > SDK-level registry entry > `undefined`.
230
323
  - The `run-ai-agent` topic of the `warlock-js-ai` skill
231
324
  - The `pick-ai-provider` topic of the `warlock-js-ai` skill
232
325
  - The `embed-text` topic of the `warlock-js-ai` skill
326
+
327
+ ## Search grounding metadata and fees
328
+
329
+ `response.providerMetadata.google.groundingMetadata` retains the raw grounding
330
+ metadata for each indexed candidate: queries, chunks, supports and search entry
331
+ point. Streaming `done` carries the same metadata. Agent reports retain it per
332
+ trip. Hosted search never produces a local function call.
333
+
334
+ `usage.serverToolUse.webSearchRequests` counts unique nonempty
335
+ `groundingMetadata.webSearchQueries`; `googleWebSearchQueries` records the raw
336
+ array length, including duplicates and empty entries. Stream snapshots replace
337
+ prior metadata per candidate rather than accumulating repeated snapshots.
338
+ The [official schema](https://ai.google.dev/api/generate-content#GroundingMetadata)
339
+ defines the queries array.
340
+
341
+ [Google's pricing guide](https://ai.google.dev/gemini-api/docs/google-search#pricing)
342
+ (verified 2026-10-18) bills Gemini 3 and newer per unique nonempty query: $14 per
343
+ 1,000 queries after the shared monthly free allowance. You can explicitly use
344
+ the exported `GOOGLE_SERVER_TOOL_PRICING` as `pricing.serverTools` for that
345
+ model family. Gemini 2.5 and older bill per grounded prompt; their query count
346
+ is not a billable-prompt count, so leave this fee unpriced or account for it
347
+ outside this per-query table. Free allowances are not subtracted locally.
348
+ No web-fetch/code-execution count is inferred from result parts. See
349
+ [cost tracking](https://warlock.js.org/v/latest/ai/recipes/cost-tracking/#hosted-tool-request-fees).
350
+
351
+ ```ts
352
+ import { GOOGLE_SERVER_TOOL_PRICING } from "@warlock.js/ai-google";
353
+ const model = google.model({
354
+ name: "gemini-3.5-flash-lite",
355
+
356
+ pricing: { ...{ input: 0.3, output: 2.5 }, serverTools: GOOGLE_SERVER_TOOL_PRICING },
357
+ });
358
+ const response = await model.complete(messages, {
359
+ providerTools: [{ provider: "google", definition: { googleSearch: {} } }],
360
+ });
361
+ console.log(response.usage.serverToolUse?.webSearchRequests);
362
+ console.log(response.providerMetadata);
363
+ ```