@warlock.js/ai-google 4.14.0 → 4.15.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.
@@ -0,0 +1,231 @@
1
+ import { applyGoogleUsage } from "./utils/apply-google-usage.mjs";
2
+ import { wrapGoogleError } from "./utils/wrap-google-error.mjs";
3
+ import "./utils/index.mjs";
4
+ import { ContentFilterError, ProviderError } from "@warlock.js/ai";
5
+ import { log } from "@warlock.js/logger";
6
+
7
+ //#region ../ai-google/src/gemini-image.ts
8
+ const LOG_MODULE = "ai.google";
9
+ /**
10
+ * Response modalities requested when the caller names none.
11
+ *
12
+ * `IMAGE` is the modality this adapter extracts; `TEXT` rides along so
13
+ * a model that narrates what it drew is not answering outside the set
14
+ * it was granted (the narration is then dropped — only inline image
15
+ * parts become `GeneratedImage`s).
16
+ *
17
+ * *Unverified:* which pairing any individual Gemini image model
18
+ * requires is not established here — no spec or run in this package
19
+ * touches the live API. `options.responseModalities` replaces this list
20
+ * verbatim for a model that wants something else.
21
+ */
22
+ const DEFAULT_RESPONSE_MODALITIES = ["TEXT", "IMAGE"];
23
+ /**
24
+ * Gemini `finishReason` values that mean generation was stopped by a
25
+ * safety / policy rule rather than by the model simply not drawing.
26
+ * Taken from the `FinishReason` enum in `@google/genai`'s own type
27
+ * declarations, whose doc comments describe each of these as content
28
+ * or image generation being "stopped" for safety, prohibited content,
29
+ * recitation, blocklist, or SPII.
30
+ */
31
+ const FILTERED_FINISH_REASONS = new Set([
32
+ "SAFETY",
33
+ "IMAGE_SAFETY",
34
+ "PROHIBITED_CONTENT",
35
+ "IMAGE_PROHIBITED_CONTENT",
36
+ "RECITATION",
37
+ "IMAGE_RECITATION",
38
+ "BLOCKLIST",
39
+ "SPII"
40
+ ]);
41
+ /** How much of a text-only answer to quote back inside the error message. */
42
+ const TEXT_EXCERPT_LIMIT = 200;
43
+ /**
44
+ * Gemini-native implementation of `ImageModelContract`, via
45
+ * `ai.models.generateContent` with `config.responseModalities`
46
+ * including `"IMAGE"`.
47
+ *
48
+ * **Why a second image adapter.** `GoogleImageModel` calls
49
+ * `ai.models.generateImages`, which the `@google/genai` bundle routes
50
+ * to `{model}:predict` (`generateImages` → `generateImagesInternal` →
51
+ * `formatMap('{model}:predict', …)`). A Gemini image model is not
52
+ * served there: asking for one returns Google's
53
+ * `404 … is not found for API version v1beta, or is not supported for
54
+ * predict`. `generateContent` is the SDK's own named replacement — its
55
+ * runtime deprecation notice for `generateImages` reads "Please use the
56
+ * generateContent method with image models instead" — so that is the
57
+ * transport this class speaks, hence a separate class rather than a
58
+ * branch inside `image.ts`.
59
+ *
60
+ * **Same envelope.** Inline image parts are mapped to the identical
61
+ * `GeneratedImage[]` shape `GoogleImageModel` produces, so `ai.image()`
62
+ * callers see no difference between the two paths.
63
+ *
64
+ * **Token usage is passed through, not zeroed.** The Imagen path
65
+ * returns a hard `{ 0, 0, 0 }` because Imagen reports no tokens at all;
66
+ * here, whatever `usageMetadata` Google attaches is mapped by the same
67
+ * {@link applyGoogleUsage} the chat model uses, and only an absent
68
+ * block collapses to zeros. Price accordingly.
69
+ *
70
+ * **No model-id validation.** `config.name` is forwarded to
71
+ * `generateContent` exactly as given; nothing here inspects it. An id
72
+ * Google does not serve fails at Google, wrapped into the typed
73
+ * `AIError` hierarchy — never with a local throw.
74
+ *
75
+ * **Evidence, in two tiers.** No spec in this package calls Google.
76
+ * *Measured here:* a `gemini-*` image id, which 404s on the `predict`
77
+ * transport, reached the model on this one and came back with a quota
78
+ * error (HTTP 429) — the endpoint accepts the id. *Reported by the
79
+ * maintainer:* once billing was enabled on the project, an image came
80
+ * back end-to-end from an application running a locally linked build.
81
+ * *Still unestablished:* whether these models report token usage — no
82
+ * `usageMetadata` from a successful image call has been observed, so
83
+ * the pass-through above is untested against a real response.
84
+ *
85
+ * @example
86
+ * const model = new GeminiImageModel(ai, { name: "gemini-3.1-flash-lite-image" });
87
+ * const { images, usage } = await model.generate("a red bicycle on a white background");
88
+ */
89
+ var GeminiImageModel = class {
90
+ constructor(ai, config, provider = "google") {
91
+ this.logger = log;
92
+ this.ai = ai;
93
+ this.name = config.name;
94
+ this.provider = provider;
95
+ this.pricing = config.pricing;
96
+ }
97
+ async generate(prompt, options) {
98
+ const config = this.buildConfig(options);
99
+ this.logger.debug(LOG_MODULE, "image.request", "models.generateContent", {
100
+ model: this.name,
101
+ responseModalities: config.responseModalities
102
+ });
103
+ let response;
104
+ try {
105
+ response = await this.ai.models.generateContent({
106
+ model: this.name,
107
+ contents: prompt,
108
+ config
109
+ });
110
+ } catch (thrown) {
111
+ const wrapped = wrapGoogleError(thrown);
112
+ this.logger.error(LOG_MODULE, "image.error", wrapped.message, {
113
+ code: wrapped.code,
114
+ context: wrapped.context
115
+ });
116
+ throw wrapped;
117
+ }
118
+ const parts = collectParts(response);
119
+ const images = toGeneratedImages(parts);
120
+ if (images.length === 0) throw this.noImageError(response, parts);
121
+ const usage = {
122
+ input: 0,
123
+ output: 0,
124
+ total: 0
125
+ };
126
+ if (response.usageMetadata) applyGoogleUsage(usage, response.usageMetadata);
127
+ this.logger.debug(LOG_MODULE, "image.response", "models.generateContent succeeded", {
128
+ images: images.length,
129
+ usage
130
+ });
131
+ return {
132
+ images,
133
+ usage
134
+ };
135
+ }
136
+ /**
137
+ * Assemble the `GenerateContentConfig` for an image turn: the
138
+ * requested modalities, the image-specific knobs Gemini exposes under
139
+ * `imageConfig`, and the cancellation handle.
140
+ *
141
+ * Three neutral options are deliberately NOT forwarded, because
142
+ * `GenerateContentConfig` / `ImageConfig` in `@google/genai` expose
143
+ * no equivalent for them on this path: `count` (no per-request image
144
+ * count — every inline image part the model does return is mapped),
145
+ * `negativePrompt` (an Imagen-only field), and `format`
146
+ * (`ImageConfig.outputMimeType` is documented "not supported in
147
+ * Gemini API"). Fold those intentions into the prompt instead.
148
+ */
149
+ buildConfig(options) {
150
+ const imageConfig = {};
151
+ if (options?.aspectRatio !== void 0) imageConfig.aspectRatio = options.aspectRatio;
152
+ if (typeof options?.imageSize === "string") imageConfig.imageSize = options.imageSize;
153
+ if (typeof options?.personGeneration === "string") imageConfig.personGeneration = options.personGeneration;
154
+ const requested = options?.responseModalities;
155
+ return {
156
+ responseModalities: Array.isArray(requested) ? requested : DEFAULT_RESPONSE_MODALITIES,
157
+ ...Object.keys(imageConfig).length > 0 ? { imageConfig } : {},
158
+ ...options?.signal ? { abortSignal: options.signal } : {}
159
+ };
160
+ }
161
+ /**
162
+ * Build the typed error for a response that carried no inline image
163
+ * part. Never a silent empty success: the caller asked for an image
164
+ * and got something else, so the error names what actually came back.
165
+ *
166
+ * - A blocked prompt (`promptFeedback.blockReason`) or a
167
+ * safety/policy `finishReason` → `ContentFilterError` carrying the
168
+ * reason, matching how the Imagen path reports `raiFilteredReason`.
169
+ * - A text-only answer → `ProviderError` quoting the text, so the
170
+ * log says what the model replied instead of guessing.
171
+ * - Anything else → `ProviderError` naming the finish reason and how
172
+ * many parts arrived.
173
+ */
174
+ noImageError(response, parts) {
175
+ const blockReason = response.promptFeedback?.blockReason;
176
+ if (blockReason) return new ContentFilterError(`Gemini blocked the prompt for ${this.name}: ${blockReason}`, { reason: blockReason });
177
+ const finishReason = response.candidates?.[0]?.finishReason;
178
+ if (finishReason && FILTERED_FINISH_REASONS.has(finishReason)) return new ContentFilterError(`Gemini filtered the image for ${this.name}: ${finishReason}`, { reason: finishReason });
179
+ const text = collectText(parts);
180
+ if (text) return new ProviderError(`Gemini returned no image for ${this.name} — the response was text only: "${excerpt(text)}"`, { context: {
181
+ model: this.name,
182
+ ...finishReason ? { finishReason } : {}
183
+ } });
184
+ return new ProviderError(`Gemini returned no image part for ${this.name} (parts: ${parts.length}${finishReason ? `, finishReason: ${finishReason}` : ""}).`, { context: {
185
+ model: this.name,
186
+ parts: parts.length
187
+ } });
188
+ }
189
+ };
190
+ /**
191
+ * Flatten every candidate's content parts into one list. Read off
192
+ * `candidates[].content.parts` rather than the response's convenience
193
+ * getters: `response.text` covers only the first candidate's text and
194
+ * there is no getter for inline image data at all.
195
+ */
196
+ function collectParts(response) {
197
+ const parts = [];
198
+ for (const candidate of response.candidates ?? []) parts.push(...candidate.content?.parts ?? []);
199
+ return parts;
200
+ }
201
+ /**
202
+ * Map the inline image parts to the neutral `GeneratedImage[]` — the
203
+ * same `{ type: "base64", base64, mediaType }` shape the Imagen path
204
+ * emits, including its `image/png` fallback for a part that arrives
205
+ * without a declared mime type.
206
+ */
207
+ function toGeneratedImages(parts) {
208
+ const images = [];
209
+ for (const part of parts) {
210
+ const data = part.inlineData?.data;
211
+ if (!data) continue;
212
+ images.push({
213
+ type: "base64",
214
+ base64: data,
215
+ mediaType: part.inlineData?.mimeType ?? "image/png"
216
+ });
217
+ }
218
+ return images;
219
+ }
220
+ /** Join the text parts of a response — what the model said instead of drawing. */
221
+ function collectText(parts) {
222
+ return parts.map((part) => part.text).filter((text) => typeof text === "string" && text.length > 0).join(" ").trim();
223
+ }
224
+ /** Trim a quoted model answer so an error message stays readable. */
225
+ function excerpt(text) {
226
+ return text.length > TEXT_EXCERPT_LIMIT ? `${text.slice(0, TEXT_EXCERPT_LIMIT)}…` : text;
227
+ }
228
+
229
+ //#endregion
230
+ export { GeminiImageModel };
231
+ //# sourceMappingURL=gemini-image.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gemini-image.mjs","names":[],"sources":["../../../../../../ai-google/src/gemini-image.ts"],"sourcesContent":["import {\n ContentFilterError,\n ProviderError,\n type AIError,\n type GeneratedImage,\n type ImageGenerationOptions,\n type ImageGenerationResponse,\n type ImageModelContract,\n type ImageModelPricing,\n type Usage,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type {\n GenerateContentConfig,\n GenerateContentResponse,\n GoogleGenAI,\n ImageConfig,\n Part,\n} from \"@google/genai\";\nimport type { GoogleImageConfig } from \"./config.type\";\nimport { applyGoogleUsage, wrapGoogleError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.google\";\n\n/**\n * Response modalities requested when the caller names none.\n *\n * `IMAGE` is the modality this adapter extracts; `TEXT` rides along so\n * a model that narrates what it drew is not answering outside the set\n * it was granted (the narration is then dropped — only inline image\n * parts become `GeneratedImage`s).\n *\n * *Unverified:* which pairing any individual Gemini image model\n * requires is not established here — no spec or run in this package\n * touches the live API. `options.responseModalities` replaces this list\n * verbatim for a model that wants something else.\n */\nconst DEFAULT_RESPONSE_MODALITIES = [\"TEXT\", \"IMAGE\"];\n\n/**\n * Gemini `finishReason` values that mean generation was stopped by a\n * safety / policy rule rather than by the model simply not drawing.\n * Taken from the `FinishReason` enum in `@google/genai`'s own type\n * declarations, whose doc comments describe each of these as content\n * or image generation being \"stopped\" for safety, prohibited content,\n * recitation, blocklist, or SPII.\n */\nconst FILTERED_FINISH_REASONS = new Set([\n \"SAFETY\",\n \"IMAGE_SAFETY\",\n \"PROHIBITED_CONTENT\",\n \"IMAGE_PROHIBITED_CONTENT\",\n \"RECITATION\",\n \"IMAGE_RECITATION\",\n \"BLOCKLIST\",\n \"SPII\",\n]);\n\n/** How much of a text-only answer to quote back inside the error message. */\nconst TEXT_EXCERPT_LIMIT = 200;\n\n/**\n * Gemini-native implementation of `ImageModelContract`, via\n * `ai.models.generateContent` with `config.responseModalities`\n * including `\"IMAGE\"`.\n *\n * **Why a second image adapter.** `GoogleImageModel` calls\n * `ai.models.generateImages`, which the `@google/genai` bundle routes\n * to `{model}:predict` (`generateImages` → `generateImagesInternal` →\n * `formatMap('{model}:predict', …)`). A Gemini image model is not\n * served there: asking for one returns Google's\n * `404 … is not found for API version v1beta, or is not supported for\n * predict`. `generateContent` is the SDK's own named replacement — its\n * runtime deprecation notice for `generateImages` reads \"Please use the\n * generateContent method with image models instead\" — so that is the\n * transport this class speaks, hence a separate class rather than a\n * branch inside `image.ts`.\n *\n * **Same envelope.** Inline image parts are mapped to the identical\n * `GeneratedImage[]` shape `GoogleImageModel` produces, so `ai.image()`\n * callers see no difference between the two paths.\n *\n * **Token usage is passed through, not zeroed.** The Imagen path\n * returns a hard `{ 0, 0, 0 }` because Imagen reports no tokens at all;\n * here, whatever `usageMetadata` Google attaches is mapped by the same\n * {@link applyGoogleUsage} the chat model uses, and only an absent\n * block collapses to zeros. Price accordingly.\n *\n * **No model-id validation.** `config.name` is forwarded to\n * `generateContent` exactly as given; nothing here inspects it. An id\n * Google does not serve fails at Google, wrapped into the typed\n * `AIError` hierarchy — never with a local throw.\n *\n * **Evidence, in two tiers.** No spec in this package calls Google.\n * *Measured here:* a `gemini-*` image id, which 404s on the `predict`\n * transport, reached the model on this one and came back with a quota\n * error (HTTP 429) — the endpoint accepts the id. *Reported by the\n * maintainer:* once billing was enabled on the project, an image came\n * back end-to-end from an application running a locally linked build.\n * *Still unestablished:* whether these models report token usage — no\n * `usageMetadata` from a successful image call has been observed, so\n * the pass-through above is untested against a real response.\n *\n * @example\n * const model = new GeminiImageModel(ai, { name: \"gemini-3.1-flash-lite-image\" });\n * const { images, usage } = await model.generate(\"a red bicycle on a white background\");\n */\nexport class GeminiImageModel implements ImageModelContract {\n public readonly name: string;\n public readonly provider: string;\n public readonly pricing?: ImageModelPricing;\n\n private readonly ai: GoogleGenAI;\n private readonly logger: Logger = log;\n\n public constructor(ai: GoogleGenAI, config: GoogleImageConfig, provider: string = \"google\") {\n this.ai = ai;\n this.name = config.name;\n this.provider = provider;\n this.pricing = config.pricing;\n }\n\n public async generate(\n prompt: string,\n options?: ImageGenerationOptions,\n ): Promise<ImageGenerationResponse> {\n const config = this.buildConfig(options);\n\n this.logger.debug(LOG_MODULE, \"image.request\", \"models.generateContent\", {\n model: this.name,\n responseModalities: config.responseModalities,\n });\n\n let response: GenerateContentResponse;\n\n try {\n // `contents` takes a bare string: the SDK's own `generateContent`\n // example passes one (`contents: 'Why is the sky blue?'`).\n response = await this.ai.models.generateContent({\n model: this.name,\n contents: prompt,\n config,\n });\n } catch (thrown) {\n const wrapped = wrapGoogleError(thrown);\n\n this.logger.error(LOG_MODULE, \"image.error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n throw wrapped;\n }\n\n const parts = collectParts(response);\n const images = toGeneratedImages(parts);\n\n if (images.length === 0) {\n throw this.noImageError(response, parts);\n }\n\n const usage: Usage = { input: 0, output: 0, total: 0 };\n\n if (response.usageMetadata) {\n applyGoogleUsage(usage, response.usageMetadata);\n }\n\n this.logger.debug(LOG_MODULE, \"image.response\", \"models.generateContent succeeded\", {\n images: images.length,\n usage,\n });\n\n return { images, usage };\n }\n\n /**\n * Assemble the `GenerateContentConfig` for an image turn: the\n * requested modalities, the image-specific knobs Gemini exposes under\n * `imageConfig`, and the cancellation handle.\n *\n * Three neutral options are deliberately NOT forwarded, because\n * `GenerateContentConfig` / `ImageConfig` in `@google/genai` expose\n * no equivalent for them on this path: `count` (no per-request image\n * count — every inline image part the model does return is mapped),\n * `negativePrompt` (an Imagen-only field), and `format`\n * (`ImageConfig.outputMimeType` is documented \"not supported in\n * Gemini API\"). Fold those intentions into the prompt instead.\n */\n private buildConfig(options: ImageGenerationOptions | undefined): GenerateContentConfig {\n const imageConfig: ImageConfig = {};\n\n if (options?.aspectRatio !== undefined) {\n imageConfig.aspectRatio = options.aspectRatio;\n }\n\n // Provider passthroughs off the neutral options' index signature —\n // forwarded verbatim, never re-spelled, so the value the caller\n // wrote is the value Google rules on. `ImageConfig` documents\n // `imageSize` as `1K`/`2K`/`4K` and `personGeneration` as\n // `ALLOW_ALL`/`ALLOW_ADULT`/`ALLOW_NONE`.\n if (typeof options?.imageSize === \"string\") {\n imageConfig.imageSize = options.imageSize;\n }\n\n if (typeof options?.personGeneration === \"string\") {\n imageConfig.personGeneration = options.personGeneration;\n }\n\n const requested = options?.responseModalities;\n const responseModalities = Array.isArray(requested)\n ? (requested as string[])\n : DEFAULT_RESPONSE_MODALITIES;\n\n return {\n responseModalities,\n ...(Object.keys(imageConfig).length > 0 ? { imageConfig } : {}),\n ...(options?.signal ? { abortSignal: options.signal } : {}),\n };\n }\n\n /**\n * Build the typed error for a response that carried no inline image\n * part. Never a silent empty success: the caller asked for an image\n * and got something else, so the error names what actually came back.\n *\n * - A blocked prompt (`promptFeedback.blockReason`) or a\n * safety/policy `finishReason` → `ContentFilterError` carrying the\n * reason, matching how the Imagen path reports `raiFilteredReason`.\n * - A text-only answer → `ProviderError` quoting the text, so the\n * log says what the model replied instead of guessing.\n * - Anything else → `ProviderError` naming the finish reason and how\n * many parts arrived.\n */\n private noImageError(response: GenerateContentResponse, parts: Part[]): AIError {\n const blockReason = response.promptFeedback?.blockReason;\n\n if (blockReason) {\n return new ContentFilterError(\n `Gemini blocked the prompt for ${this.name}: ${blockReason}`,\n { reason: blockReason },\n );\n }\n\n const finishReason = response.candidates?.[0]?.finishReason;\n\n if (finishReason && FILTERED_FINISH_REASONS.has(finishReason)) {\n return new ContentFilterError(\n `Gemini filtered the image for ${this.name}: ${finishReason}`,\n { reason: finishReason },\n );\n }\n\n const text = collectText(parts);\n\n if (text) {\n return new ProviderError(\n `Gemini returned no image for ${this.name} — the response was text only: \"${excerpt(text)}\"`,\n { context: { model: this.name, ...(finishReason ? { finishReason } : {}) } },\n );\n }\n\n return new ProviderError(\n `Gemini returned no image part for ${this.name} (parts: ${parts.length}${\n finishReason ? `, finishReason: ${finishReason}` : \"\"\n }).`,\n { context: { model: this.name, parts: parts.length } },\n );\n }\n}\n\n/**\n * Flatten every candidate's content parts into one list. Read off\n * `candidates[].content.parts` rather than the response's convenience\n * getters: `response.text` covers only the first candidate's text and\n * there is no getter for inline image data at all.\n */\nfunction collectParts(response: GenerateContentResponse): Part[] {\n const parts: Part[] = [];\n\n for (const candidate of response.candidates ?? []) {\n parts.push(...(candidate.content?.parts ?? []));\n }\n\n return parts;\n}\n\n/**\n * Map the inline image parts to the neutral `GeneratedImage[]` — the\n * same `{ type: \"base64\", base64, mediaType }` shape the Imagen path\n * emits, including its `image/png` fallback for a part that arrives\n * without a declared mime type.\n */\nfunction toGeneratedImages(parts: Part[]): GeneratedImage[] {\n const images: GeneratedImage[] = [];\n\n for (const part of parts) {\n const data = part.inlineData?.data;\n\n if (!data) {\n continue;\n }\n\n images.push({\n type: \"base64\",\n base64: data,\n mediaType: part.inlineData?.mimeType ?? \"image/png\",\n });\n }\n\n return images;\n}\n\n/** Join the text parts of a response — what the model said instead of drawing. */\nfunction collectText(parts: Part[]): string {\n return parts\n .map((part) => part.text)\n .filter((text): text is string => typeof text === \"string\" && text.length > 0)\n .join(\" \")\n .trim();\n}\n\n/** Trim a quoted model answer so an error message stays readable. */\nfunction excerpt(text: string): string {\n return text.length > TEXT_EXCERPT_LIMIT ? `${text.slice(0, TEXT_EXCERPT_LIMIT)}…` : text;\n}\n"],"mappings":"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"}
package/esm/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { GoogleEmbedderConfig, GoogleImageConfig, GoogleModelConfig, GoogleSDKConfig } from "./config.type.mjs";
2
2
  import { GoogleSDK } from "./sdk.mjs";
