@warlock.js/ai-google 4.13.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.
- package/CHANGELOG.md +35 -0
- package/cjs/index.cjs +307 -65
- package/cjs/index.cjs.map +1 -1
- package/esm/config.type.d.mts +11 -3
- package/esm/config.type.d.mts.map +1 -1
- package/esm/gemini-image.d.mts +91 -0
- package/esm/gemini-image.d.mts.map +1 -0
- package/esm/gemini-image.mjs +231 -0
- package/esm/gemini-image.mjs.map +1 -0
- package/esm/image.d.mts +7 -4
- package/esm/image.d.mts.map +1 -1
- package/esm/image.mjs +8 -7
- package/esm/image.mjs.map +1 -1
- package/esm/index.d.mts +2 -2
- package/esm/index.mjs +2 -2
- package/esm/model.mjs +7 -25
- package/esm/model.mjs.map +1 -1
- package/esm/sdk.d.mts +16 -7
- package/esm/sdk.d.mts.map +1 -1
- package/esm/sdk.mjs +48 -7
- package/esm/sdk.mjs.map +1 -1
- package/esm/utils/apply-google-usage.mjs +30 -0
- package/esm/utils/apply-google-usage.mjs.map +1 -0
- package/esm/utils/index.mjs +1 -0
- package/llms-full.txt +50 -6
- package/llms.txt +1 -1
- package/package.json +6 -6
- package/skills/setup-google/SKILL.md +50 -6
- package/esm/known-image-models.d.mts +0 -30
- package/esm/known-image-models.d.mts.map +0 -1
- package/esm/known-image-models.mjs +0 -33
- package/esm/known-image-models.mjs.map +0 -1
|
@@ -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/image.d.mts
CHANGED
|
@@ -8,10 +8,13 @@ import { ImageGenerationOptions, ImageGenerationResponse, ImageModelContract, Im
|
|
|
8
8
|
* `ai.models.generateImages`. Imagen is per-image-metered and returns
|
|
9
9
|
* base64 image bytes (no hosted URL, no token usage).
|
|
10
10
|
*
|
|
11
|
-
* **
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* **No model-id validation.** `config.name` is passed through to
|
|
12
|
+
* `ai.models.generateImages` exactly as given — the constructor never
|
|
13
|
+
* inspects it. Google adds and retires image model ids on its own
|
|
14
|
+
* schedule, so an id this adapter does not recognize is not the
|
|
15
|
+
* adapter's call to refuse; an unsupported id surfaces as a provider
|
|
16
|
+
* error from Google (wrapped into the typed `AIError` hierarchy by
|
|
17
|
+
* `generate()`), not as a local one.
|
|
15
18
|
*
|
|
16
19
|
* **Safety filtering.** When Imagen filters every candidate for safety
|
|
17
20
|
* (`raiFilteredReason`), this surfaces a typed `ContentFilterError`
|
package/esm/image.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.d.mts","names":[],"sources":["../../../../../../ai-google/src/image.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"image.d.mts","names":[],"sources":["../../../../../../ai-google/src/image.ts"],"mappings":";;;;;;;AAoDA;;;;;;;;;;;;;;;;;;;cAAa,gBAAA,YAA4B,kBAAA;EAAA,SACvB,IAAA;EAAA,SACA,QAAA;EAAA,SACA,OAAA,GAAU,iBAAA;EAAA,iBAET,EAAA;EAAA,iBACA,MAAA;cAEE,EAAA,EAAI,WAAA,EAAa,MAAA,EAAQ,iBAAA,EAAmB,QAAA;EAOlD,QAAA,CACX,MAAA,UACA,OAAA,GAAU,sBAAA,GACT,OAAA,CAAQ,uBAAA;AAAA"}
|
package/esm/image.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { wrapGoogleError } from "./utils/wrap-google-error.mjs";
|
|
2
2
|
import "./utils/index.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { ContentFilterError, InvalidRequestError, ProviderError } from "@warlock.js/ai";
|
|
3
|
+
import { ContentFilterError, ProviderError } from "@warlock.js/ai";
|
|
5
4
|
import { log } from "@warlock.js/logger";
|
|
6
5
|
|
|
7
6
|
//#region ../ai-google/src/image.ts
|
|
@@ -21,10 +20,13 @@ function mediaTypeFor(format) {
|
|
|
21
20
|
* `ai.models.generateImages`. Imagen is per-image-metered and returns
|
|
22
21
|
* base64 image bytes (no hosted URL, no token usage).
|
|
23
22
|
*
|
|
24
|
-
* **
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
23
|
+
* **No model-id validation.** `config.name` is passed through to
|
|
24
|
+
* `ai.models.generateImages` exactly as given — the constructor never
|
|
25
|
+
* inspects it. Google adds and retires image model ids on its own
|
|
26
|
+
* schedule, so an id this adapter does not recognize is not the
|
|
27
|
+
* adapter's call to refuse; an unsupported id surfaces as a provider
|
|
28
|
+
* error from Google (wrapped into the typed `AIError` hierarchy by
|
|
29
|
+
* `generate()`), not as a local one.
|
|
28
30
|
*
|
|
29
31
|
* **Safety filtering.** When Imagen filters every candidate for safety
|
|
30
32
|
* (`raiFilteredReason`), this surfaces a typed `ContentFilterError`
|
|
@@ -37,7 +39,6 @@ function mediaTypeFor(format) {
|
|
|
37
39
|
var GoogleImageModel = class {
|
|
38
40
|
constructor(ai, config, provider = "google") {
|
|
39
41
|
this.logger = log;
|
|
40
|
-
if (!isGoogleImageModel(config.name)) throw new InvalidRequestError(`"${config.name}" is not a known Google Imagen model. Use an \`imagen-*\` model with google.image({ name }).`);
|
|
41
42
|
this.ai = ai;
|
|
42
43
|
this.name = config.name;
|
|
43
44
|
this.provider = provider;
|
package/esm/image.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.mjs","names":[],"sources":["../../../../../../ai-google/src/image.ts"],"sourcesContent":["import {\n ContentFilterError,\n
|
|
1
|
+
{"version":3,"file":"image.mjs","names":[],"sources":["../../../../../../ai-google/src/image.ts"],"sourcesContent":["import {\n ContentFilterError,\n ProviderError,\n type GeneratedImage,\n type ImageGenerationOptions,\n type ImageGenerationResponse,\n type ImageModelContract,\n type ImageModelPricing,\n} from \"@warlock.js/ai\";\nimport { log, type Logger } from \"@warlock.js/logger\";\nimport type { GenerateImagesConfig, GoogleGenAI } from \"@google/genai\";\nimport type { GoogleImageConfig } from \"./config.type\";\nimport { wrapGoogleError } from \"./utils\";\n\nconst LOG_MODULE = \"ai.google\";\n\n/** Map a neutral output container hint to an IANA media type. */\nfunction mediaTypeFor(format: string | undefined): string | undefined {\n switch (format) {\n case \"png\":\n return \"image/png\";\n case \"jpeg\":\n case \"jpg\":\n return \"image/jpeg\";\n case \"webp\":\n return \"image/webp\";\n default:\n return undefined;\n }\n}\n\n/**\n * Google Imagen-backed implementation of `ImageModelContract`, via\n * `ai.models.generateImages`. Imagen is per-image-metered and returns\n * base64 image bytes (no hosted URL, no token usage).\n *\n * **No model-id validation.** `config.name` is passed through to\n * `ai.models.generateImages` exactly as given — the constructor never\n * inspects it. Google adds and retires image model ids on its own\n * schedule, so an id this adapter does not recognize is not the\n * adapter's call to refuse; an unsupported id surfaces as a provider\n * error from Google (wrapped into the typed `AIError` hierarchy by\n * `generate()`), not as a local one.\n *\n * **Safety filtering.** When Imagen filters every candidate for safety\n * (`raiFilteredReason`), this surfaces a typed `ContentFilterError`\n * carrying the reason, rather than returning an empty success.\n *\n * @example\n * const model = new GoogleImageModel(ai, { name: \"imagen-4.0-generate-001\" }, \"google\");\n * const { images } = await model.generate(\"a watercolor lighthouse at dawn\");\n */\nexport class GoogleImageModel 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: GenerateImagesConfig = {};\n\n if (options?.count !== undefined) config.numberOfImages = options.count;\n if (options?.aspectRatio !== undefined) config.aspectRatio = options.aspectRatio;\n if (options?.negativePrompt !== undefined) config.negativePrompt = options.negativePrompt;\n if (options?.signal !== undefined) config.abortSignal = options.signal;\n\n const outputMimeType = mediaTypeFor(options?.format);\n if (outputMimeType !== undefined) config.outputMimeType = outputMimeType;\n\n // Imagen sizing is `imageSize` (\"1K\"/\"2K\") — a distinct concept from\n // OpenAI's WxH `size`, so we honor only an explicit passthrough.\n if (typeof options?.imageSize === \"string\") config.imageSize = options.imageSize;\n if (typeof options?.personGeneration === \"string\") {\n config.personGeneration = options.personGeneration as GenerateImagesConfig[\"personGeneration\"];\n }\n\n this.logger.debug(LOG_MODULE, \"image.request\", \"models.generateImages\", {\n model: this.name,\n count: options?.count ?? 1,\n });\n\n let response: Awaited<ReturnType<GoogleGenAI[\"models\"][\"generateImages\"]>>;\n\n try {\n response = await this.ai.models.generateImages({ model: this.name, prompt, config });\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 generated = response.generatedImages ?? [];\n const images: GeneratedImage[] = [];\n\n for (const candidate of generated) {\n const bytes = candidate.image?.imageBytes;\n if (!bytes) continue;\n\n images.push({\n type: \"base64\",\n base64: bytes,\n mediaType: candidate.image?.mimeType ?? outputMimeType ?? \"image/png\",\n ...(candidate.enhancedPrompt ? { revisedPrompt: candidate.enhancedPrompt } : {}),\n });\n }\n\n if (images.length === 0) {\n const filtered = generated.find((candidate) => candidate.raiFilteredReason);\n\n if (filtered?.raiFilteredReason) {\n throw new ContentFilterError(\n `Imagen filtered all candidates: ${filtered.raiFilteredReason}`,\n { reason: filtered.raiFilteredReason },\n );\n }\n\n throw new ProviderError(\"Imagen returned no images.\");\n }\n\n this.logger.debug(LOG_MODULE, \"image.response\", \"models.generateImages succeeded\", {\n images: images.length,\n });\n\n // Imagen returns no token usage — honest zero (priced per image).\n return { images, usage: { input: 0, output: 0, total: 0 } };\n }\n}\n"],"mappings":";;;;;;AAcA,MAAM,aAAa;;AAGnB,SAAS,aAAa,QAAgD;CACpE,QAAQ,QAAR;EACE,KAAK,OACH,OAAO;EACT,KAAK;EACL,KAAK,OACH,OAAO;EACT,KAAK,QACH,OAAO;EACT,SACE;CACJ;AACF;;;;;;;;;;;;;;;;;;;;;;AAuBA,IAAa,mBAAb,MAA4D;CAQ1D,AAAO,YAAY,IAAiB,QAA2B,WAAmB,UAAU;gBAF1D;EAGhC,KAAK,KAAK;EACV,KAAK,OAAO,OAAO;EACnB,KAAK,WAAW;EAChB,KAAK,UAAU,OAAO;CACxB;CAEA,MAAa,SACX,QACA,SACkC;EAClC,MAAM,SAA+B,CAAC;EAEtC,IAAI,SAAS,UAAU,QAAW,OAAO,iBAAiB,QAAQ;EAClE,IAAI,SAAS,gBAAgB,QAAW,OAAO,cAAc,QAAQ;EACrE,IAAI,SAAS,mBAAmB,QAAW,OAAO,iBAAiB,QAAQ;EAC3E,IAAI,SAAS,WAAW,QAAW,OAAO,cAAc,QAAQ;EAEhE,MAAM,iBAAiB,aAAa,SAAS,MAAM;EACnD,IAAI,mBAAmB,QAAW,OAAO,iBAAiB;EAI1D,IAAI,OAAO,SAAS,cAAc,UAAU,OAAO,YAAY,QAAQ;EACvE,IAAI,OAAO,SAAS,qBAAqB,UACvC,OAAO,mBAAmB,QAAQ;EAGpC,KAAK,OAAO,MAAM,YAAY,iBAAiB,yBAAyB;GACtE,OAAO,KAAK;GACZ,OAAO,SAAS,SAAS;EAC3B,CAAC;EAED,IAAI;EAEJ,IAAI;GACF,WAAW,MAAM,KAAK,GAAG,OAAO,eAAe;IAAE,OAAO,KAAK;IAAM;IAAQ;GAAO,CAAC;EACrF,SAAS,QAAQ;GACf,MAAM,UAAU,gBAAgB,MAAM;GAEtC,KAAK,OAAO,MAAM,YAAY,eAAe,QAAQ,SAAS;IAC5D,MAAM,QAAQ;IACd,SAAS,QAAQ;GACnB,CAAC;GAED,MAAM;EACR;EAEA,MAAM,YAAY,SAAS,mBAAmB,CAAC;EAC/C,MAAM,SAA2B,CAAC;EAElC,KAAK,MAAM,aAAa,WAAW;GACjC,MAAM,QAAQ,UAAU,OAAO;GAC/B,IAAI,CAAC,OAAO;GAEZ,OAAO,KAAK;IACV,MAAM;IACN,QAAQ;IACR,WAAW,UAAU,OAAO,YAAY,kBAAkB;IAC1D,GAAI,UAAU,iBAAiB,EAAE,eAAe,UAAU,eAAe,IAAI,CAAC;GAChF,CAAC;EACH;EAEA,IAAI,OAAO,WAAW,GAAG;GACvB,MAAM,WAAW,UAAU,MAAM,cAAc,UAAU,iBAAiB;GAE1E,IAAI,UAAU,mBACZ,MAAM,IAAI,mBACR,mCAAmC,SAAS,qBAC5C,EAAE,QAAQ,SAAS,kBAAkB,CACvC;GAGF,MAAM,IAAI,cAAc,4BAA4B;EACtD;EAEA,KAAK,OAAO,MAAM,YAAY,kBAAkB,mCAAmC,EACjF,QAAQ,OAAO,OACjB,CAAC;EAGD,OAAO;GAAE;GAAQ,OAAO;IAAE,OAAO;IAAG,QAAQ;IAAG,OAAO;GAAE;EAAE;CAC5D;AACF"}
|
package/esm/index.d.mts
CHANGED
|
@@ -1,5 +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
|
-
|
|
5
|
-
export { GOOGLE_IMAGE_MODEL_PREFIXES, type GoogleEmbedderConfig, type GoogleImageConfig, GoogleImageModel, type GoogleModelConfig, GoogleSDK, type GoogleSDKConfig, isGoogleImageModel };
|
|
5
|
+
export { GeminiImageModel, type GoogleEmbedderConfig, type GoogleImageConfig, GoogleImageModel, type GoogleModelConfig, GoogleSDK, type GoogleSDKConfig };
|
package/esm/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { GeminiImageModel } from "./gemini-image.mjs";
|
|
2
2
|
import { GoogleImageModel } from "./image.mjs";
|
|
3
3
|
import { GoogleSDK } from "./sdk.mjs";
|
|
4
4
|
|
|
5
|
-
export {
|
|
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)
|
|
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
|
-
*
|
|
277
|
-
*
|
|
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)
|
|
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,18 +57,27 @@ declare class GoogleSDK implements SDKAdapterContract {
|
|
|
57
57
|
*/
|
|
58
58
|
embedder(config: GoogleEmbedderConfig): EmbedderContract;
|
|
59
59
|
/**
|
|
60
|
-
* Build
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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).
|
|
63
72
|
*
|
|
64
73
|
* Pricing resolution mirrors `model()`: per-model `config.pricing`
|
|
65
74
|
* wins, otherwise the SDK-level registry entry keyed by `config.name`,
|
|
66
|
-
* otherwise `undefined`.
|
|
67
|
-
* entry typically carries `{ perImage }`.
|
|
75
|
+
* otherwise `undefined`.
|
|
68
76
|
*
|
|
69
77
|
* @example
|
|
70
|
-
* const
|
|
71
|
-
* const
|
|
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" });
|
|
72
81
|
*/
|
|
73
82
|
image(config: GoogleImageConfig): ImageModelContract;
|
|
74
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":";;;;;;
|
|
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"}
|