@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 +767 -0
- package/cjs/index.cjs.map +1 -0
- package/esm/config.type.d.mts +83 -0
- package/esm/config.type.d.mts.map +1 -0
- package/esm/embedder.mjs +103 -0
- package/esm/embedder.mjs.map +1 -0
- package/esm/index.d.mts +3 -0
- package/esm/index.mjs +3 -0
- package/esm/known-vision-models.mjs +39 -0
- package/esm/known-vision-models.mjs.map +1 -0
- package/esm/model.mjs +277 -0
- package/esm/model.mjs.map +1 -0
- package/esm/sdk.d.mts +62 -0
- package/esm/sdk.d.mts.map +1 -0
- package/esm/sdk.mjs +78 -0
- package/esm/sdk.mjs.map +1 -0
- package/esm/utils/index.mjs +6 -0
- package/esm/utils/map-finish-reason.mjs +34 -0
- package/esm/utils/map-finish-reason.mjs.map +1 -0
- package/esm/utils/to-google-contents.mjs +120 -0
- package/esm/utils/to-google-contents.mjs.map +1 -0
- package/esm/utils/to-google-tools.mjs +41 -0
- package/esm/utils/to-google-tools.mjs.map +1 -0
- package/esm/utils/wrap-google-error.mjs +108 -0
- package/esm/utils/wrap-google-error.mjs.map +1 -0
- package/llms-full.txt +154 -0
- package/llms.txt +9 -0
- package/package.json +39 -0
- package/skills/README.md +9 -0
- package/skills/setup-google/SKILL.md +144 -0
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
|