3
+ import { GeminiImageModel } from "./gemini-image.mjs";
3
4
  import { GoogleImageModel } from "./image.mjs";
4
- export { type GoogleEmbedderConfig, type GoogleImageConfig, GoogleImageModel, type GoogleModelConfig, GoogleSDK, type GoogleSDKConfig };
5
+ export { GeminiImageModel, type GoogleEmbedderConfig, type GoogleImageConfig, GoogleImageModel, type GoogleModelConfig, GoogleSDK, type GoogleSDKConfig };
package/esm/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
+ import { GeminiImageModel } from "./gemini-image.mjs";
1
2
  import { GoogleImageModel } from "./image.mjs";
2
3
  import { GoogleSDK } from "./sdk.mjs";
3
4
 
4
- export { GoogleImageModel, GoogleSDK };
5
+ export { GeminiImageModel, GoogleImageModel, GoogleSDK };
package/esm/model.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { applyGoogleUsage } from "./utils/apply-google-usage.mjs";
1
2
  import { mapFinishReason } from "./utils/map-finish-reason.mjs";
2
3
  import { toGoogleContents } from "./utils/to-google-contents.mjs";
3
4
  import { toGoogleTools } from "./utils/to-google-tools.mjs";
@@ -159,7 +160,7 @@ var GoogleModel = class {
159
160
  }
