@warlock.js/ai-google 4.1.1

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/cjs/index.cjs ADDED
@@ -0,0 +1,767 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ let _google_genai = require("@google/genai");
3
+ let _warlock_js_ai = require("@warlock.js/ai");
4
+ let _warlock_js_logger = require("@warlock.js/logger");
5
+
6
+ //#region ../../@warlock.js/ai-google/src/utils/map-finish-reason.ts
7
+ const finishReasonMap = {
8
+ STOP: "stop",
9
+ MAX_TOKENS: "length"
10
+ };
11
+ /**
12
+ * Map Gemini's `FinishReason` enum value to the normalized
13
+ * `FinishReason` union.
14
+ *
15
+ * `STOP` is the natural terminal. `MAX_TOKENS` maps to `length`.
16
+ * Everything else — `SAFETY`, `RECITATION`, `BLOCKLIST`,
17
+ * `PROHIBITED_CONTENT`, `SPII`, `MALFORMED_FUNCTION_CALL`,
18
+ * `UNEXPECTED_TOOL_CALL`, `LANGUAGE`, `OTHER`,
19
+ * `FINISH_REASON_UNSPECIFIED`, `null`, or any future value — falls
20
+ * through to `"error"`.
21
+ *
22
+ * Note: Gemini reports `STOP` even when the turn ended in a function
23
+ * call (it has no `tool_use` reason). `GoogleModel` overrides the
24
+ * mapped reason to `"tool_calls"` when the response carries function
25
+ * calls — this map intentionally stays purely about the raw signal.
26
+ *
27
+ * @example
28
+ * mapFinishReason("STOP"); // "stop"
29
+ * mapFinishReason("MAX_TOKENS"); // "length"
30
+ * mapFinishReason("SAFETY"); // "error"
31
+ * mapFinishReason(undefined); // "error"
32
+ */
33
+ function mapFinishReason(raw) {
34
+ return finishReasonMap[raw ?? ""] ?? "error";
35
+ }
36
+
37
+ //#endregion
38
+ //#region ../../@warlock.js/ai-google/src/utils/to-google-contents.ts
39
+ /**
40
+ * Convert vendor-neutral `Message[]` into Gemini's request shape.
41
+ *
42
+ * Gemini specifics this function absorbs:
43
+ *
44
+ * 1. **No `system` role.** System messages concatenate into the
45
+ * separate `systemInstruction` config field.
46
+ * 2. **Role names differ.** Neutral `assistant` → Gemini `"model"`;
47
+ * `user` stays `"user"`.
48
+ * 3. **Tool results are `user` turns.** A neutral `tool` message
49
+ * becomes a `"user"` content with a single `functionResponse` part.
50
+ * 4. **Tool calls are `functionCall` parts.** An assistant message
51
+ * with `toolCalls` becomes a `"model"` content: an optional leading
52
+ * `text` part followed by one `functionCall` part per call.
53
+ *
54
+ * @example
55
+ * const { systemInstruction, contents } = toGoogleContents([
56
+ * { role: "system", content: "Be concise." },
57
+ * { role: "user", content: "Hi" },
58
+ * ]);
59
+ */
60
+ function toGoogleContents(messages) {
61
+ const systemParts = [];
62
+ const contents = [];
63
+ for (const message of messages) {
64
+ if (message.role === "system") {
65
+ systemParts.push(stringifyContent(message.content));
66
+ continue;
67
+ }
68
+ if (message.role === "tool") {
69
+ contents.push({
70
+ role: "user",
71
+ parts: [{ functionResponse: {
72
+ name: message.toolCallId ?? "",
73
+ response: toResponseObject(stringifyContent(message.content))
74
+ } }]
75
+ });
76
+ continue;
77
+ }
78
+ if (message.role === "assistant" && message.toolCalls && message.toolCalls.length > 0) {
79
+ const parts = [];
80
+ const text = stringifyContent(message.content);
81
+ if (text) parts.push({ text });
82
+ for (const toolCall of message.toolCalls) {
83
+ const thoughtSignature = toolCall.providerMetadata?.thoughtSignature;
84
+ parts.push({
85
+ ...typeof thoughtSignature === "string" ? { thoughtSignature } : {},
86
+ functionCall: {
87
+ name: toolCall.name,
88
+ args: toolCall.input ?? {}
89
+ }
90
+ });
91
+ }
92
+ contents.push({
93
+ role: "model",
94
+ parts
95
+ });
96
+ continue;
97
+ }
98
+ if (message.role === "user" && Array.isArray(message.content)) {
99
+ contents.push({
100
+ role: "user",
101
+ parts: message.content.map(toGooglePart)
102
+ });
103
+ continue;
104
+ }
105
+ contents.push({
106
+ role: message.role === "assistant" ? "model" : "user",
107
+ parts: [{ text: stringifyContent(message.content) }]
108
+ });
109
+ }
110
+ return {
111
+ systemInstruction: systemParts.length > 0 ? systemParts.join("\n\n") : void 0,
112
+ contents
113
+ };
114
+ }
115
+ /**
116
+ * Multipart content is only meaningful on user messages — for any
117
+ * other role collapse a `ContentPart[]` to concatenated text. Plain
118
+ * strings pass through unchanged.
119
+ */
120
+ function stringifyContent(content) {
121
+ if (typeof content === "string") return content;
122
+ return content.filter((part) => part.type === "text").map((part) => part.text).join("");
123
+ }
124
+ /**
125
+ * Gemini's `functionResponse.response` must be a JSON object. Tool
126
+ * results arrive as a string (usually stringified JSON) — parse it
127
+ * when it is a JSON object, otherwise wrap the raw string under a
128
+ * `result` key so the model always receives a well-formed object.
129
+ */
130
+ function toResponseObject(raw) {
131
+ const parsed = (0, _warlock_js_ai.safeJsonParse)(raw, void 0);
132
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
133
+ return { result: raw };
134
+ }
135
+ /**
136
+ * Map a resolved `ContentPart` to a Gemini `Part`. Images are sent as
137
+ * inline base64 (`inlineData`). Gemini's `generateContent` does not
138
+ * fetch arbitrary remote URLs (only Files API / GCS URIs via
139
+ * `fileData`), so a neutral `{ url }` image surfaces a typed
140
+ * `InvalidRequestError` upfront rather than a downstream Gemini fault.
141
+ * The agent resolves attachments before this point, so nothing is
142
+ * read or fetched here.
143
+ */
144
+ function toGooglePart(part) {
145
+ if (part.type === "text") return { text: part.text };
146
+ if ("url" in part.source) throw new _warlock_js_ai.InvalidRequestError("Gemini generateContent does not fetch remote-URL images; supply base64 image bytes instead.");
147
+ return { inlineData: {
148
+ mimeType: part.source.mediaType,
149
+ data: part.source.base64
150
+ } };
151
+ }
152
+
153
+ //#endregion
154
+ //#region ../../@warlock.js/ai-google/src/utils/to-google-tools.ts
155
+ /**
156
+ * Convert vendor-neutral `ToolConfig[]` into Gemini's `tools` array —
157
+ * a single `Tool` carrying one `functionDeclarations` entry per tool.
158
+ *
159
+ * The input schema is forwarded via `parametersJsonSchema` (raw JSON
160
+ * Schema, mutually exclusive with Gemini's typed `parameters`).
161
+ * Non-object extractions degrade to a parameterless object so
162
+ * registration never fails.
163
+ *
164
+ * Returns `undefined` when there are no tools so the caller can omit
165
+ * `config.tools` entirely.
166
+ *
167
+ * @example
168
+ * const tools = toGoogleTools([weatherTool]);
169
+ * await ai.models.generateContent({ model, contents, config: { tools } });
170
+ */
171
+ function toGoogleTools(tools) {
172
+ if (!tools || tools.length === 0) return;
173
+ return [{ functionDeclarations: tools.map((tool) => ({
174
+ name: tool.name,
175
+ description: tool.description,
176
+ parametersJsonSchema: toJsonSchema(tool.input)
177
+ })) }];
178
+ }
179
+ /**
180
+ * Resolve a tool's input schema to a JSON-Schema object. Gemini wants
181
+ * an object root for function parameters; anything else (or a failed
182
+ * extraction) degrades to a parameterless object.
183
+ */
184
+ function toJsonSchema(input) {
185
+ const schema = (0, _warlock_js_ai.extractJsonSchema)(input);
186
+ if (schema && schema.type === "object") return schema;
187
+ return { type: "object" };
188
+ }
189
+
190
+ //#endregion
191
+ //#region ../../@warlock.js/ai-google/src/utils/wrap-google-error.ts
192
+ /**
193
+ * Wrap any thrown value caught inside the Gemini adapter into the
194
+ * appropriate `@warlock.js/ai` `AIError` subclass.
195
+ *
196
+ * **Dispatch strategy.** Gemini has no machine error `code`; the
197
+ * signals are the HTTP `status` and the canonical status phrase Google
198
+ * embeds in `message` (`PERMISSION_DENIED`, `RESOURCE_EXHAUSTED`,
199
+ * `INVALID_ARGUMENT`, …). Dispatch keys on `status`, using the message
200
+ * phrase as the tie-breaker for the two 400 sub-cases
201
+ * (context-length vs generic) and for status-less auth/quota errors.
202
+ *
203
+ * `AIError` instances pass through unchanged so `catch/throw wrap(e)`
204
+ * pipelines never double-wrap.
205
+ *
206
+ * @example
207
+ * try {
208
+ * return await this.ai.models.generateContent(...);
209
+ * } catch (thrown) {
210
+ * throw wrapGoogleError(thrown);
211
+ * }
212
+ */
213
+ function wrapGoogleError(thrown) {
214
+ if (thrown instanceof _warlock_js_ai.AIError) return thrown;
215
+ const shape = toShape(thrown);
216
+ const context = buildContext(shape);
217
+ const message = shape.message ?? (thrown instanceof Error ? thrown.message : String(thrown));
218
+ if (isTimeout(shape)) return new _warlock_js_ai.ProviderTimeoutError(message, {
219
+ cause: thrown,
220
+ context
221
+ });
222
+ if (shape.status === 401 || shape.status === 403 || /permission_denied|api key not valid|unauthenticated/i.test(message)) return new _warlock_js_ai.ProviderAuthError(message, {
223
+ cause: thrown,
224
+ context
225
+ });
226
+ if (shape.status === 429 || /resource_exhausted|quota/i.test(message)) return new _warlock_js_ai.ProviderRateLimitError(message, {
227
+ cause: thrown,
228
+ context
229
+ });
230
+ if (shape.status === 400) {
231
+ if (/token count|context length|exceeds the maximum|input is too long/i.test(message)) return new _warlock_js_ai.ContextLengthExceededError(message, {
232
+ cause: thrown,
233
+ context
234
+ });
235
+ return new _warlock_js_ai.InvalidRequestError(message, {
236
+ cause: thrown,
237
+ context
238
+ });
239
+ }
240
+ if (shape.status === 404 || isClientStatus(shape.status)) return new _warlock_js_ai.InvalidRequestError(message, {
241
+ cause: thrown,
242
+ context
243
+ });
244
+ return new _warlock_js_ai.ProviderError(message, {
245
+ cause: thrown,
246
+ context
247
+ });
248
+ }
249
+ /**
250
+ * Read the raw error shape. The Gemini SDK's `ApiError` carries a
251
+ * numeric `status`; flattened/proxied errors may carry it (or `code`)
252
+ * loosely.
253
+ */
254
+ function toShape(thrown) {
255
+ if (thrown instanceof _google_genai.ApiError) return {
256
+ status: thrown.status,
257
+ message: thrown.message,
258
+ name: thrown.name
259
+ };
260
+ if (typeof thrown === "object" && thrown !== null) {
261
+ const raw = thrown;
262
+ return {
263
+ status: typeof raw.status === "number" ? raw.status : void 0,
264
+ message: typeof raw.message === "string" ? raw.message : void 0,
265
+ name: typeof raw.name === "string" ? raw.name : void 0,
266
+ code: typeof raw.code === "string" ? raw.code : void 0
267
+ };
268
+ }
269
+ return {};
270
+ }
271
+ /**
272
+ * Decide whether the error is a timeout. Gemini maps gateway timeouts
273
+ * to HTTP 504 (`DEADLINE_EXCEEDED`); transport aborts surface as
274
+ * `AbortError` / `ETIMEDOUT` / `ECONNABORTED`.
275
+ */
276
+ function isTimeout(shape) {
277
+ if (shape.status === 504) return true;
278
+ if (shape.name === "AbortError" || /deadline_exceeded/i.test(shape.message ?? "")) return true;
279
+ return shape.code === "ETIMEDOUT" || shape.code === "ECONNABORTED";
280
+ }
281
+ /** True for HTTP 4xx — a client-side request problem, not a server fault. */
282
+ function isClientStatus(status) {
283
+ return typeof status === "number" && status >= 400 && status < 500;
284
+ }
285
+ /** Attach the diagnostic fields to `error.context`. */
286
+ function buildContext(shape) {
287
+ const context = {};
288
+ if (shape.status !== void 0) context.status = shape.status;
289
+ if (shape.name) context.code = shape.name;
290
+ return context;
291
+ }
292
+
293
+ //#endregion
294
+ //#region ../../@warlock.js/ai-google/src/embedder.ts
295
+ const LOG_MODULE$1 = "ai.google";
296
+ /**
297
+ * Token usage is not returned by Gemini's `embedContent`, so every
298
+ * embedding result reports a zeroed `EmbeddingUsage` (honest absence,
299
+ * not a fabricated estimate).
300
+ */
301
+ const NO_USAGE = {
302
+ promptTokens: 0,
303
+ totalTokens: 0
304
+ };
305
+ /**
306
+ * Google Gemini-backed implementation of `EmbedderContract`
307
+ * (`gemini-embedding-001`, `text-embedding-004`, …) via
308
+ * `models.embedContent`.
309
+ *
310
+ * **Role.** Converts text into floating-point vectors. Standalone
311
+ * primitive — unrelated to generateContent / tools / the agent loop.
312
+ *
313
+ * **Batch is native.** Gemini's `embedContent` accepts an array of
314
+ * inputs and returns embeddings in the same order, so `embedMany` is
315
+ * a single request (unlike the Bedrock/Titan adapter, which has to
316
+ * loop).
317
+ *
318
+ * **No usage.** Gemini's embed endpoint returns no token counts;
319
+ * `usage` is always `{ promptTokens: 0, totalTokens: 0 }`.
320
+ *
321
+ * **Dimensions.** When no `dimensions` override is given,
322
+ * `this.dimensions` starts at `0` and is populated from the first
323
+ * response's vector length, then cached. Passing `dimensions`
324
+ * forwards Gemini's `outputDimensionality` truncation hint and sets
325
+ * the initial value immediately.
326
+ *
327
+ * @example
328
+ * const embedder = new GoogleEmbedder(ai, { name: "gemini-embedding-001" });
329
+ * const { vector } = await embedder.embed("Hello world");
330
+ * const { vectors } = await embedder.embedMany(["doc 1", "doc 2"]);
331
+ */
332
+ var GoogleEmbedder = class {
333
+ constructor(ai, config, provider = "google") {
334
+ this.logger = _warlock_js_logger.log;
335
+ this.ai = ai;
336
+ this.name = config.name;
337
+ this.provider = provider;
338
+ this.configuredDimensions = config.dimensions;
339
+ this.dimensions = config.dimensions ?? 0;
340
+ }
341
+ async embed(input) {
342
+ return {
343
+ vector: (await this.request([input]))[0],
344
+ dimensions: this.dimensions,
345
+ usage: NO_USAGE
346
+ };
347
+ }
348
+ async embedMany(inputs) {
349
+ return {
350
+ vectors: await this.request(inputs),
351
+ dimensions: this.dimensions,
352
+ usage: NO_USAGE
353
+ };
354
+ }
355
+ /**
356
+ * Shared transport: one `embedContent` call for the whole batch,
357
+ * wrap provider errors, cache `dimensions` from the first vector,
358
+ * and return the raw vectors in input order.
359
+ */
360
+ async request(inputs) {
361
+ this.logger.debug(LOG_MODULE$1, "embedder.request", "embedContent", {
362
+ model: this.name,
363
+ count: inputs.length
364
+ });
365
+ let response;
366
+ try {
367
+ response = await this.ai.models.embedContent({
368
+ model: this.name,
369
+ contents: inputs,
370
+ ...this.configuredDimensions !== void 0 ? { config: { outputDimensionality: this.configuredDimensions } } : {}
371
+ });
372
+ } catch (thrown) {
373
+ const wrapped = wrapGoogleError(thrown);
374
+ this.logger.error(LOG_MODULE$1, "embedder.error", wrapped.message, {
375
+ code: wrapped.code,
376
+ context: wrapped.context
377
+ });
378
+ throw wrapped;
379
+ }
380
+ const vectors = (response.embeddings ?? []).map((embedding) => embedding.values ?? []);
381
+ if (this.dimensions === 0 && vectors[0]) this.dimensions = vectors[0].length;
382
+ this.logger.debug(LOG_MODULE$1, "embedder.response", "embedContent returned", {
383
+ count: vectors.length,
384
+ dimensions: this.dimensions
385
+ });
386
+ return vectors;
387
+ }
388
+ };
389
+
390
+ //#endregion
391
+ //#region ../../@warlock.js/ai-google/src/known-vision-models.ts
392
+ /**
393
+ * Substrings identifying Gemini model ids whose family accepts image
394
+ * input (vision).
395
+ *
396
+ * Every Gemini 1.5, 2.x, and 2.5 model is natively multimodal, as is
397
+ * the legacy `gemini-pro-vision`. Only the original text-only
398
+ * `gemini-pro` / `gemini-1.0-pro` is excluded. A substring match
399
+ * tolerates the date/preview suffixes Google appends
400
+ * (`gemini-2.5-flash-preview-05-20`). Override per-model via
401
+ * `google.model({ name, vision: true | false })`.
402
+ */
403
+ const VISION_CAPABLE_SUBSTRINGS = [
404
+ "gemini-1.5",
405
+ "gemini-2",
406
+ "gemini-exp",
407
+ "gemini-pro-vision",
408
+ "gemini-flash"
409
+ ];
410
+ /**
411
+ * Infer whether a Gemini model id supports vision based on the known
412
+ * multimodal-family substrings. Unknown ids default to `false` so
413
+ * passing an image attachment to an unsupported model surfaces a
414
+ * clear, agent-side capability error instead of an opaque Gemini 400.
415
+ *
416
+ * @example
417
+ * inferVisionCapability("gemini-2.5-flash"); // → true
418
+ * inferVisionCapability("gemini-1.5-pro-002"); // → true
419
+ * inferVisionCapability("gemini-1.0-pro"); // → false
420
+ * inferVisionCapability("text-embedding-004"); // → false
421
+ */
422
+ function inferVisionCapability(modelId) {
423
+ const normalized = modelId.toLowerCase();
424
+ return VISION_CAPABLE_SUBSTRINGS.some((fragment) => normalized.includes(fragment));
425
+ }
426
+
427
+ //#endregion
428
+ //#region ../../@warlock.js/ai-google/src/model.ts
429
+ const LOG_MODULE = "ai.google";
430
+ /**
431
+ * Google Gemini-backed implementation of `ModelContract`.
432
+ *
433
+ * **Role.** The provider-facing bridge between the vendor-neutral
434
+ * `@warlock.js/ai` agent runtime and the `@google/genai` SDK
435
+ * (`models.generateContent` / `generateContentStream`).
436
+ *
437
+ * **Responsibility.**
438
+ * - Owns: a long-lived `GoogleGenAI` client + frozen `ModelConfig`
439
+ * (name, temperature, maxTokens) used as per-call defaults.
440
+ * - Owns: translating vendor-neutral `Message[]` / `ToolConfig[]` into
441
+ * Gemini shapes (systemInstruction hoisting, `model` role,
442
+ * `functionCall` / `functionResponse` parts, inline image bytes) on
443
+ * the way out, and Gemini's candidate/parts response (text, function
444
+ * calls, finish reason, token usage) back into neutral shapes on the
445
+ * way in.
446
+ * - Does NOT own: dispatching tools, looping, history, retries — those
447
+ * are agent concerns. The model is a per-call protocol adapter.
448
+ *
449
+ * Modeled as a class (see §4.2 of code-style.md — "long-lived state
450
+ * across calls"): the `GoogleGenAI` client is reused for the SDK's
451
+ * lifetime.
452
+ *
453
+ * @example
454
+ * import { GoogleGenAI } from "@google/genai";
455
+ * const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
456
+ * const model = new GoogleModel(ai, { name: "gemini-2.5-flash" });
457
+ *
458
+ * const myAgent = agent({ model, tools: [searchTool] });
459
+ * const result = await myAgent.execute("Summarize today's news.");
460
+ */
461
+ var GoogleModel = class {
462
+ constructor(ai, config, provider = "google") {
463
+ this.logger = _warlock_js_logger.log;
464
+ this.ai = ai;
465
+ this.config = config;
466
+ this.name = config.name;
467
+ this.provider = provider;
468
+ this.pricing = config.pricing;
469
+ this.capabilities = {
470
+ structuredOutput: config.structuredOutput ?? true,
471
+ vision: config.vision ?? inferVisionCapability(config.name)
472
+ };
473
+ }
474
+ /**
475
+ * Single-shot completion. Sends the full message list to
476
+ * `generateContent`, waits for the terminal response, and reshapes
477
+ * it into a vendor-neutral `ModelResponse`. Per-call `options`
478
+ * override the instance defaults for this call only.
479
+ */
480
+ async complete(messages, options) {
481
+ this.logger.debug(LOG_MODULE, "request", "Starting generateContent call", {
482
+ model: this.name,
483
+ messageCount: messages.length,
484
+ streaming: false,
485
+ toolCount: options?.tools?.length ?? 0
486
+ });
487
+ const { systemInstruction, contents } = toGoogleContents(messages);
488
+ let response;
489
+ try {
490
+ response = await this.ai.models.generateContent({
491
+ model: this.name,
492
+ contents,
493
+ config: this.buildConfig(systemInstruction, options)
494
+ });
495
+ } catch (thrown) {
496
+ throw this.logAndWrap(thrown);
497
+ }
498
+ const toolCalls = this.extractToolCalls(response);
499
+ const finishReason = toolCalls ? "tool_calls" : mapFinishReason(response.candidates?.[0]?.finishReason);
500
+ const usage = this.extractUsage(response);
501
+ this.logger.debug(LOG_MODULE, "response", "generateContent call succeeded", {
502
+ finishReason,
503
+ usage
504
+ });
505
+ return {
506
+ content: response.text ?? "",
507
+ finishReason,
508
+ usage,
509
+ toolCalls
510
+ };
511
+ }
512
+ /**
513
+ * Incremental streaming completion via `generateContentStream`.
514
+ * Yields neutral `ModelStreamChunk`s — `delta` for text, `tool-call`
515
+ * per function call (Gemini emits a fully-formed call, not partial
516
+ * JSON), and a terminal `done` with the final finish reason + usage.
517
+ */
518
+ async *stream(messages, options) {
519
+ this.logger.debug(LOG_MODULE, "request", "Starting generateContentStream call", {
520
+ model: this.name,
521
+ messageCount: messages.length,
522
+ streaming: true,
523
+ toolCount: options?.tools?.length ?? 0
524
+ });
525
+ const { systemInstruction, contents } = toGoogleContents(messages);
526
+ let iterable;
527
+ try {
528
+ iterable = await this.ai.models.generateContentStream({
529
+ model: this.name,
530
+ contents,
531
+ config: this.buildConfig(systemInstruction, options)
532
+ });
533
+ } catch (thrown) {
534
+ throw this.logAndWrap(thrown);
535
+ }
536
+ let rawFinishReason;
537
+ let sawToolCall = false;
538
+ const usage = {
539
+ input: 0,
540
+ output: 0,
541
+ total: 0
542
+ };
543
+ try {
544
+ for await (const chunk of iterable) {
545
+ const text = chunk.text;
546
+ if (text) yield {
547
+ type: "delta",
548
+ content: text
549
+ };
550
+ for (const part of chunk.candidates?.[0]?.content?.parts ?? []) {
551
+ const toolCall = this.partToToolCall(part);
552
+ if (!toolCall) continue;
553
+ sawToolCall = true;
554
+ yield {
555
+ type: "tool-call",
556
+ id: toolCall.id,
557
+ name: toolCall.name,
558
+ input: toolCall.input,
559
+ ...toolCall.providerMetadata ? { providerMetadata: toolCall.providerMetadata } : {}
560
+ };
561
+ }
562
+ const candidateFinish = chunk.candidates?.[0]?.finishReason;
563
+ if (candidateFinish) rawFinishReason = candidateFinish;
564
+ if (chunk.usageMetadata) this.applyUsage(usage, chunk.usageMetadata);
565
+ }
566
+ } catch (thrown) {
567
+ throw this.logAndWrap(thrown);
568
+ }
569
+ const finishReason = sawToolCall ? "tool_calls" : mapFinishReason(rawFinishReason);
570
+ this.logger.debug(LOG_MODULE, "response", "generateContentStream call succeeded", {
571
+ finishReason,
572
+ usage
573
+ });
574
+ yield {
575
+ type: "done",
576
+ finishReason,
577
+ usage
578
+ };
579
+ }
580
+ /**
581
+ * Assemble the `GenerateContentConfig` shared by `complete()` and
582
+ * `stream()`: inference params, hoisted system instruction,
583
+ * cancellation signal, and conditional tools + native structured
584
+ * output.
585
+ */
586
+ buildConfig(systemInstruction, options) {
587
+ const temperature = options?.temperature ?? this.config.temperature;
588
+ const maxOutputTokens = options?.maxTokens ?? this.config.maxTokens;
589
+ return {
590
+ ...systemInstruction ? { systemInstruction } : {},
591
+ ...temperature !== void 0 ? { temperature } : {},
592
+ ...maxOutputTokens !== void 0 ? { maxOutputTokens } : {},
593
+ ...options?.signal ? { abortSignal: options.signal } : {},
594
+ ...this.buildTools(options?.tools),
595
+ ...this.buildStructuredOutput(options?.responseSchema)
596
+ };
597
+ }
598
+ /**
599
+ * Spread-friendly tools fragment. Empty object when no tools were
600
+ * supplied so the caller can unconditionally spread it.
601
+ */
602
+ buildTools(tools) {
603
+ const mapped = toGoogleTools(tools);
604
+ return mapped ? { tools: mapped } : {};
605
+ }
606
+ /**
607
+ * Translate the neutral `responseSchema` into Gemini's native JSON
608
+ * structured output (`responseMimeType: "application/json"` +
609
+ * `responseJsonSchema`, which takes a raw JSON Schema directly).
610
+ * Emitted only when the model is `structuredOutput`-capable and the
611
+ * schema is an object root — otherwise the agent's soft prompt hint
612
+ * + client-side `validate()` carry shape.
613
+ */
614
+ buildStructuredOutput(responseSchema) {
615
+ if (!responseSchema || !this.capabilities.structuredOutput) return {};
616
+ if (responseSchema.type !== "object" || typeof responseSchema.properties !== "object") return {};
617
+ return {
618
+ responseMimeType: "application/json",
619
+ responseJsonSchema: responseSchema
620
+ };
621
+ }
622
+ /**
623
+ * Reshape Gemini's function-call content parts into the neutral
624
+ * `ModelToolCallRequest[]`. Returns `undefined` when the model
625
+ * requested no functions so callers can branch on presence.
626
+ *
627
+ * Reads `candidates[0].content.parts` directly rather than the
628
+ * `response.functionCalls` getter: the getter discards the
629
+ * part-level `thoughtSignature`, and Gemini "thinking" models 400
630
+ * the follow-up turn if that signature is not echoed back. See
631
+ * `partToToolCall`.
632
+ */
633
+ extractToolCalls(response) {
634
+ const toolCalls = (response.candidates?.[0]?.content?.parts ?? []).map((part) => this.partToToolCall(part)).filter((call) => call !== void 0);
635
+ return toolCalls.length > 0 ? toolCalls : void 0;
636
+ }
637
+ /**
638
+ * Map a single Gemini `Part` to a neutral `ModelToolCallRequest`,
639
+ * or `undefined` when the part is not a function call. The part's
640
+ * `thoughtSignature` (opaque, set by thinking models) is carried on
641
+ * `providerMetadata` so `toGoogleContents` can replay it on the
642
+ * assistant turn — Gemini rejects the next request without it.
643
+ */
644
+ partToToolCall(part) {
645
+ if (!part.functionCall) return;
646
+ const call = part.functionCall;
647
+ return {
648
+ id: call.id ?? call.name ?? "",
649
+ name: call.name ?? "",
650
+ input: call.args ?? {},
651
+ ...part.thoughtSignature ? { providerMetadata: { thoughtSignature: part.thoughtSignature } } : {}
652
+ };
653
+ }
654
+ /**
655
+ * Normalize Gemini's `usageMetadata` into the neutral `Usage` shape.
656
+ * Cache-read tokens are surfaced as `cachedTokens` only when
657
+ * non-zero. Absent usage collapses to zeros.
658
+ */
659
+ extractUsage(response) {
660
+ const usage = {
661
+ input: 0,
662
+ output: 0,
663
+ total: 0
664
+ };
665
+ if (response.usageMetadata) this.applyUsage(usage, response.usageMetadata);
666
+ return usage;
667
+ }
668
+ /**
669
+ * Fold a Gemini `usageMetadata` block into the running neutral
670
+ * `Usage` accumulator. Shared by `complete()` and the streaming
671
+ * loop (where the final chunk carries cumulative totals).
672
+ */
673
+ applyUsage(usage, raw) {
674
+ usage.input = raw.promptTokenCount ?? usage.input;
675
+ usage.output = raw.candidatesTokenCount ?? usage.output;
676
+ usage.total = raw.totalTokenCount ?? usage.input + usage.output;
677
+ const cached = raw.cachedContentTokenCount;
678
+ if (cached && cached > 0) usage.cachedTokens = cached;
679
+ }
680
+ /**
681
+ * Wrap a thrown provider error into the typed `AIError` hierarchy
682
+ * and emit the standard error log line before it propagates.
683
+ */
684
+ logAndWrap(thrown) {
685
+ const wrapped = wrapGoogleError(thrown);
686
+ this.logger.error(LOG_MODULE, "error", wrapped.message, {
687
+ code: wrapped.code,
688
+ context: wrapped.context
689
+ });
690
+ return wrapped;
691
+ }
692
+ };
693
+
694
+ //#endregion
695
+ //#region ../../@warlock.js/ai-google/src/sdk.ts
696
+ /**
697
+ * Google Gemini-backed implementation of `SDKAdapterContract`.
698
+ *
699
+ * **Role.** The package entry point for Gemini models via the
700
+ * `@google/genai` SDK. A single `GoogleSDK` holds one live
701
+ * `GoogleGenAI` client, shared by every `ModelContract` /
702
+ * `EmbedderContract` it produces. Construct one SDK per
703
+ * account/project and reuse it everywhere.
704
+ *
705
+ * **Responsibility.**
706
+ * - Owns: a long-lived `GoogleGenAI` client (auth, Vertex vs Gemini
707
+ * API) and its lifetime. Factory for `GoogleModel` /
708
+ * `GoogleEmbedder` instances sharing that client.
709
+ * - Does NOT own: anything per-call — those live in `GoogleModel` /
710
+ * `GoogleEmbedder` and the agent runtime.
711
+ *
712
+ * Modeled as a class (see §4.2 of code-style.md — "long-lived state
713
+ * across many calls"), fronted by FP usage like the other adapters.
714
+ *
715
+ * @example
716
+ * const google = new GoogleSDK({ apiKey: process.env.GEMINI_API_KEY! });
717
+ * const model = google.model({ name: "gemini-2.5-flash", temperature: 0.7 });
718
+ * const embedder = google.embedder({ name: "gemini-embedding-001" });
719
+ */
720
+ var GoogleSDK = class {
721
+ constructor(config) {
722
+ const { provider, pricing, ...clientOptions } = config;
723
+ this.ai = new _google_genai.GoogleGenAI(clientOptions);
724
+ this.provider = provider ?? "google";
725
+ this.pricing = pricing;
726
+ }
727
+ /**
728
+ * Build a `GoogleModel` bound to this SDK's client. Each call
729
+ * returns a fresh instance; all instances share the underlying
730
+ * `GoogleGenAI` client. The SDK's `provider` label is forwarded.
731
+ *
732
+ * Pricing resolution: per-model `config.pricing` wins; otherwise the
733
+ * SDK-level registry entry keyed by `config.name`; otherwise
734
+ * `undefined` (no cost computed).
735
+ */
736
+ model(config) {
737
+ const resolvedPricing = config.pricing ?? this.pricing?.[config.name];
738
+ const resolvedConfig = resolvedPricing === config.pricing ? config : {
739
+ ...config,
740
+ pricing: resolvedPricing
741
+ };
742
+ return new GoogleModel(this.ai, resolvedConfig, this.provider);
743
+ }
744
+ /**
745
+ * Rough token-count estimate. Uses the character-heuristic
746
+ * (`approximateTokenCount`) from the core package — Gemini's
747
+ * `countTokens` is a network round-trip; `count()` is intentionally
748
+ * offline. Good for budgeting/quota guards, not billing.
749
+ */
750
+ async count(text, _model) {
751
+ return (0, _warlock_js_ai.approximateTokenCount)(text);
752
+ }
753
+ /**
754
+ * Build a `GoogleEmbedder` bound to this SDK's client.
755
+ *
756
+ * @example
757
+ * const embedder = google.embedder({ name: "gemini-embedding-001" });
758
+ * const { vector } = await embedder.embed("Hello world");
759
+ */
760
+ embedder(config) {
761
+ return new GoogleEmbedder(this.ai, config, this.provider);
762
+ }
763
+ };
764
+
765
+ //#endregion
766
+ exports.GoogleSDK = GoogleSDK;
767
+ //# sourceMappingURL=index.cjs.map