@llumiverse/drivers 1.0.0 → 1.1.0-dev.20260427.054520Z
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/lib/cjs/bedrock/index.js +90 -10
- package/lib/cjs/bedrock/index.js.map +1 -1
- package/lib/cjs/openai/index.js +2 -0
- package/lib/cjs/openai/index.js.map +1 -1
- package/lib/cjs/vertexai/index.js +31 -22
- package/lib/cjs/vertexai/index.js.map +1 -1
- package/lib/cjs/vertexai/models/claude.js +99 -26
- package/lib/cjs/vertexai/models/claude.js.map +1 -1
- package/lib/cjs/vertexai/models/gemini.js +35 -335
- package/lib/cjs/vertexai/models/gemini.js.map +1 -1
- package/lib/esm/bedrock/index.js +90 -10
- package/lib/esm/bedrock/index.js.map +1 -1
- package/lib/esm/openai/index.js +2 -0
- package/lib/esm/openai/index.js.map +1 -1
- package/lib/esm/vertexai/index.js +31 -22
- package/lib/esm/vertexai/index.js.map +1 -1
- package/lib/esm/vertexai/models/claude.js +99 -28
- package/lib/esm/vertexai/models/claude.js.map +1 -1
- package/lib/esm/vertexai/models/gemini.js +36 -336
- package/lib/esm/vertexai/models/gemini.js.map +1 -1
- package/lib/types/bedrock/index.d.ts +5 -2
- package/lib/types/bedrock/index.d.ts.map +1 -1
- package/lib/types/openai/index.d.ts.map +1 -1
- package/lib/types/vertexai/index.d.ts +4 -1
- package/lib/types/vertexai/index.d.ts.map +1 -1
- package/lib/types/vertexai/models/claude.d.ts +16 -0
- package/lib/types/vertexai/models/claude.d.ts.map +1 -1
- package/lib/types/vertexai/models/gemini.d.ts +4 -8
- package/lib/types/vertexai/models/gemini.d.ts.map +1 -1
- package/package.json +8 -8
- package/src/bedrock/index.ts +104 -12
- package/src/bedrock/streaming-tool-use.test.ts +250 -0
- package/src/openai/index.ts +2 -0
- package/src/vertexai/index.ts +32 -22
- package/src/vertexai/models/claude-streaming-spacing.test.ts +174 -0
- package/src/vertexai/models/claude.ts +120 -29
- package/src/vertexai/models/gemini-conversation-mutation.test.ts +174 -0
- package/src/vertexai/models/gemini.ts +48 -391
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
import type { ApiError } from "@google/genai";
|
|
2
2
|
import {
|
|
3
|
-
Content, FinishReason, FunctionCallingConfigMode, FunctionDeclaration, GenerateContentConfig, GenerateContentParameters,
|
|
4
|
-
GenerateContentResponseUsageMetadata,
|
|
5
|
-
HarmBlockThreshold, HarmCategory, Modality, Part,
|
|
3
|
+
type Content, FinishReason, FunctionCallingConfigMode, type FunctionDeclaration, type GenerateContentConfig, type GenerateContentParameters,
|
|
4
|
+
type GenerateContentResponseUsageMetadata,
|
|
5
|
+
HarmBlockThreshold, HarmCategory, Modality, type Part,
|
|
6
6
|
ProminentPeople,
|
|
7
|
-
SafetySetting,
|
|
7
|
+
type SafetySetting, type ThinkingConfig,
|
|
8
8
|
ThinkingLevel,
|
|
9
|
-
Tool
|
|
9
|
+
type Tool
|
|
10
10
|
} from "@google/genai";
|
|
11
11
|
import {
|
|
12
|
-
AIModel, Completion, CompletionChunkObject, CompletionResult, ExecutionOptions,
|
|
13
|
-
ExecutionTokenUsage,
|
|
12
|
+
type AIModel, type Completion, type CompletionChunkObject, type CompletionResult, type ExecutionOptions,
|
|
13
|
+
type ExecutionTokenUsage,
|
|
14
14
|
getConversationMeta,
|
|
15
15
|
getGeminiModelVersion,
|
|
16
16
|
incrementConversationTurn,
|
|
17
17
|
isGeminiModelVersionGte,
|
|
18
|
-
JSONObject,
|
|
19
|
-
PromptSegment, readStreamAsBase64, StatelessExecutionOptions,
|
|
18
|
+
type JSONObject, LlumiverseError, type LlumiverseErrorContext, ModelType, type PromptOptions, PromptRole,
|
|
19
|
+
type PromptSegment, readStreamAsBase64, type StatelessExecutionOptions,
|
|
20
20
|
stripBase64ImagesFromConversation,
|
|
21
21
|
stripHeartbeatsFromConversation,
|
|
22
|
-
ToolDefinition, ToolUse,
|
|
22
|
+
type ToolDefinition, type ToolUse,
|
|
23
23
|
truncateLargeTextInConversation,
|
|
24
24
|
unwrapConversationArray,
|
|
25
|
-
VertexAIGeminiOptions
|
|
25
|
+
type VertexAIGeminiOptions
|
|
26
26
|
} from "@llumiverse/core";
|
|
27
27
|
import { asyncMap } from "@llumiverse/core/async";
|
|
28
|
-
import { GenerateContentPrompt, VertexAIDriver } from "../index.js";
|
|
29
|
-
import { ModelDefinition } from "../models.js";
|
|
28
|
+
import type { GenerateContentPrompt, VertexAIDriver } from "../index.js";
|
|
29
|
+
import type { ModelDefinition } from "../models.js";
|
|
30
30
|
|
|
31
31
|
function supportsStructuredOutput(options: PromptOptions): boolean {
|
|
32
32
|
// Gemini 1.0 Ultra does not support JSON output, 1.0 Pro does.
|
|
@@ -79,13 +79,15 @@ function getGeminiPayload(options: ExecutionOptions, prompt: GenerateContentProm
|
|
|
79
79
|
const tools = getToolDefinitions(options.tools);
|
|
80
80
|
|
|
81
81
|
// When no tools are provided but conversation contains functionCall/functionResponse parts
|
|
82
|
-
// (e.g. checkpoint summary calls), convert them to text to avoid API errors
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
// (e.g. checkpoint summary calls), convert them to text to avoid API errors.
|
|
83
|
+
// Use a local variable to avoid mutating the caller's conversation object.
|
|
84
|
+
let payloadContents = prompt.contents;
|
|
85
|
+
if (!tools && payloadContents) {
|
|
86
|
+
const hasToolParts = payloadContents.some(c =>
|
|
85
87
|
c.parts?.some(p => p.functionCall || p.functionResponse)
|
|
86
88
|
);
|
|
87
89
|
if (hasToolParts) {
|
|
88
|
-
|
|
90
|
+
payloadContents = convertGeminiFunctionPartsToText(payloadContents);
|
|
89
91
|
}
|
|
90
92
|
}
|
|
91
93
|
|
|
@@ -102,6 +104,7 @@ function getGeminiPayload(options: ExecutionOptions, prompt: GenerateContentProm
|
|
|
102
104
|
maxOutputTokens: model_options?.max_tokens,
|
|
103
105
|
stopSequences: model_options?.stop_sequence,
|
|
104
106
|
thinkingConfig: geminiThinkingConfig(options),
|
|
107
|
+
labels: options.labels,
|
|
105
108
|
imageConfig: {
|
|
106
109
|
imageSize: model_options?.image_size,
|
|
107
110
|
aspectRatio: model_options?.image_aspect_ratio,
|
|
@@ -124,7 +127,7 @@ function getGeminiPayload(options: ExecutionOptions, prompt: GenerateContentProm
|
|
|
124
127
|
candidateCount: 1,
|
|
125
128
|
//JSON/Structured output
|
|
126
129
|
responseMimeType: useStructuredOutput ? "application/json" : undefined,
|
|
127
|
-
|
|
130
|
+
responseJsonSchema: useStructuredOutput ? options.result_schema : undefined,
|
|
128
131
|
//Model options
|
|
129
132
|
temperature: model_options?.temperature,
|
|
130
133
|
topP: model_options?.top_p,
|
|
@@ -135,340 +138,16 @@ function getGeminiPayload(options: ExecutionOptions, prompt: GenerateContentProm
|
|
|
135
138
|
frequencyPenalty: model_options?.frequency_penalty,
|
|
136
139
|
seed: model_options?.seed,
|
|
137
140
|
thinkingConfig: geminiThinkingConfig(options),
|
|
141
|
+
labels: options.labels,
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
return {
|
|
141
145
|
model: options.model,
|
|
142
|
-
contents:
|
|
146
|
+
contents: payloadContents,
|
|
143
147
|
config: options.model.toLowerCase().includes("image") ? configNanoBanana : config,
|
|
144
148
|
};
|
|
145
149
|
}
|
|
146
150
|
|
|
147
|
-
/**
|
|
148
|
-
* Convert JSONSchema to Gemini Schema,
|
|
149
|
-
* Make all properties required by default
|
|
150
|
-
* Properties previously marked as optional will be marked as nullable.
|
|
151
|
-
*/
|
|
152
|
-
function parseJSONtoSchema(schema?: JSONSchema, requiredAll = false): Schema {
|
|
153
|
-
if (!schema) {
|
|
154
|
-
return {};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return convertSchema(schema, 0, requiredAll);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Convert JSONSchema type to Gemini Schema Type
|
|
162
|
-
*/
|
|
163
|
-
function convertType(type?: string | string[]): Type | undefined {
|
|
164
|
-
if (!type) return undefined;
|
|
165
|
-
|
|
166
|
-
// Handle single type
|
|
167
|
-
if (typeof type === 'string') {
|
|
168
|
-
switch (type) {
|
|
169
|
-
case 'string': return Type.STRING;
|
|
170
|
-
case 'number': return Type.NUMBER;
|
|
171
|
-
case 'integer': return Type.INTEGER;
|
|
172
|
-
case 'boolean': return Type.BOOLEAN;
|
|
173
|
-
case 'object': return Type.OBJECT;
|
|
174
|
-
case 'array': return Type.ARRAY;
|
|
175
|
-
default: return type as Type; // For unsupported types, return as is
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// For array of types, take the first valid one as the primary type
|
|
180
|
-
// The full set of types will be handled with anyOf
|
|
181
|
-
for (const t of type) {
|
|
182
|
-
const converted = convertType(t);
|
|
183
|
-
if (converted) return converted;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return undefined;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Deep clone and convert the schema from JSONSchema to Gemini Schema
|
|
191
|
-
* @throws {Error} If circular references are detected (max depth exceeded)
|
|
192
|
-
*/
|
|
193
|
-
function convertSchema(jsSchema?: JSONSchema, depth: number = 0, requiredAll = false): Schema {
|
|
194
|
-
// Prevent circular references
|
|
195
|
-
if (depth > 20) {
|
|
196
|
-
throw new Error("Maximum schema depth (20) exceeded. Possible circular reference detected.");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (!jsSchema) return {};
|
|
200
|
-
|
|
201
|
-
// Create new schema object rather than mutating
|
|
202
|
-
const result: Schema = {};
|
|
203
|
-
|
|
204
|
-
// Handle types
|
|
205
|
-
result.type = convertSchemaType(jsSchema);
|
|
206
|
-
|
|
207
|
-
// Handle description
|
|
208
|
-
if (jsSchema.description) {
|
|
209
|
-
result.description = jsSchema.description;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Handle properties and required fields
|
|
213
|
-
if (jsSchema.properties) {
|
|
214
|
-
const propertyResult = convertSchemaProperties(jsSchema, depth + 1, requiredAll);
|
|
215
|
-
Object.assign(result, propertyResult);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// Handle items for arrays
|
|
219
|
-
if (jsSchema.items) {
|
|
220
|
-
result.items = convertSchema(jsSchema.items, depth + 1);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Handle enum values
|
|
224
|
-
if (jsSchema.enum) {
|
|
225
|
-
result.enum = [...jsSchema.enum]; // Create a copy instead of reference
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Copy constraints
|
|
229
|
-
Object.assign(result, extractConstraints(jsSchema));
|
|
230
|
-
|
|
231
|
-
return result;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Convert schema type information, handling anyOf for multiple types
|
|
236
|
-
*/
|
|
237
|
-
function convertSchemaType(jsSchema: JSONSchema): Type | undefined {
|
|
238
|
-
// Handle multiple types using anyOf
|
|
239
|
-
if (jsSchema.type && Array.isArray(jsSchema.type) && jsSchema.type.length > 1) {
|
|
240
|
-
// Since anyOf is an advanced type, we'll return the first valid type
|
|
241
|
-
// and handle the multi-type case separately in the schema
|
|
242
|
-
return convertType(jsSchema.type[0]);
|
|
243
|
-
}
|
|
244
|
-
// Handle single type
|
|
245
|
-
else if (jsSchema.type) {
|
|
246
|
-
return convertType(jsSchema.type);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return undefined;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Handle properties conversion and required fields
|
|
254
|
-
*/
|
|
255
|
-
function convertSchemaProperties(jsSchema: JSONSchema, depth: number, requiredAll: boolean): Partial<Schema> {
|
|
256
|
-
const result: Partial<Schema> = { properties: {} };
|
|
257
|
-
if (jsSchema.required) {
|
|
258
|
-
result.required = [...jsSchema.required]; // Create a copy
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Extract property ordering from the object keys
|
|
262
|
-
const propertyNames = Object.keys(jsSchema.properties || {});
|
|
263
|
-
|
|
264
|
-
// Set property ordering based on the existing order in the schema
|
|
265
|
-
if (propertyNames.length > 0) {
|
|
266
|
-
result.propertyOrdering = propertyNames;
|
|
267
|
-
|
|
268
|
-
if (requiredAll) {
|
|
269
|
-
// Mark all properties as required by default
|
|
270
|
-
// This ensures the model fills all fields
|
|
271
|
-
result.required = propertyNames;
|
|
272
|
-
|
|
273
|
-
// Get the original required properties
|
|
274
|
-
const originalRequired = jsSchema.required || [];
|
|
275
|
-
|
|
276
|
-
// Make previously optional properties nullable since we're marking them as required
|
|
277
|
-
for (const key of propertyNames) {
|
|
278
|
-
const propSchema = jsSchema.properties?.[key];
|
|
279
|
-
if (propSchema && !originalRequired.includes(key)) {
|
|
280
|
-
// Initialize the property if needed
|
|
281
|
-
if (!result.properties![key]) {
|
|
282
|
-
result.properties![key] = {};
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Mark as nullable
|
|
286
|
-
result.properties![key].nullable = true;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Convert each property schema
|
|
293
|
-
for (const [key, value] of Object.entries(jsSchema.properties || {})) {
|
|
294
|
-
if (!result.properties![key]) {
|
|
295
|
-
result.properties![key] = {};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Merge with converted schema
|
|
299
|
-
result.properties![key] = {
|
|
300
|
-
...result.properties![key],
|
|
301
|
-
...convertSchema(value, depth)
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Override with explicit propertyOrdering if present
|
|
306
|
-
if (jsSchema.propertyOrdering) {
|
|
307
|
-
result.propertyOrdering = [...jsSchema.propertyOrdering]; // Create a copy
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return result;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Extract schema constraints (min/max values, formats, etc.)
|
|
315
|
-
*/
|
|
316
|
-
function extractConstraints(jsSchema: JSONSchema): Partial<Schema> {
|
|
317
|
-
const constraints: Partial<Schema> = {};
|
|
318
|
-
|
|
319
|
-
if (jsSchema.minimum !== undefined) constraints.minimum = jsSchema.minimum;
|
|
320
|
-
if (jsSchema.maximum !== undefined) constraints.maximum = jsSchema.maximum;
|
|
321
|
-
if (jsSchema.minLength !== undefined) constraints.minLength = jsSchema.minLength;
|
|
322
|
-
if (jsSchema.maxLength !== undefined) constraints.maxLength = jsSchema.maxLength;
|
|
323
|
-
if (jsSchema.minItems !== undefined) constraints.minItems = jsSchema.minItems;
|
|
324
|
-
if (jsSchema.maxItems !== undefined) constraints.maxItems = jsSchema.maxItems;
|
|
325
|
-
if (jsSchema.nullable !== undefined) constraints.nullable = jsSchema.nullable;
|
|
326
|
-
if (jsSchema.pattern) constraints.pattern = jsSchema.pattern;
|
|
327
|
-
if (jsSchema.format) constraints.format = jsSchema.format;
|
|
328
|
-
if (jsSchema.default !== undefined) constraints.default = jsSchema.default;
|
|
329
|
-
if (jsSchema.example !== undefined) constraints.example = jsSchema.example;
|
|
330
|
-
|
|
331
|
-
return constraints;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Check if a value is empty (null, undefined, empty string, empty array, empty object)
|
|
336
|
-
* @param value The value to check
|
|
337
|
-
* @returns True if the value is considered empty
|
|
338
|
-
*/
|
|
339
|
-
function isEmpty(value: any): boolean {
|
|
340
|
-
if (value === null || value === undefined) {
|
|
341
|
-
return true;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (typeof value === 'string' && value.trim() === '') {
|
|
345
|
-
return true;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (Array.isArray(value) && value.length === 0) {
|
|
349
|
-
return true;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Check for empty object (no own enumerable properties)
|
|
353
|
-
if (typeof value === 'object' && Object.keys(value).length === 0) {
|
|
354
|
-
return true;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Check for array of empty objects
|
|
358
|
-
if (Array.isArray(value) && value.every(item => isEmpty(item))) {
|
|
359
|
-
return true;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
return false;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// No array cleaning function needed as we're only working with JSONObjects
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Clean up the JSON result by removing empty values for optional fields
|
|
369
|
-
* Uses immutable patterns to create a new Content object rather than modifying the original
|
|
370
|
-
* @param content The original content from Gemini
|
|
371
|
-
* @param result_schema The JSON schema to use for cleaning
|
|
372
|
-
* @returns A new Content object with cleaned JSON text
|
|
373
|
-
*/
|
|
374
|
-
function cleanEmptyFieldsContent(content: Content, result_schema?: JSONSchema): Content {
|
|
375
|
-
// If no schema provided, return original content
|
|
376
|
-
if (!result_schema) {
|
|
377
|
-
return content;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Create a new content object (shallow copy)
|
|
381
|
-
const cleanedContent: Content = { ...content };
|
|
382
|
-
|
|
383
|
-
// Create a new parts array if it exists
|
|
384
|
-
if (cleanedContent.parts) {
|
|
385
|
-
cleanedContent.parts = cleanedContent.parts.map(part => {
|
|
386
|
-
// Only process parts with text
|
|
387
|
-
if (!part.text) {
|
|
388
|
-
return part; // Return unchanged if no text
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Create a new part object
|
|
392
|
-
const newPart = { ...part };
|
|
393
|
-
|
|
394
|
-
try {
|
|
395
|
-
// Parse JSON, clean it based on schema, then stringify
|
|
396
|
-
const jsonText = JSON.parse(part.text);
|
|
397
|
-
// Skip cleaning if not an object
|
|
398
|
-
if (typeof jsonText === 'object' && jsonText !== null && !Array.isArray(jsonText)) {
|
|
399
|
-
const cleanedJson = removeEmptyFields(jsonText, result_schema);
|
|
400
|
-
newPart.text = JSON.stringify(cleanedJson);
|
|
401
|
-
} else {
|
|
402
|
-
// Keep original if not an object (string, number, array, etc.)
|
|
403
|
-
newPart.text = part.text;
|
|
404
|
-
}
|
|
405
|
-
} catch (e) {
|
|
406
|
-
// On error, keep the original text
|
|
407
|
-
console.warn("Error parsing Gemini output to JSON in part:", e);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
return newPart;
|
|
411
|
-
});
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
return cleanedContent;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Removes empty optional fields from the JSON result based on the provided schema
|
|
419
|
-
* @param object The object to clean
|
|
420
|
-
* @param schema The JSON schema to use for cleaning
|
|
421
|
-
* @returns A new object with empty optional fields removed
|
|
422
|
-
*/
|
|
423
|
-
function removeEmptyFields(object: JSONObject | any[], schema: JSONSchema): JSONObject | any[] {
|
|
424
|
-
if (!object) {
|
|
425
|
-
return object
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
if (Array.isArray(object)) {
|
|
429
|
-
return removeEmptyJSONArray(object, schema);
|
|
430
|
-
}
|
|
431
|
-
if (typeof object == 'object' || object === null) {
|
|
432
|
-
return removeEmptyJSONObject(object, schema);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
return object;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function removeEmptyJSONObject(object: JSONObject, schema: JSONSchema): JSONObject {
|
|
439
|
-
// Get the original required properties from schema
|
|
440
|
-
const requiredProps = schema.required || [];
|
|
441
|
-
const cleanedResult: JSONObject = { ...object };
|
|
442
|
-
|
|
443
|
-
// Process each property
|
|
444
|
-
for (const [key, value] of Object.entries(object)) {
|
|
445
|
-
const isRequired = requiredProps.includes(key);
|
|
446
|
-
const propSchema = schema.properties?.[key];
|
|
447
|
-
|
|
448
|
-
// Recursively clean nested objects based on their schema
|
|
449
|
-
cleanedResult[key] = removeEmptyFields(value as JSONObject, propSchema ?? {});
|
|
450
|
-
|
|
451
|
-
if (isEmpty(value)) {
|
|
452
|
-
if (isRequired) {
|
|
453
|
-
continue; // Keep required fields even if empty
|
|
454
|
-
} else {
|
|
455
|
-
delete cleanedResult[key]; // Remove empty optional fields
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
return cleanedResult;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function removeEmptyJSONArray(array: any[], schema: JSONSchema): any[] {
|
|
464
|
-
const cleanedArray = array.map(item => {
|
|
465
|
-
return removeEmptyFields(item, schema);
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
// Filter out empty objects from the array
|
|
469
|
-
return cleanedArray.filter(item => !isEmpty(item));
|
|
470
|
-
}
|
|
471
|
-
|
|
472
151
|
/**
|
|
473
152
|
* Collect all parts (text and images) from content in order.
|
|
474
153
|
* This preserves the original ordering of text and image parts.
|
|
@@ -569,7 +248,7 @@ function geminiThinkingBudget(option: StatelessExecutionOptions) {
|
|
|
569
248
|
}
|
|
570
249
|
// Set minimum thinking level by default.
|
|
571
250
|
// Docs: https://ai.google.dev/gemini-api/docs/thinking#set-budget
|
|
572
|
-
if (getGeminiModelVersion(option.model)
|
|
251
|
+
if (getGeminiModelVersion(option.model) === '2.5') {
|
|
573
252
|
if (option.model.includes("pro")) {
|
|
574
253
|
return 128;
|
|
575
254
|
}
|
|
@@ -595,6 +274,13 @@ function geminiThinkingConfig(option: StatelessExecutionOptions): ThinkingConfig
|
|
|
595
274
|
// Docs: https://ai.google.dev/gemini-api/docs/thinking#set-budget
|
|
596
275
|
// https://docs.cloud.google.com/vertex-ai/generative-ai/docs/thinking
|
|
597
276
|
if (isGeminiModelVersionGte(option.model, '3.0')) {
|
|
277
|
+
if (option.model.includes("gemini-3-pro-image")) {
|
|
278
|
+
// Does not support thinking level.
|
|
279
|
+
return {
|
|
280
|
+
includeThoughts: include_thoughts,
|
|
281
|
+
thinkingBudget: -1
|
|
282
|
+
};
|
|
283
|
+
}
|
|
598
284
|
return {
|
|
599
285
|
includeThoughts: include_thoughts,
|
|
600
286
|
thinkingLevel: ThinkingLevel.LOW
|
|
@@ -623,31 +309,6 @@ export class GeminiModelDefinition implements ModelDefinition<GenerateContentPro
|
|
|
623
309
|
} satisfies AIModel;
|
|
624
310
|
}
|
|
625
311
|
|
|
626
|
-
preValidationProcessing(result: Completion, options: ExecutionOptions): { result: Completion, options: ExecutionOptions } {
|
|
627
|
-
// Guard clause, if no result_schema, error, or tool use, skip processing
|
|
628
|
-
if (!options.result_schema || !result.result || result.tool_use || result.error) {
|
|
629
|
-
return { result, options };
|
|
630
|
-
}
|
|
631
|
-
try {
|
|
632
|
-
// Extract text content for JSON processing - only process first text result
|
|
633
|
-
const textResult = result.result.find(r => r.type === 'text')?.value;
|
|
634
|
-
if (textResult) {
|
|
635
|
-
const jsonResult = JSON.parse(textResult);
|
|
636
|
-
const cleanedJson = JSON.stringify(removeEmptyFields(jsonResult, options.result_schema));
|
|
637
|
-
// Replace the text result with cleaned version
|
|
638
|
-
result.result = result.result.map(r =>
|
|
639
|
-
r.type === 'text' ? { ...r, value: cleanedJson } : r
|
|
640
|
-
);
|
|
641
|
-
}
|
|
642
|
-
return { result, options };
|
|
643
|
-
} catch (error) {
|
|
644
|
-
// Log error during processing but don't fail the completion
|
|
645
|
-
console.warn('Error during Gemini JSON pre-validation: ', error);
|
|
646
|
-
// Return original result if cleanup fails
|
|
647
|
-
return { result, options };
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
312
|
async createPrompt(_driver: VertexAIDriver, segments: PromptSegment[], options: ExecutionOptions): Promise<GenerateContentPrompt> {
|
|
652
313
|
const splits = options.model.split("/");
|
|
653
314
|
const modelName = splits[splits.length - 1];
|
|
@@ -779,14 +440,19 @@ export class GeminiModelDefinition implements ModelDefinition<GenerateContentPro
|
|
|
779
440
|
if (!usageMetadata || !usageMetadata.totalTokenCount) {
|
|
780
441
|
return {};
|
|
781
442
|
}
|
|
782
|
-
const tokenUsage: ExecutionTokenUsage = {
|
|
443
|
+
const tokenUsage: ExecutionTokenUsage = {
|
|
444
|
+
total: usageMetadata.totalTokenCount,
|
|
445
|
+
prompt: usageMetadata.promptTokenCount,
|
|
446
|
+
prompt_cached: usageMetadata.cachedContentTokenCount ?? undefined,
|
|
447
|
+
prompt_new: (usageMetadata.promptTokenCount ?? 0) - (usageMetadata.cachedContentTokenCount ?? 0),
|
|
448
|
+
};
|
|
783
449
|
|
|
784
450
|
//Output/Response side
|
|
785
451
|
tokenUsage.result = (usageMetadata.candidatesTokenCount ?? 0)
|
|
786
452
|
+ (usageMetadata.thoughtsTokenCount ?? 0)
|
|
787
453
|
+ (usageMetadata.toolUsePromptTokenCount ?? 0);
|
|
788
454
|
|
|
789
|
-
if ((tokenUsage.total ?? 0)
|
|
455
|
+
if ((tokenUsage.total ?? 0) !== (tokenUsage.prompt ?? 0) + tokenUsage.result) {
|
|
790
456
|
console.warn("[VertexAI] Gemini token usage mismatch: total does not equal prompt + result", {
|
|
791
457
|
total: tokenUsage.total,
|
|
792
458
|
prompt: tokenUsage.prompt,
|
|
@@ -827,7 +493,8 @@ export class GeminiModelDefinition implements ModelDefinition<GenerateContentPro
|
|
|
827
493
|
region = "global"; // Gemini Flash Image only available in global region, this is for nano-banana model
|
|
828
494
|
}
|
|
829
495
|
|
|
830
|
-
const
|
|
496
|
+
const model_options = options.model_options as VertexAIGeminiOptions | undefined;
|
|
497
|
+
const client = driver.getGoogleGenAIClient(region, model_options?.flex ?? false);
|
|
831
498
|
|
|
832
499
|
const payload = getGeminiPayload(options, prompt);
|
|
833
500
|
const response = await client.models.generateContent(payload);
|
|
@@ -863,11 +530,8 @@ export class GeminiModelDefinition implements ModelDefinition<GenerateContentPro
|
|
|
863
530
|
`Model tried to call undeclared tool(s): ${tool_use.map(t => t.tool_name).join(', ')}`);
|
|
864
531
|
}
|
|
865
532
|
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
// Collect all parts in order (text and images)
|
|
869
|
-
result = extractCompletionResults(cleanedContent);
|
|
870
|
-
conversation = updateConversation(conversation, [cleanedContent]);
|
|
533
|
+
result = extractCompletionResults(content);
|
|
534
|
+
conversation = updateConversation(conversation, [content]);
|
|
871
535
|
}
|
|
872
536
|
}
|
|
873
537
|
|
|
@@ -937,7 +601,8 @@ export class GeminiModelDefinition implements ModelDefinition<GenerateContentPro
|
|
|
937
601
|
region = "global"; // Gemini Flash Image only available in global region, this is for nano-banana model
|
|
938
602
|
}
|
|
939
603
|
|
|
940
|
-
const
|
|
604
|
+
const model_options = options.model_options as VertexAIGeminiOptions | undefined;
|
|
605
|
+
const client = driver.getGoogleGenAIClient(region, model_options?.flex ?? false);
|
|
941
606
|
|
|
942
607
|
const payload = getGeminiPayload(options, prompt);
|
|
943
608
|
const response = await client.models.generateContentStream(payload);
|
|
@@ -1174,21 +839,13 @@ function getToolDefinitions(tools: ToolDefinition[] | undefined | null): Tool |
|
|
|
1174
839
|
}
|
|
1175
840
|
|
|
1176
841
|
function getToolFunction(tool: ToolDefinition): FunctionDeclaration {
|
|
1177
|
-
// If input_schema is a string, parse it; if it's already an object, use it directly
|
|
1178
|
-
let toolSchema: Schema | undefined;
|
|
1179
|
-
|
|
1180
|
-
// Using a try-catch for safety, as the input_schema might not be a valid JSONSchema
|
|
1181
|
-
try {
|
|
1182
|
-
toolSchema = parseJSONtoSchema(tool.input_schema as JSONSchema, false);
|
|
1183
|
-
}
|
|
1184
|
-
catch (e) {
|
|
1185
|
-
toolSchema = { ...tool.input_schema, type: Type.OBJECT } as unknown as Schema;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
842
|
return {
|
|
1189
843
|
name: tool.name,
|
|
1190
844
|
description: tool.description,
|
|
1191
|
-
|
|
845
|
+
// Pass the input_schema directly as a JSON Schema object.
|
|
846
|
+
// parametersJsonSchema accepts standard JSON Schema and is mutually exclusive
|
|
847
|
+
// with the legacy parameters field (which required a proprietary Gemini Schema type).
|
|
848
|
+
parametersJsonSchema: tool.input_schema,
|
|
1192
849
|
};
|
|
1193
850
|
}
|
|
1194
851
|
|