160
161
  const candidateFinish = chunk.candidates?.[0]?.finishReason;
161
162
  if (candidateFinish) rawFinishReason = candidateFinish;
162
- if (chunk.usageMetadata) this.applyUsage(usage, chunk.usageMetadata);
163
+ if (chunk.usageMetadata) applyGoogleUsage(usage, chunk.usageMetadata);
163
164
  }
164
165
  } catch (thrown) {
165
166
  throw this.logAndWrap(thrown);
@@ -272,9 +273,10 @@ var GoogleModel = class {
272
273
  };
273
274
  }
274
275
  /**
275
- * Normalize Gemini's `usageMetadata` into the neutral `Usage` shape.
276
- * Cache-read tokens are surfaced as `cachedTokens` only when
277
- * non-zero. Absent usage collapses to zeros.
276
+ * Normalize Gemini's `usageMetadata` into the neutral `Usage` shape
277
+ * via the shared {@link applyGoogleUsage} mapper (the same one the
278
+ * streaming loop and the Gemini image model use). Absent usage
279
+ * collapses to zeros.
278
280
  */
279
281
  extractUsage(response) {
280
282
  const usage = {
@@ -282,30 +284,10 @@ var GoogleModel = class {
282
284
  output: 0,
283
285
  total: 0
284
286
  };
285
- if (response.usageMetadata) this.applyUsage(usage, response.usageMetadata);
287
+ if (response.usageMetadata) applyGoogleUsage(usage, response.usageMetadata);
286
288
  return usage;
287
289
  }
288
290
  /**
289
- * Fold a Gemini `usageMetadata` block into the running neutral
290
- * `Usage` accumulator. Shared by `complete()` and the streaming
291
- * loop (where the final chunk carries cumulative totals).
292
- *
293
- * Cache-read hits (`cachedContentTokenCount`, implicit or explicit
294
- * context caching) surface as `cachedTokens`; the thinking-phase
295
- * tokens of a reasoning model (`thoughtsTokenCount`) surface as
296
- * `reasoningTokens`. Both are emitted only when reported `> 0` so an
297
- * absent channel leaves the field undefined.
298
- */
299
- applyUsage(usage, raw) {
300
- usage.input = raw.promptTokenCount ?? usage.input;
301
- usage.output = raw.candidatesTokenCount ?? usage.output;
302
- usage.total = raw.totalTokenCount ?? usage.input + usage.output;
303
- const cached = raw.cachedContentTokenCount;
304
- if (cached && cached > 0) usage.cachedTokens = cached;
305
- const reasoning = raw.thoughtsTokenCount;
306
- if (reasoning && reasoning > 0) usage.reasoningTokens = reasoning;
307
- }
308
- /**
309
291
  * Wrap a thrown provider error into the typed `AIError` hierarchy
310
292
  * and emit the standard error log line before it propagates.
311
293
  */
package/esm/model.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"model.mjs","names":[],"sources":["../../../../../../ai-google/src/model.ts"],"sourcesContent":["import {\n type Message,\n type ModelCallOptions,\n type ModelCapabilities,\n type ModelContract,\n type ModelPricing,\n type ModelResponse,\n type ModelStreamChunk,\n type ModelToolCallRequest,\n type ReasoningEffort,\n type Usage,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type {\n GenerateContentConfig,\n GenerateContentResponse,\n GoogleGenAI,\n Part,\n} from \"@google/genai\";\nimport type { GoogleModelConfig } from \"./config.type\";\nimport { inferVisionCapability } from \"./known-vision-models\";\nimport { mapFinishReason, toGoogleContents, toGoogleTools, wrapGoogleError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.google\";\n\n/**\n * Bucketed `thinkingBudget` (token caps) for the neutral\n * `reasoning.effort` levels when the caller gives no explicit\n * `reasoning.maxTokens`. Gemini 2.5 accepts a positive budget as a cap\n * on the thinking phase; these mirror the spread the OpenAI\n * `reasoning_effort` low/medium/high tiers imply.\n */\nconst EFFORT_THINKING_BUDGET: Record<Exclude<ReasoningEffort, \"none\">, number> = {\n low: 1024,\n medium: 8192,\n high: 24576,\n};\n\n/**\n * Google Gemini-backed implementation of `ModelContract`.\n *\n * **Role.** The provider-facing bridge between the vendor-neutral\n * `@warlock.js/ai` agent runtime and the `@google/genai` SDK\n * (`models.generateContent` / `generateContentStream`).\n *\n * **Responsibility.**\n * - Owns: a long-lived `GoogleGenAI` client + frozen `ModelConfig`\n * (name, temperature, maxTokens) used as per-call defaults.\n * - Owns: translating vendor-neutral `Message[]` / `ToolConfig[]` into\n * Gemini shapes (systemInstruction hoisting, `model` role,\n * `functionCall` / `functionResponse` parts, inline image bytes) on\n * the way out, and Gemini's candidate/parts response (text, function\n * calls, finish reason, token usage) back into neutral shapes on the\n * way in.\n * - Does NOT own: dispatching tools, looping, history, retries — those\n * are agent concerns. The model is a per-call protocol adapter.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across calls\"): the `GoogleGenAI` client is reused for the SDK's\n * lifetime.\n *\n * @example\n * import { GoogleGenAI } from \"@google/genai\";\n * const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });\n * const model = new GoogleModel(ai, { name: \"gemini-2.5-flash\" });\n *\n * const myAgent = agent({ model, tools: [searchTool] });\n * const result = await myAgent.execute(\"Summarize today's news.\");\n */\nexport class GoogleModel implements ModelContract {\n public readonly name: string;\n public readonly provider: string;\n public readonly capabilities: ModelCapabilities;\n public readonly pricing?: ModelPricing;\n\n private readonly ai: GoogleGenAI;\n private readonly config: GoogleModelConfig;\n private readonly logger: Logger = log;\n\n public constructor(ai: GoogleGenAI, config: GoogleModelConfig, provider: string = \"google\") {\n this.ai = ai;\n this.config = config;\n this.name = config.name;\n this.provider = provider;\n this.pricing = config.pricing;\n const multimodal = config.vision ?? inferVisionCapability(config.name);\n\n this.capabilities = {\n structuredOutput: config.structuredOutput ?? true,\n vision: multimodal,\n // Every Gemini 2.5 model thinks; older families harmlessly ignore\n // an empty thinking budget. Defaulting `true` lets the agent\n // forward reasoning options; an explicit `false` opts a model out.\n reasoning: config.reasoning ?? true,\n // Gemini reports cache-read hits (`cachedContentTokenCount`) on\n // every call via implicit caching, and accepts explicit context\n // caching. Read-side accounting is always honored.\n promptCaching: true,\n // The multimodal Gemini families that accept images also accept\n // audio and PDF/document parts. Mirror the vision inference unless\n // explicitly overridden.\n audio: config.audio ?? multimodal,\n pdf: config.pdf ?? multimodal,\n };\n }\n\n /**\n * Single-shot completion. Sends the full message list to\n * `generateContent`, waits for the terminal response, and reshapes\n * it into a vendor-neutral `ModelResponse`. Per-call `options`\n * override the instance defaults for this call only.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting generateContent call\", {\n model: this.name,\n messageCount: messages.length,\n streaming: false,\n toolCount: options?.tools?.length ?? 0,\n });\n\n const { systemInstruction, contents } = toGoogleContents(messages);\n\n let response: GenerateContentResponse;\n\n try {\n response = await this.ai.models.generateContent({\n model: this.name,\n contents,\n config: this.buildConfig(systemInstruction, options),\n });\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n const toolCalls = this.extractToolCalls(response);\n const finishReason = toolCalls\n ? \"tool_calls\"\n : mapFinishReason(response.candidates?.[0]?.finishReason);\n const usage = this.extractUsage(response);\n\n this.logger.debug(LOG_MODULE, \"response\", \"generateContent call succeeded\", {\n finishReason,\n usage,\n });\n\n return {\n content: response.text ?? \"\",\n finishReason,\n usage,\n toolCalls,\n };\n }\n\n /**\n * Incremental streaming completion via `generateContentStream`.\n * Yields neutral `ModelStreamChunk`s — `delta` for text, `tool-call`\n * per function call (Gemini emits a fully-formed call, not partial\n * JSON), and a terminal `done` with the final finish reason + usage.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting generateContentStream call\", {\n model: this.name,\n messageCount: messages.length,\n streaming: true,\n toolCount: options?.tools?.length ?? 0,\n });\n\n const { systemInstruction, contents } = toGoogleContents(messages);\n\n let iterable: AsyncGenerator<GenerateContentResponse>;\n\n try {\n iterable = await this.ai.models.generateContentStream({\n model: this.name,\n contents,\n config: this.buildConfig(systemInstruction, options),\n });\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n let rawFinishReason: string | undefined;\n let sawToolCall = false;\n const usage: Usage = { input: 0, output: 0, total: 0 };\n\n try {\n for await (const chunk of iterable) {\n const text = chunk.text;\n\n if (text) {\n yield { type: \"delta\", content: text };\n }\n\n for (const part of chunk.candidates?.[0]?.content?.parts ?? []) {\n const toolCall = this.partToToolCall(part);\n\n if (!toolCall) {\n continue;\n }\n\n sawToolCall = true;\n\n yield {\n type: \"tool-call\",\n id: toolCall.id,\n name: toolCall.name,\n input: toolCall.input,\n ...(toolCall.providerMetadata\n ? { providerMetadata: toolCall.providerMetadata }\n : {}),\n };\n }\n\n const candidateFinish = chunk.candidates?.[0]?.finishReason;\n\n if (candidateFinish) {\n rawFinishReason = candidateFinish;\n }\n\n if (chunk.usageMetadata) {\n this.applyUsage(usage, chunk.usageMetadata);\n }\n }\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n const finishReason = sawToolCall ? \"tool_calls\" : mapFinishReason(rawFinishReason);\n\n this.logger.debug(LOG_MODULE, \"response\", \"generateContentStream call succeeded\", {\n finishReason,\n usage,\n });\n\n yield { type: \"done\", finishReason, usage };\n }\n\n /**\n * Assemble the `GenerateContentConfig` shared by `complete()` and\n * `stream()`: inference params, hoisted system instruction,\n * cancellation signal, and conditional tools + native structured\n * output.\n */\n private buildConfig(\n systemInstruction: string | undefined,\n options: ModelCallOptions | undefined,\n ): GenerateContentConfig {\n const temperature = options?.temperature ?? this.config.temperature;\n const maxOutputTokens = options?.maxTokens ?? this.config.maxTokens;\n\n return {\n ...(systemInstruction ? { systemInstruction } : {}),\n ...(temperature !== undefined ? { temperature } : {}),\n ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),\n ...(options?.signal ? { abortSignal: options.signal } : {}),\n ...this.buildTools(options?.tools),\n ...this.buildStructuredOutput(options?.responseSchema),\n ...this.buildThinking(options?.reasoning),\n };\n }\n\n /**\n * Translate the neutral `reasoning` option into Gemini's\n * `thinkingConfig`. `reasoning.maxTokens` maps directly to\n * `thinkingBudget` (token cap on the thinking phase); when only\n * `reasoning.effort` is given it is bucketed into a budget. Emitted\n * only when the model is `reasoning`-capable — a `false` capability\n * (config override) drops it so a non-thinking model never receives\n * an unsupported `thinkingConfig`.\n *\n * Gemini's `thinkingBudget` semantics: `0` disables thinking, `-1`\n * lets the model decide automatically. A positive value caps the\n * thinking tokens. The neutral `effort: \"none\"` (\"run without\n * reasoning\") maps to `thinkingBudget: 0`.\n */\n private buildThinking(\n reasoning: ModelCallOptions[\"reasoning\"],\n ): Pick<GenerateContentConfig, \"thinkingConfig\"> {\n if (!reasoning || !this.capabilities.reasoning) {\n return {};\n }\n\n // `effort: \"none\"` = explicit \"run without reasoning\". Gemini disables\n // thinking with `thinkingBudget: 0` (its native off switch), so emit\n // that rather than omitting the config — an omitted config lets a\n // thinking model reason at its default budget.\n if (reasoning.effort === \"none\") {\n return { thinkingConfig: { thinkingBudget: 0 } };\n }\n\n const thinkingBudget =\n reasoning.maxTokens ?? (reasoning.effort ? EFFORT_THINKING_BUDGET[reasoning.effort] : undefined);\n\n if (thinkingBudget === undefined) {\n return {};\n }\n\n return { thinkingConfig: { thinkingBudget } };\n }\n\n /**\n * Spread-friendly tools fragment. Empty object when no tools were\n * supplied so the caller can unconditionally spread it.\n */\n private buildTools(tools: ModelCallOptions[\"tools\"]): Pick<GenerateContentConfig, \"tools\"> {\n const mapped = toGoogleTools(tools);\n\n return mapped ? { tools: mapped } : {};\n }\n\n /**\n * Translate the neutral `responseSchema` into Gemini's native JSON\n * structured output (`responseMimeType: \"application/json\"` +\n * `responseJsonSchema`, which takes a raw JSON Schema directly).\n * Emitted only when the model is `structuredOutput`-capable and the\n * schema is an object root — otherwise the agent's soft prompt hint\n * + client-side `validate()` carry shape.\n */\n private buildStructuredOutput(\n responseSchema: Record<string, unknown> | undefined,\n ): Pick<GenerateContentConfig, \"responseMimeType\" | \"responseJsonSchema\"> {\n if (!responseSchema || !this.capabilities.structuredOutput) {\n return {};\n }\n\n if (responseSchema.type !== \"object\" || typeof responseSchema.properties !== \"object\") {\n return {};\n }\n\n return {\n responseMimeType: \"application/json\",\n responseJsonSchema: responseSchema,\n };\n }\n\n /**\n * Reshape Gemini's function-call content parts into the neutral\n * `ModelToolCallRequest[]`. Returns `undefined` when the model\n * requested no functions so callers can branch on presence.\n *\n * Reads `candidates[0].content.parts` directly rather than the\n * `response.functionCalls` getter: the getter discards the\n * part-level `thoughtSignature`, and Gemini \"thinking\" models 400\n * the follow-up turn if that signature is not echoed back. See\n * `partToToolCall`.\n */\n private extractToolCalls(\n response: GenerateContentResponse,\n ): ModelToolCallRequest[] | undefined {\n const parts = response.candidates?.[0]?.content?.parts ?? [];\n const toolCalls = parts\n .map((part) => this.partToToolCall(part))\n .filter((call): call is ModelToolCallRequest => call !== undefined);\n\n return toolCalls.length > 0 ? toolCalls : undefined;\n }\n\n /**\n * Map a single Gemini `Part` to a neutral `ModelToolCallRequest`,\n * or `undefined` when the part is not a function call. The part's\n * `thoughtSignature` (opaque, set by thinking models) is carried on\n * `providerMetadata` so `toGoogleContents` can replay it on the\n * assistant turn — Gemini rejects the next request without it.\n */\n private partToToolCall(part: Part): ModelToolCallRequest | undefined {\n if (!part.functionCall) {\n return undefined;\n }\n\n const call = part.functionCall;\n\n return {\n // The Gemini Developer API does not assign function-call ids\n // (only Vertex parallel-calling does). Fall back to the function\n // name so the neutral `toolCallId` is non-empty and the echoed\n // `functionResponse.name` resolves — Gemini matches a result to\n // its call by name. See decisions §49.\n id: call.id ?? call.name ?? \"\",\n name: call.name ?? \"\",\n input: (call.args ?? {}) as Record<string, unknown>,\n ...(part.thoughtSignature\n ? { providerMetadata: { thoughtSignature: part.thoughtSignature } }\n : {}),\n };\n }\n\n /**\n * Normalize Gemini's `usageMetadata` into the neutral `Usage` shape.\n * Cache-read tokens are surfaced as `cachedTokens` only when\n * non-zero. Absent usage collapses to zeros.\n */\n private extractUsage(response: GenerateContentResponse): Usage {\n const usage: Usage = { input: 0, output: 0, total: 0 };\n\n if (response.usageMetadata) {\n this.applyUsage(usage, response.usageMetadata);\n }\n\n return usage;\n }\n\n /**\n * Fold a Gemini `usageMetadata` block into the running neutral\n * `Usage` accumulator. Shared by `complete()` and the streaming\n * loop (where the final chunk carries cumulative totals).\n *\n * Cache-read hits (`cachedContentTokenCount`, implicit or explicit\n * context caching) surface as `cachedTokens`; the thinking-phase\n * tokens of a reasoning model (`thoughtsTokenCount`) surface as\n * `reasoningTokens`. Both are emitted only when reported `> 0` so an\n * absent channel leaves the field undefined.\n */\n private applyUsage(\n usage: Usage,\n raw: NonNullable<GenerateContentResponse[\"usageMetadata\"]>,\n ): void {\n usage.input = raw.promptTokenCount ?? usage.input;\n usage.output = raw.candidatesTokenCount ?? usage.output;\n usage.total = raw.totalTokenCount ?? usage.input + usage.output;\n\n const cached = raw.cachedContentTokenCount;\n\n if (cached && cached > 0) {\n usage.cachedTokens = cached;\n }\n\n const reasoning = raw.thoughtsTokenCount;\n\n if (reasoning && reasoning > 0) {\n usage.reasoningTokens = reasoning;\n }\n }\n\n /**\n * Wrap a thrown provider error into the typed `AIError` hierarchy\n * and emit the standard error log line before it propagates.\n */\n private logAndWrap(thrown: unknown) {\n const wrapped = wrapGoogleError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n return wrapped;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"model.mjs","names":[],"sources":["../../../../../../ai-google/src/model.ts"],"sourcesContent":["import {\n type Message,\n type ModelCallOptions,\n type ModelCapabilities,\n type ModelContract,\n type ModelPricing,\n type ModelResponse,\n type ModelStreamChunk,\n type ModelToolCallRequest,\n type ReasoningEffort,\n type Usage,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type {\n GenerateContentConfig,\n GenerateContentResponse,\n GoogleGenAI,\n Part,\n} from \"@google/genai\";\nimport type { GoogleModelConfig } from \"./config.type\";\nimport { inferVisionCapability } from \"./known-vision-models\";\nimport {\n applyGoogleUsage,\n mapFinishReason,\n toGoogleContents,\n toGoogleTools,\n wrapGoogleError,\n} from \"./utils\";\n\nconst LOG_MODULE = \"ai.google\";\n\n/**\n * Bucketed `thinkingBudget` (token caps) for the neutral\n * `reasoning.effort` levels when the caller gives no explicit\n * `reasoning.maxTokens`. Gemini 2.5 accepts a positive budget as a cap\n * on the thinking phase; these mirror the spread the OpenAI\n * `reasoning_effort` low/medium/high tiers imply.\n */\nconst EFFORT_THINKING_BUDGET: Record<Exclude<ReasoningEffort, \"none\">, number> = {\n low: 1024,\n medium: 8192,\n high: 24576,\n};\n\n/**\n * Google Gemini-backed implementation of `ModelContract`.\n *\n * **Role.** The provider-facing bridge between the vendor-neutral\n * `@warlock.js/ai` agent runtime and the `@google/genai` SDK\n * (`models.generateContent` / `generateContentStream`).\n *\n * **Responsibility.**\n * - Owns: a long-lived `GoogleGenAI` client + frozen `ModelConfig`\n * (name, temperature, maxTokens) used as per-call defaults.\n * - Owns: translating vendor-neutral `Message[]` / `ToolConfig[]` into\n * Gemini shapes (systemInstruction hoisting, `model` role,\n * `functionCall` / `functionResponse` parts, inline image bytes) on\n * the way out, and Gemini's candidate/parts response (text, function\n * calls, finish reason, token usage) back into neutral shapes on the\n * way in.\n * - Does NOT own: dispatching tools, looping, history, retries — those\n * are agent concerns. The model is a per-call protocol adapter.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across calls\"): the `GoogleGenAI` client is reused for the SDK's\n * lifetime.\n *\n * @example\n * import { GoogleGenAI } from \"@google/genai\";\n * const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });\n * const model = new GoogleModel(ai, { name: \"gemini-2.5-flash\" });\n *\n * const myAgent = agent({ model, tools: [searchTool] });\n * const result = await myAgent.execute(\"Summarize today's news.\");\n */\nexport class GoogleModel implements ModelContract {\n public readonly name: string;\n public readonly provider: string;\n public readonly capabilities: ModelCapabilities;\n public readonly pricing?: ModelPricing;\n\n private readonly ai: GoogleGenAI;\n private readonly config: GoogleModelConfig;\n private readonly logger: Logger = log;\n\n public constructor(ai: GoogleGenAI, config: GoogleModelConfig, provider: string = \"google\") {\n this.ai = ai;\n this.config = config;\n this.name = config.name;\n this.provider = provider;\n this.pricing = config.pricing;\n const multimodal = config.vision ?? inferVisionCapability(config.name);\n\n this.capabilities = {\n structuredOutput: config.structuredOutput ?? true,\n vision: multimodal,\n // Every Gemini 2.5 model thinks; older families harmlessly ignore\n // an empty thinking budget. Defaulting `true` lets the agent\n // forward reasoning options; an explicit `false` opts a model out.\n reasoning: config.reasoning ?? true,\n // Gemini reports cache-read hits (`cachedContentTokenCount`) on\n // every call via implicit caching, and accepts explicit context\n // caching. Read-side accounting is always honored.\n promptCaching: true,\n // The multimodal Gemini families that accept images also accept\n // audio and PDF/document parts. Mirror the vision inference unless\n // explicitly overridden.\n audio: config.audio ?? multimodal,\n pdf: config.pdf ?? multimodal,\n };\n }\n\n /**\n * Single-shot completion. Sends the full message list to\n * `generateContent`, waits for the terminal response, and reshapes\n * it into a vendor-neutral `ModelResponse`. Per-call `options`\n * override the instance defaults for this call only.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting generateContent call\", {\n model: this.name,\n messageCount: messages.length,\n streaming: false,\n toolCount: options?.tools?.length ?? 0,\n });\n\n const { systemInstruction, contents } = toGoogleContents(messages);\n\n let response: GenerateContentResponse;\n\n try {\n response = await this.ai.models.generateContent({\n model: this.name,\n contents,\n config: this.buildConfig(systemInstruction, options),\n });\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n const toolCalls = this.extractToolCalls(response);\n const finishReason = toolCalls\n ? \"tool_calls\"\n : mapFinishReason(response.candidates?.[0]?.finishReason);\n const usage = this.extractUsage(response);\n\n this.logger.debug(LOG_MODULE, \"response\", \"generateContent call succeeded\", {\n finishReason,\n usage,\n });\n\n return {\n content: response.text ?? \"\",\n finishReason,\n usage,\n toolCalls,\n };\n }\n\n /**\n * Incremental streaming completion via `generateContentStream`.\n * Yields neutral `ModelStreamChunk`s — `delta` for text, `tool-call`\n * per function call (Gemini emits a fully-formed call, not partial\n * JSON), and a terminal `done` with the final finish reason + usage.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n this.logger.debug(LOG_MODULE, \"request\", \"Starting generateContentStream call\", {\n model: this.name,\n messageCount: messages.length,\n streaming: true,\n toolCount: options?.tools?.length ?? 0,\n });\n\n const { systemInstruction, contents } = toGoogleContents(messages);\n\n let iterable: AsyncGenerator<GenerateContentResponse>;\n\n try {\n iterable = await this.ai.models.generateContentStream({\n model: this.name,\n contents,\n config: this.buildConfig(systemInstruction, options),\n });\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n let rawFinishReason: string | undefined;\n let sawToolCall = false;\n const usage: Usage = { input: 0, output: 0, total: 0 };\n\n try {\n for await (const chunk of iterable) {\n const text = chunk.text;\n\n if (text) {\n yield { type: \"delta\", content: text };\n }\n\n for (const part of chunk.candidates?.[0]?.content?.parts ?? []) {\n const toolCall = this.partToToolCall(part);\n\n if (!toolCall) {\n continue;\n }\n\n sawToolCall = true;\n\n yield {\n type: \"tool-call\",\n id: toolCall.id,\n name: toolCall.name,\n input: toolCall.input,\n ...(toolCall.providerMetadata\n ? { providerMetadata: toolCall.providerMetadata }\n : {}),\n };\n }\n\n const candidateFinish = chunk.candidates?.[0]?.finishReason;\n\n if (candidateFinish) {\n rawFinishReason = candidateFinish;\n }\n\n if (chunk.usageMetadata) {\n applyGoogleUsage(usage, chunk.usageMetadata);\n }\n }\n } catch (thrown) {\n throw this.logAndWrap(thrown);\n }\n\n const finishReason = sawToolCall ? \"tool_calls\" : mapFinishReason(rawFinishReason);\n\n this.logger.debug(LOG_MODULE, \"response\", \"generateContentStream call succeeded\", {\n finishReason,\n usage,\n });\n\n yield { type: \"done\", finishReason, usage };\n }\n\n /**\n * Assemble the `GenerateContentConfig` shared by `complete()` and\n * `stream()`: inference params, hoisted system instruction,\n * cancellation signal, and conditional tools + native structured\n * output.\n */\n private buildConfig(\n systemInstruction: string | undefined,\n options: ModelCallOptions | undefined,\n ): GenerateContentConfig {\n const temperature = options?.temperature ?? this.config.temperature;\n const maxOutputTokens = options?.maxTokens ?? this.config.maxTokens;\n\n return {\n ...(systemInstruction ? { systemInstruction } : {}),\n ...(temperature !== undefined ? { temperature } : {}),\n ...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),\n ...(options?.signal ? { abortSignal: options.signal } : {}),\n ...this.buildTools(options?.tools),\n ...this.buildStructuredOutput(options?.responseSchema),\n ...this.buildThinking(options?.reasoning),\n };\n }\n\n /**\n * Translate the neutral `reasoning` option into Gemini's\n * `thinkingConfig`. `reasoning.maxTokens` maps directly to\n * `thinkingBudget` (token cap on the thinking phase); when only\n * `reasoning.effort` is given it is bucketed into a budget. Emitted\n * only when the model is `reasoning`-capable — a `false` capability\n * (config override) drops it so a non-thinking model never receives\n * an unsupported `thinkingConfig`.\n *\n * Gemini's `thinkingBudget` semantics: `0` disables thinking, `-1`\n * lets the model decide automatically. A positive value caps the\n * thinking tokens. The neutral `effort: \"none\"` (\"run without\n * reasoning\") maps to `thinkingBudget: 0`.\n */\n private buildThinking(\n reasoning: ModelCallOptions[\"reasoning\"],\n ): Pick<GenerateContentConfig, \"thinkingConfig\"> {\n if (!reasoning || !this.capabilities.reasoning) {\n return {};\n }\n\n // `effort: \"none\"` = explicit \"run without reasoning\". Gemini disables\n // thinking with `thinkingBudget: 0` (its native off switch), so emit\n // that rather than omitting the config — an omitted config lets a\n // thinking model reason at its default budget.\n if (reasoning.effort === \"none\") {\n return { thinkingConfig: { thinkingBudget: 0 } };\n }\n\n const thinkingBudget =\n reasoning.maxTokens ?? (reasoning.effort ? EFFORT_THINKING_BUDGET[reasoning.effort] : undefined);\n\n if (thinkingBudget === undefined) {\n return {};\n }\n\n return { thinkingConfig: { thinkingBudget } };\n }\n\n /**\n * Spread-friendly tools fragment. Empty object when no tools were\n * supplied so the caller can unconditionally spread it.\n */\n private buildTools(tools: ModelCallOptions[\"tools\"]): Pick<GenerateContentConfig, \"tools\"> {\n const mapped = toGoogleTools(tools);\n\n return mapped ? { tools: mapped } : {};\n }\n\n /**\n * Translate the neutral `responseSchema` into Gemini's native JSON\n * structured output (`responseMimeType: \"application/json\"` +\n * `responseJsonSchema`, which takes a raw JSON Schema directly).\n * Emitted only when the model is `structuredOutput`-capable and the\n * schema is an object root — otherwise the agent's soft prompt hint\n * + client-side `validate()` carry shape.\n */\n private buildStructuredOutput(\n responseSchema: Record<string, unknown> | undefined,\n ): Pick<GenerateContentConfig, \"responseMimeType\" | \"responseJsonSchema\"> {\n if (!responseSchema || !this.capabilities.structuredOutput) {\n return {};\n }\n\n if (responseSchema.type !== \"object\" || typeof responseSchema.properties !== \"object\") {\n return {};\n }\n\n return {\n responseMimeType: \"application/json\",\n responseJsonSchema: responseSchema,\n };\n }\n\n /**\n * Reshape Gemini's function-call content parts into the neutral\n * `ModelToolCallRequest[]`. Returns `undefined` when the model\n * requested no functions so callers can branch on presence.\n *\n * Reads `candidates[0].content.parts` directly rather than the\n * `response.functionCalls` getter: the getter discards the\n * part-level `thoughtSignature`, and Gemini \"thinking\" models 400\n * the follow-up turn if that signature is not echoed back. See\n * `partToToolCall`.\n */\n private extractToolCalls(\n response: GenerateContentResponse,\n ): ModelToolCallRequest[] | undefined {\n const parts = response.candidates?.[0]?.content?.parts ?? [];\n const toolCalls = parts\n .map((part) => this.partToToolCall(part))\n .filter((call): call is ModelToolCallRequest => call !== undefined);\n\n return toolCalls.length > 0 ? toolCalls : undefined;\n }\n\n /**\n * Map a single Gemini `Part` to a neutral `ModelToolCallRequest`,\n * or `undefined` when the part is not a function call. The part's\n * `thoughtSignature` (opaque, set by thinking models) is carried on\n * `providerMetadata` so `toGoogleContents` can replay it on the\n * assistant turn — Gemini rejects the next request without it.\n */\n private partToToolCall(part: Part): ModelToolCallRequest | undefined {\n if (!part.functionCall) {\n return undefined;\n }\n\n const call = part.functionCall;\n\n return {\n // The Gemini Developer API does not assign function-call ids\n // (only Vertex parallel-calling does). Fall back to the function\n // name so the neutral `toolCallId` is non-empty and the echoed\n // `functionResponse.name` resolves — Gemini matches a result to\n // its call by name. See decisions §49.\n id: call.id ?? call.name ?? \"\",\n name: call.name ?? \"\",\n input: (call.args ?? {}) as Record<string, unknown>,\n ...(part.thoughtSignature\n ? { providerMetadata: { thoughtSignature: part.thoughtSignature } }\n : {}),\n };\n }\n\n /**\n * Normalize Gemini's `usageMetadata` into the neutral `Usage` shape\n * via the shared {@link applyGoogleUsage} mapper (the same one the\n * streaming loop and the Gemini image model use). Absent usage\n * collapses to zeros.\n */\n private extractUsage(response: GenerateContentResponse): Usage {\n const usage: Usage = { input: 0, output: 0, total: 0 };\n\n if (response.usageMetadata) {\n applyGoogleUsage(usage, response.usageMetadata);\n }\n\n return usage;\n }\n\n /**\n * Wrap a thrown provider error into the typed `AIError` hierarchy\n * and emit the standard error log line before it propagates.\n */\n private logAndWrap(thrown: unknown) {\n const wrapped = wrapGoogleError(thrown);\n\n this.logger.error(LOG_MODULE, \"error\", wrapped.message, {\n code: wrapped.code,\n context: wrapped.context,\n });\n\n return wrapped;\n }\n}\n"],"mappings":"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"}
package/esm/sdk.d.mts CHANGED
@@ -57,19 +57,27 @@ declare class GoogleSDK implements SDKAdapterContract {
57
57
  */
58
58
  embedder(config: GoogleEmbedderConfig): EmbedderContract;
59
59
  /**
60
- * Build a `GoogleImageModel` (Imagen) bound to this SDK's client for
61
- * use with `ai.image({ model, prompt })`. `config.name` is passed
62
- * through to `ai.models.generateImages` as given no id is rejected
63
- * locally, so an unsupported model fails at Google, not here.
60
+ * Build an image model bound to this SDK's client for use with
61
+ * `ai.image({ model, prompt })`. `config.name` decides the transport
62
+ * (see {@link usesGeminiImageTransport}) a `gemini-` id gets the
63
+ * `generateContent` implementation, everything else the Imagen
64
+ * `generateImages` one. No id is rejected locally either way, so an
65
+ * unsupported model fails at Google, not here.
66
+ *
67
+ * The two differ in what usage they can report, which is what the
68
+ * caller must price for: the Imagen path always returns a zero token
69
+ * `Usage` (Imagen reports none — price with `{ perImage }`), while the
70
+ * Gemini path passes through whatever `usageMetadata` Google attaches
71
+ * (price with `{ input, output }` when tokens come back).
64
72
  *
65
73
  * Pricing resolution mirrors `model()`: per-model `config.pricing`
66
74
  * wins, otherwise the SDK-level registry entry keyed by `config.name`,
67
- * otherwise `undefined`. Imagen is per-image-metered, so the registry
68
- * entry typically carries `{ perImage }`.
75
+ * otherwise `undefined`.
69
76
  *
70
77
  * @example
71
- * const model = google.image({ name: "imagen-4.0-generate-001" });
72
- * const { data } = await ai.image({ model, prompt: "a watercolor lighthouse" });
78
+ * const imagen = google.image({ name: "imagen-4.0-generate-001" });
79
+ * const gemini = google.image({ name: "gemini-3.1-flash-lite-image" });
80
+ * const { data } = await ai.image({ model: gemini, prompt: "a red bicycle" });
73
81
  */
74
82
  image(config: GoogleImageConfig): ImageModelContract;
75
83
  }
package/esm/sdk.d.mts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.d.mts","names":[],"sources":["../../../../../../ai-google/src/sdk.ts"],"mappings":";;;;;;AA2CA;;;;;;;;;;;;;;;;;;;;;;cAAa,SAAA,YAAqB,kBAAA;EAAA,iBACf,EAAA;EAAA,iBACA,QAAA;EAAA,iBACA,OAAA;cAEE,MAAA,EAAQ,eAAA;EA+BM;;;;;;;;;EAd1B,KAAA,CAAM,MAAA,EAAQ,iBAAA,GAAoB,aAAA;EA4CkB;AAAA;;;;;EA9B9C,KAAA,CAAM,IAAA,UAAc,MAAA,YAAkB,OAAA;;;;;;;;EAW5C,QAAA,CAAS,MAAA,EAAQ,oBAAA,GAAuB,gBAAA;;;;;;;;;;;;;;;;EAmBxC,KAAA,CAAM,MAAA,EAAQ,iBAAA,GAAoB,kBAAA;AAAA"}
1
+ {"version":3,"file":"sdk.d.mts","names":[],"sources":["../../../../../../ai-google/src/sdk.ts"],"mappings":";;;;;;AA2EA;;;;;;;;;;;;;;;;;;;;;;cAAa,SAAA,YAAqB,kBAAA;EAAA,iBACf,EAAA;EAAA,iBACA,QAAA;EAAA,iBACA,OAAA;cAEE,MAAA,EAAQ,eAAA;EA+BM;;;;;;;;;EAd1B,KAAA,CAAM,MAAA,EAAQ,iBAAA,GAAoB,aAAA;EAoDkB;AAAA;;;;;EAtC9C,KAAA,CAAM,IAAA,UAAc,MAAA,YAAkB,OAAA;;;;;;;;EAW5C,QAAA,CAAS,MAAA,EAAQ,oBAAA,GAAuB,gBAAA;;;;;;;;;;;;;;;;;;;;;;;;EA2BxC,KAAA,CAAM,MAAA,EAAQ,iBAAA,GAAoB,kBAAA;AAAA"}
package/esm/sdk.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import { GoogleEmbedder } from "./embedder.mjs";
2
+ import { GeminiImageModel } from "./gemini-image.mjs";
2
3
  import { GoogleImageModel } from "./image.mjs";
3
4
  import { GoogleModel } from "./model.mjs";
4
5
  import { GoogleGenAI } from "@google/genai";
@@ -6,6 +7,36 @@ import { approximateTokenCount } from "@warlock.js/ai";
6
7
 
7
8
  //#region ../ai-google/src/sdk.ts
8
9
  /**
10
+ * Pick the transport for an image model id.
11
+ *
12
+ * `ai.models.generateImages` calls `{model}:predict`, and a `gemini-`
13
+ * id sent there comes back `404 … is not supported for predict`
14
+ * (observed verbatim from Google). `generateContent` is what the SDK
15
+ * itself points `generateImages` users at — its deprecation notice
16
+ * reads "Please use the generateContent method with image models
17
+ * instead" — so the id has to choose the transport.
18
+ *
19
+ * Runs in this package establish where a `gemini-` id is ACCEPTED, not
20
+ * what it returns: on this transport such an id got as far as a quota
21
+ * error (HTTP 429) instead of the 404. That an image
22
+ * comes back end-to-end once billing is enabled is reported by the
23
+ * maintainer from a locally linked build, not measured here. Whether
24
+ * these models report token usage is still unknown.
25
+ *
26
+ * This is ROUTING, not validation — no id is refused here. An id this
27
+ * function does not recognize takes the `generateImages` route, the
28
+ * only route that existed before Gemini image support landed, so every
29
+ * id that reached Google before still reaches Google the same way and
30
+ * still fails (or succeeds) at the provider.
31
+ *
32
+ * A leading `models/` resource prefix is tolerated, matching the id
33
+ * shapes `inferVisionCapability` already accepts
34
+ * (`models/gemini-1.5-flash-001`).
35
+ */
36
+ function usesGeminiImageTransport(name) {
37
+ return name.toLowerCase().replace(/^models\//, "").startsWith("gemini-");
38
+ }
39
+ /**
9
40
  * Google Gemini-backed implementation of `SDKAdapterContract`.
10
41
  *
11
42
  * **Role.** The package entry point for Gemini models via the
@@ -73,19 +104,27 @@ var GoogleSDK = class {
73
104
  return new GoogleEmbedder(this.ai, config, this.provider);
74
105
  }
75
106
  /**
76
- * Build a `GoogleImageModel` (Imagen) bound to this SDK's client for
77
- * use with `ai.image({ model, prompt })`. `config.name` is passed
78
- * through to `ai.models.generateImages` as given no id is rejected
79
- * locally, so an unsupported model fails at Google, not here.
107
+ * Build an image model bound to this SDK's client for use with
108
+ * `ai.image({ model, prompt })`. `config.name` decides the transport
109
+ * (see {@link usesGeminiImageTransport}) a `gemini-` id gets the
110
+ * `generateContent` implementation, everything else the Imagen
111
+ * `generateImages` one. No id is rejected locally either way, so an
112
+ * unsupported model fails at Google, not here.
113
+ *
114
+ * The two differ in what usage they can report, which is what the
115
+ * caller must price for: the Imagen path always returns a zero token
116
+ * `Usage` (Imagen reports none — price with `{ perImage }`), while the
117
+ * Gemini path passes through whatever `usageMetadata` Google attaches
118
+ * (price with `{ input, output }` when tokens come back).
80
119
  *
81
120
  * Pricing resolution mirrors `model()`: per-model `config.pricing`
82
121
  * wins, otherwise the SDK-level registry entry keyed by `config.name`,
83
- * otherwise `undefined`. Imagen is per-image-metered, so the registry
84
- * entry typically carries `{ perImage }`.
122
+ * otherwise `undefined`.
85
123
  *
86
124
  * @example
87
- * const model = google.image({ name: "imagen-4.0-generate-001" });
88
- * const { data } = await ai.image({ model, prompt: "a watercolor lighthouse" });
125
+ * const imagen = google.image({ name: "imagen-4.0-generate-001" });
126
+ * const gemini = google.image({ name: "gemini-3.1-flash-lite-image" });
127
+ * const { data } = await ai.image({ model: gemini, prompt: "a red bicycle" });
89
128
  */
90
129
  image(config) {
91
130
  const resolvedPricing = config.pricing ?? this.pricing?.[config.name];
@@ -93,6 +132,7 @@ var GoogleSDK = class {
93
132
  ...config,
94
133
  pricing: resolvedPricing
95
134
  };
135
+ if (usesGeminiImageTransport(config.name)) return new GeminiImageModel(this.ai, resolvedConfig, this.provider);
96
136
  return new GoogleImageModel(this.ai, resolvedConfig, this.provider);
97
137
  }
98
138
  };
package/esm/sdk.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"sdk.mjs","names":[],"sources":["../../../../../../ai-google/src/sdk.ts"],"sourcesContent":["import { GoogleGenAI } from \"@google/genai\";\nimport type {\n EmbedderContract,\n ImageModelContract,\n ModelContract,\n ModelPricing,\n SDKAdapterContract,\n} from \"@warlock.js/ai\";\nimport { approximateTokenCount } from \"@warlock.js/ai\";\nimport type {\n GoogleEmbedderConfig,\n GoogleImageConfig,\n GoogleModelConfig,\n GoogleSDKConfig,\n} from \"./config.type\";\nimport { GoogleEmbedder } from \"./embedder\";\nimport { GoogleImageModel } from \"./image\";\nimport { GoogleModel } from \"./model\";\n\n/**\n * Google Gemini-backed implementation of `SDKAdapterContract`.\n *\n * **Role.** The package entry point for Gemini models via the\n * `@google/genai` SDK. A single `GoogleSDK` holds one live\n * `GoogleGenAI` client, shared by every `ModelContract` /\n * `EmbedderContract` it produces. Construct one SDK per\n * account/project and reuse it everywhere.\n *\n * **Responsibility.**\n * - Owns: a long-lived `GoogleGenAI` client (auth, Vertex vs Gemini\n * API) and its lifetime. Factory for `GoogleModel` /\n * `GoogleEmbedder` instances sharing that client.\n * - Does NOT own: anything per-call — those live in `GoogleModel` /\n * `GoogleEmbedder` and the agent runtime.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across many calls\"), fronted by FP usage like the other adapters.\n *\n * @example\n * const google = new GoogleSDK({ apiKey: process.env.GEMINI_API_KEY! });\n * const model = google.model({ name: \"gemini-2.5-flash\", temperature: 0.7 });\n * const embedder = google.embedder({ name: \"gemini-embedding-001\" });\n */\nexport class GoogleSDK implements SDKAdapterContract {\n private readonly ai: GoogleGenAI;\n private readonly provider: string;\n private readonly pricing?: Record<string, ModelPricing>;\n\n public constructor(config: GoogleSDKConfig) {\n const { provider, pricing, ...clientOptions } = config;\n\n this.ai = new GoogleGenAI(clientOptions);\n this.provider = provider ?? \"google\";\n this.pricing = pricing;\n }\n\n /**\n * Build a `GoogleModel` bound to this SDK's client. Each call\n * returns a fresh instance; all instances share the underlying\n * `GoogleGenAI` client. The SDK's `provider` label is forwarded.\n *\n * Pricing resolution: per-model `config.pricing` wins; otherwise the\n * SDK-level registry entry keyed by `config.name`; otherwise\n * `undefined` (no cost computed).\n */\n public model(config: GoogleModelConfig): ModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: GoogleModelConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n return new GoogleModel(this.ai, resolvedConfig, this.provider);\n }\n\n /**\n * Rough token-count estimate. Uses the character-heuristic\n * (`approximateTokenCount`) from the core package — Gemini's\n * `countTokens` is a network round-trip; `count()` is intentionally\n * offline. Good for budgeting/quota guards, not billing.\n */\n public async count(text: string, _model?: string): Promise<number> {\n return approximateTokenCount(text);\n }\n\n /**\n * Build a `GoogleEmbedder` bound to this SDK's client.\n *\n * @example\n * const embedder = google.embedder({ name: \"gemini-embedding-001\" });\n * const { vector } = await embedder.embed(\"Hello world\");\n */\n public embedder(config: GoogleEmbedderConfig): EmbedderContract {\n return new GoogleEmbedder(this.ai, config, this.provider);\n }\n\n /**\n * Build a `GoogleImageModel` (Imagen) bound to this SDK's client for\n * use with `ai.image({ model, prompt })`. `config.name` is passed\n * through to `ai.models.generateImages` as given no id is rejected\n * locally, so an unsupported model fails at Google, not here.\n *\n * Pricing resolution mirrors `model()`: per-model `config.pricing`\n * wins, otherwise the SDK-level registry entry keyed by `config.name`,\n * otherwise `undefined`. Imagen is per-image-metered, so the registry\n * entry typically carries `{ perImage }`.\n *\n * @example\n * const model = google.image({ name: \"imagen-4.0-generate-001\" });\n * const { data } = await ai.image({ model, prompt: \"a watercolor lighthouse\" });\n */\n public image(config: GoogleImageConfig): ImageModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: GoogleImageConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n return new GoogleImageModel(this.ai, resolvedConfig, this.provider);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,IAAa,YAAb,MAAqD;CAKnD,AAAO,YAAY,QAAyB;EAC1C,MAAM,EAAE,UAAU,SAAS,GAAG,kBAAkB;EAEhD,KAAK,KAAK,IAAI,YAAY,aAAa;EACvC,KAAK,WAAW,YAAY;EAC5B,KAAK,UAAU;CACjB;;;;;;;;;;CAWA,AAAO,MAAM,QAA0C;EACrD,MAAM,kBAAkB,OAAO,WAAW,KAAK,UAAU,OAAO;EAChE,MAAM,iBACJ,oBAAoB,OAAO,UAAU,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAgB;EAEtF,OAAO,IAAI,YAAY,KAAK,IAAI,gBAAgB,KAAK,QAAQ;CAC/D;;;;;;;CAQA,MAAa,MAAM,MAAc,QAAkC;EACjE,OAAO,sBAAsB,IAAI;CACnC;;;;;;;;CASA,AAAO,SAAS,QAAgD;EAC9D,OAAO,IAAI,eAAe,KAAK,IAAI,QAAQ,KAAK,QAAQ;CAC1D;;;;;;;;;;;;;;;;CAiBA,AAAO,MAAM,QAA+C;EAC1D,MAAM,kBAAkB,OAAO,WAAW,KAAK,UAAU,OAAO;EAChE,MAAM,iBACJ,oBAAoB,OAAO,UAAU,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAgB;EAEtF,OAAO,IAAI,iBAAiB,KAAK,IAAI,gBAAgB,KAAK,QAAQ;CACpE;AACF"}
1
+ {"version":3,"file":"sdk.mjs","names":[],"sources":["../../../../../../ai-google/src/sdk.ts"],"sourcesContent":["import { GoogleGenAI } from \"@google/genai\";\nimport type {\n EmbedderContract,\n ImageModelContract,\n ModelContract,\n ModelPricing,\n SDKAdapterContract,\n} from \"@warlock.js/ai\";\nimport { approximateTokenCount } from \"@warlock.js/ai\";\nimport type {\n GoogleEmbedderConfig,\n GoogleImageConfig,\n GoogleModelConfig,\n GoogleSDKConfig,\n} from \"./config.type\";\nimport { GoogleEmbedder } from \"./embedder\";\nimport { GeminiImageModel } from \"./gemini-image\";\nimport { GoogleImageModel } from \"./image\";\nimport { GoogleModel } from \"./model\";\n\n/**\n * Pick the transport for an image model id.\n *\n * `ai.models.generateImages` calls `{model}:predict`, and a `gemini-`\n * id sent there comes back `404 … is not supported for predict`\n * (observed verbatim from Google). `generateContent` is what the SDK\n * itself points `generateImages` users at — its deprecation notice\n * reads \"Please use the generateContent method with image models\n * instead\" — so the id has to choose the transport.\n *\n * Runs in this package establish where a `gemini-` id is ACCEPTED, not\n * what it returns: on this transport such an id got as far as a quota\n * error (HTTP 429) instead of the 404. That an image\n * comes back end-to-end once billing is enabled is reported by the\n * maintainer from a locally linked build, not measured here. Whether\n * these models report token usage is still unknown.\n *\n * This is ROUTING, not validation — no id is refused here. An id this\n * function does not recognize takes the `generateImages` route, the\n * only route that existed before Gemini image support landed, so every\n * id that reached Google before still reaches Google the same way and\n * still fails (or succeeds) at the provider.\n *\n * A leading `models/` resource prefix is tolerated, matching the id\n * shapes `inferVisionCapability` already accepts\n * (`models/gemini-1.5-flash-001`).\n */\nfunction usesGeminiImageTransport(name: string): boolean {\n return name.toLowerCase().replace(/^models\\//, \"\").startsWith(\"gemini-\");\n}\n\n/**\n * Google Gemini-backed implementation of `SDKAdapterContract`.\n *\n * **Role.** The package entry point for Gemini models via the\n * `@google/genai` SDK. A single `GoogleSDK` holds one live\n * `GoogleGenAI` client, shared by every `ModelContract` /\n * `EmbedderContract` it produces. Construct one SDK per\n * account/project and reuse it everywhere.\n *\n * **Responsibility.**\n * - Owns: a long-lived `GoogleGenAI` client (auth, Vertex vs Gemini\n * API) and its lifetime. Factory for `GoogleModel` /\n * `GoogleEmbedder` instances sharing that client.\n * - Does NOT own: anything per-call — those live in `GoogleModel` /\n * `GoogleEmbedder` and the agent runtime.\n *\n * Modeled as a class (see §4.2 of code-style.md — \"long-lived state\n * across many calls\"), fronted by FP usage like the other adapters.\n *\n * @example\n * const google = new GoogleSDK({ apiKey: process.env.GEMINI_API_KEY! });\n * const model = google.model({ name: \"gemini-2.5-flash\", temperature: 0.7 });\n * const embedder = google.embedder({ name: \"gemini-embedding-001\" });\n */\nexport class GoogleSDK implements SDKAdapterContract {\n private readonly ai: GoogleGenAI;\n private readonly provider: string;\n private readonly pricing?: Record<string, ModelPricing>;\n\n public constructor(config: GoogleSDKConfig) {\n const { provider, pricing, ...clientOptions } = config;\n\n this.ai = new GoogleGenAI(clientOptions);\n this.provider = provider ?? \"google\";\n this.pricing = pricing;\n }\n\n /**\n * Build a `GoogleModel` bound to this SDK's client. Each call\n * returns a fresh instance; all instances share the underlying\n * `GoogleGenAI` client. The SDK's `provider` label is forwarded.\n *\n * Pricing resolution: per-model `config.pricing` wins; otherwise the\n * SDK-level registry entry keyed by `config.name`; otherwise\n * `undefined` (no cost computed).\n */\n public model(config: GoogleModelConfig): ModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: GoogleModelConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n return new GoogleModel(this.ai, resolvedConfig, this.provider);\n }\n\n /**\n * Rough token-count estimate. Uses the character-heuristic\n * (`approximateTokenCount`) from the core package — Gemini's\n * `countTokens` is a network round-trip; `count()` is intentionally\n * offline. Good for budgeting/quota guards, not billing.\n */\n public async count(text: string, _model?: string): Promise<number> {\n return approximateTokenCount(text);\n }\n\n /**\n * Build a `GoogleEmbedder` bound to this SDK's client.\n *\n * @example\n * const embedder = google.embedder({ name: \"gemini-embedding-001\" });\n * const { vector } = await embedder.embed(\"Hello world\");\n */\n public embedder(config: GoogleEmbedderConfig): EmbedderContract {\n return new GoogleEmbedder(this.ai, config, this.provider);\n }\n\n /**\n * Build an image model bound to this SDK's client for use with\n * `ai.image({ model, prompt })`. `config.name` decides the transport\n * (see {@link usesGeminiImageTransport}) — a `gemini-` id gets the\n * `generateContent` implementation, everything else the Imagen\n * `generateImages` one. No id is rejected locally either way, so an\n * unsupported model fails at Google, not here.\n *\n * The two differ in what usage they can report, which is what the\n * caller must price for: the Imagen path always returns a zero token\n * `Usage` (Imagen reports none — price with `{ perImage }`), while the\n * Gemini path passes through whatever `usageMetadata` Google attaches\n * (price with `{ input, output }` when tokens come back).\n *\n * Pricing resolution mirrors `model()`: per-model `config.pricing`\n * wins, otherwise the SDK-level registry entry keyed by `config.name`,\n * otherwise `undefined`.\n *\n * @example\n * const imagen = google.image({ name: \"imagen-4.0-generate-001\" });\n * const gemini = google.image({ name: \"gemini-3.1-flash-lite-image\" });\n * const { data } = await ai.image({ model: gemini, prompt: \"a red bicycle\" });\n */\n public image(config: GoogleImageConfig): ImageModelContract {\n const resolvedPricing = config.pricing ?? this.pricing?.[config.name];\n const resolvedConfig: GoogleImageConfig =\n resolvedPricing === config.pricing ? config : { ...config, pricing: resolvedPricing };\n\n if (usesGeminiImageTransport(config.name)) {\n return new GeminiImageModel(this.ai, resolvedConfig, this.provider);\n }\n\n return new GoogleImageModel(this.ai, resolvedConfig, this.provider);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CA,SAAS,yBAAyB,MAAuB;CACvD,OAAO,KAAK,YAAY,CAAC,CAAC,QAAQ,aAAa,EAAE,CAAC,CAAC,WAAW,SAAS;AACzE;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,IAAa,YAAb,MAAqD;CAKnD,AAAO,YAAY,QAAyB;EAC1C,MAAM,EAAE,UAAU,SAAS,GAAG,kBAAkB;EAEhD,KAAK,KAAK,IAAI,YAAY,aAAa;EACvC,KAAK,WAAW,YAAY;EAC5B,KAAK,UAAU;CACjB;;;;;;;;;;CAWA,AAAO,MAAM,QAA0C;EACrD,MAAM,kBAAkB,OAAO,WAAW,KAAK,UAAU,OAAO;EAChE,MAAM,iBACJ,oBAAoB,OAAO,UAAU,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAgB;EAEtF,OAAO,IAAI,YAAY,KAAK,IAAI,gBAAgB,KAAK,QAAQ;CAC/D;;;;;;;CAQA,MAAa,MAAM,MAAc,QAAkC;EACjE,OAAO,sBAAsB,IAAI;CACnC;;;;;;;;CASA,AAAO,SAAS,QAAgD;EAC9D,OAAO,IAAI,eAAe,KAAK,IAAI,QAAQ,KAAK,QAAQ;CAC1D;;;;;;;;;;;;;;;;;;;;;;;;CAyBA,AAAO,MAAM,QAA+C;EAC1D,MAAM,kBAAkB,OAAO,WAAW,KAAK,UAAU,OAAO;EAChE,MAAM,iBACJ,oBAAoB,OAAO,UAAU,SAAS;GAAE,GAAG;GAAQ,SAAS;EAAgB;EAEtF,IAAI,yBAAyB,OAAO,IAAI,GACtC,OAAO,IAAI,iBAAiB,KAAK,IAAI,gBAAgB,KAAK,QAAQ;EAGpE,OAAO,IAAI,iBAAiB,KAAK,IAAI,gBAAgB,KAAK,QAAQ;CACpE;AACF"}
@@ -0,0 +1,30 @@
1
+ //#region ../ai-google/src/utils/apply-google-usage.ts
2
+ /**
3
+ * Fold a Gemini `usageMetadata` block into a running neutral `Usage`
4
+ * accumulator. Shared by every `generateContent`-backed surface — the
5
+ * chat model's `complete()`, its streaming loop (where the final chunk
6
+ * carries cumulative totals), and the Gemini image model — so one
7
+ * mapping decides what a Gemini token report means package-wide.
8
+ *
9
+ * Cache-read hits (`cachedContentTokenCount`, implicit or explicit
10
+ * context caching) surface as `cachedTokens`; the thinking-phase tokens
11
+ * of a reasoning model (`thoughtsTokenCount`) surface as
12
+ * `reasoningTokens`. Both are emitted only when reported `> 0` so an
13
+ * absent channel leaves the field undefined rather than a false zero.
14
+ *
15
+ * `total` falls back to `input + output` when Google omits
16
+ * `totalTokenCount`.
17
+ */
18
+ function applyGoogleUsage(usage, raw) {
19
+ usage.input = raw.promptTokenCount ?? usage.input;
20
+ usage.output = raw.candidatesTokenCount ?? usage.output;
21
+ usage.total = raw.totalTokenCount ?? usage.input + usage.output;
22
+ const cached = raw.cachedContentTokenCount;
23
+ if (cached && cached > 0) usage.cachedTokens = cached;
24
+ const reasoning = raw.thoughtsTokenCount;
25
+ if (reasoning && reasoning > 0) usage.reasoningTokens = reasoning;
26
+ }
27
+
28
+ //#endregion
29
+ export { applyGoogleUsage };
30
+ //# sourceMappingURL=apply-google-usage.mjs.map