@lobehub/chat 1.138.3 → 1.138.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/packages/database/src/repositories/aiInfra/index.test.ts +656 -0
- package/packages/model-runtime/src/core/contextBuilders/google.test.ts +585 -0
- package/packages/model-runtime/src/core/contextBuilders/google.ts +201 -0
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.test.ts +191 -179
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +305 -47
- package/packages/model-runtime/src/providers/anthropic/generateObject.test.ts +93 -84
- package/packages/model-runtime/src/providers/anthropic/generateObject.ts +3 -3
- package/packages/model-runtime/src/providers/google/generateObject.test.ts +588 -83
- package/packages/model-runtime/src/providers/google/generateObject.ts +104 -6
- package/packages/model-runtime/src/providers/google/index.test.ts +0 -395
- package/packages/model-runtime/src/providers/google/index.ts +28 -194
- package/packages/model-runtime/src/providers/openai/index.test.ts +18 -17
- package/packages/model-runtime/src/types/structureOutput.ts +3 -4
- package/packages/types/src/aiChat.ts +0 -1
- package/src/server/routers/lambda/aiChat.ts +1 -2
|
@@ -2,145 +2,174 @@
|
|
|
2
2
|
import { Type as SchemaType } from '@google/genai';
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
convertOpenAISchemaToGoogleSchema,
|
|
7
|
+
createGoogleGenerateObject,
|
|
8
|
+
createGoogleGenerateObjectWithTools,
|
|
9
|
+
} from './generateObject';
|
|
6
10
|
|
|
7
11
|
describe('Google generateObject', () => {
|
|
8
12
|
describe('convertOpenAISchemaToGoogleSchema', () => {
|
|
9
13
|
it('should convert basic types correctly', () => {
|
|
10
14
|
const openAISchema = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
name: 'person',
|
|
16
|
+
schema: {
|
|
17
|
+
properties: {
|
|
18
|
+
age: { type: 'number' },
|
|
19
|
+
count: { type: 'integer' },
|
|
20
|
+
isActive: { type: 'boolean' },
|
|
21
|
+
name: { type: 'string' },
|
|
22
|
+
},
|
|
23
|
+
type: 'object' as const,
|
|
17
24
|
},
|
|
18
25
|
};
|
|
19
26
|
|
|
20
27
|
const result = convertOpenAISchemaToGoogleSchema(openAISchema);
|
|
21
28
|
|
|
22
29
|
expect(result).toEqual({
|
|
23
|
-
type: SchemaType.OBJECT,
|
|
24
30
|
properties: {
|
|
25
|
-
name: { type: SchemaType.STRING },
|
|
26
31
|
age: { type: SchemaType.NUMBER },
|
|
27
|
-
isActive: { type: SchemaType.BOOLEAN },
|
|
28
32
|
count: { type: SchemaType.INTEGER },
|
|
33
|
+
isActive: { type: SchemaType.BOOLEAN },
|
|
34
|
+
name: { type: SchemaType.STRING },
|
|
29
35
|
},
|
|
36
|
+
type: SchemaType.OBJECT,
|
|
30
37
|
});
|
|
31
38
|
});
|
|
32
39
|
|
|
33
40
|
it('should convert array schemas correctly', () => {
|
|
34
41
|
const openAISchema = {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
type: 'object',
|
|
42
|
+
name: 'recipes',
|
|
43
|
+
schema: {
|
|
38
44
|
properties: {
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
recipes: {
|
|
46
|
+
items: {
|
|
47
|
+
properties: {
|
|
48
|
+
ingredients: {
|
|
49
|
+
items: { type: 'string' },
|
|
50
|
+
type: 'array',
|
|
51
|
+
},
|
|
52
|
+
recipeName: { type: 'string' },
|
|
53
|
+
},
|
|
54
|
+
propertyOrdering: ['recipeName', 'ingredients'],
|
|
55
|
+
type: 'object',
|
|
56
|
+
},
|
|
41
57
|
type: 'array',
|
|
42
|
-
items: { type: 'string' },
|
|
43
58
|
},
|
|
44
59
|
},
|
|
45
|
-
|
|
60
|
+
type: 'object' as const,
|
|
46
61
|
},
|
|
47
62
|
};
|
|
48
63
|
|
|
49
64
|
const result = convertOpenAISchemaToGoogleSchema(openAISchema);
|
|
50
65
|
|
|
51
66
|
expect(result).toEqual({
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
properties: {
|
|
68
|
+
recipes: {
|
|
69
|
+
items: {
|
|
70
|
+
properties: {
|
|
71
|
+
ingredients: {
|
|
72
|
+
items: { type: SchemaType.STRING },
|
|
73
|
+
type: SchemaType.ARRAY,
|
|
74
|
+
},
|
|
75
|
+
recipeName: { type: SchemaType.STRING },
|
|
76
|
+
},
|
|
77
|
+
propertyOrdering: ['recipeName', 'ingredients'],
|
|
78
|
+
type: SchemaType.OBJECT,
|
|
60
79
|
},
|
|
80
|
+
type: SchemaType.ARRAY,
|
|
61
81
|
},
|
|
62
|
-
propertyOrdering: ['recipeName', 'ingredients'],
|
|
63
82
|
},
|
|
83
|
+
type: SchemaType.OBJECT,
|
|
64
84
|
});
|
|
65
85
|
});
|
|
66
86
|
|
|
67
87
|
it('should handle nested objects', () => {
|
|
68
88
|
const openAISchema = {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
89
|
+
name: 'user_data',
|
|
90
|
+
schema: {
|
|
91
|
+
properties: {
|
|
92
|
+
user: {
|
|
93
|
+
properties: {
|
|
94
|
+
profile: {
|
|
95
|
+
properties: {
|
|
96
|
+
preferences: {
|
|
97
|
+
items: { type: 'string' },
|
|
98
|
+
type: 'array',
|
|
99
|
+
},
|
|
80
100
|
},
|
|
101
|
+
type: 'object',
|
|
81
102
|
},
|
|
82
103
|
},
|
|
104
|
+
type: 'object',
|
|
83
105
|
},
|
|
84
106
|
},
|
|
107
|
+
type: 'object' as const,
|
|
85
108
|
},
|
|
86
109
|
};
|
|
87
110
|
|
|
88
111
|
const result = convertOpenAISchemaToGoogleSchema(openAISchema);
|
|
89
112
|
|
|
90
113
|
expect(result).toEqual({
|
|
91
|
-
type: SchemaType.OBJECT,
|
|
92
114
|
properties: {
|
|
93
115
|
user: {
|
|
94
|
-
type: SchemaType.OBJECT,
|
|
95
116
|
properties: {
|
|
96
117
|
profile: {
|
|
97
|
-
type: SchemaType.OBJECT,
|
|
98
118
|
properties: {
|
|
99
119
|
preferences: {
|
|
100
|
-
type: SchemaType.ARRAY,
|
|
101
120
|
items: { type: SchemaType.STRING },
|
|
121
|
+
type: SchemaType.ARRAY,
|
|
102
122
|
},
|
|
103
123
|
},
|
|
124
|
+
type: SchemaType.OBJECT,
|
|
104
125
|
},
|
|
105
126
|
},
|
|
127
|
+
type: SchemaType.OBJECT,
|
|
106
128
|
},
|
|
107
129
|
},
|
|
130
|
+
type: SchemaType.OBJECT,
|
|
108
131
|
});
|
|
109
132
|
});
|
|
110
133
|
|
|
111
134
|
it('should preserve additional properties like description, enum, required', () => {
|
|
112
135
|
const openAISchema = {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
136
|
+
name: 'person',
|
|
137
|
+
schema: {
|
|
138
|
+
description: 'A person object',
|
|
139
|
+
properties: {
|
|
140
|
+
status: {
|
|
141
|
+
description: 'The status of the person',
|
|
142
|
+
enum: ['active', 'inactive'],
|
|
143
|
+
type: 'string',
|
|
144
|
+
},
|
|
120
145
|
},
|
|
121
|
-
|
|
122
|
-
|
|
146
|
+
required: ['status'],
|
|
147
|
+
type: 'object' as const,
|
|
148
|
+
} as any,
|
|
123
149
|
};
|
|
124
150
|
|
|
125
151
|
const result = convertOpenAISchemaToGoogleSchema(openAISchema);
|
|
126
152
|
|
|
127
153
|
expect(result).toEqual({
|
|
128
|
-
type: SchemaType.OBJECT,
|
|
129
154
|
description: 'A person object',
|
|
130
155
|
properties: {
|
|
131
156
|
status: {
|
|
132
|
-
type: SchemaType.STRING,
|
|
133
|
-
enum: ['active', 'inactive'],
|
|
134
157
|
description: 'The status of the person',
|
|
158
|
+
enum: ['active', 'inactive'],
|
|
159
|
+
type: SchemaType.STRING,
|
|
135
160
|
},
|
|
136
161
|
},
|
|
137
162
|
required: ['status'],
|
|
163
|
+
type: SchemaType.OBJECT,
|
|
138
164
|
});
|
|
139
165
|
});
|
|
140
166
|
|
|
141
167
|
it('should handle unknown types by defaulting to STRING', () => {
|
|
142
168
|
const openAISchema = {
|
|
143
|
-
|
|
169
|
+
name: 'test',
|
|
170
|
+
schema: {
|
|
171
|
+
type: 'unknown-type' as any,
|
|
172
|
+
} as any,
|
|
144
173
|
};
|
|
145
174
|
|
|
146
175
|
const result = convertOpenAISchemaToGoogleSchema(openAISchema);
|
|
@@ -161,15 +190,18 @@ describe('Google generateObject', () => {
|
|
|
161
190
|
},
|
|
162
191
|
};
|
|
163
192
|
|
|
164
|
-
const contents = [{
|
|
193
|
+
const contents = [{ parts: [{ text: 'Generate a person object' }], role: 'user' }];
|
|
165
194
|
|
|
166
195
|
const payload = {
|
|
167
196
|
contents,
|
|
197
|
+
model: 'gemini-2.5-flash',
|
|
168
198
|
schema: {
|
|
169
|
-
|
|
170
|
-
|
|
199
|
+
name: 'person',
|
|
200
|
+
schema: {
|
|
201
|
+
properties: { age: { type: 'number' }, name: { type: 'string' } },
|
|
202
|
+
type: 'object' as const,
|
|
203
|
+
},
|
|
171
204
|
},
|
|
172
|
-
model: 'gemini-2.5-flash',
|
|
173
205
|
};
|
|
174
206
|
|
|
175
207
|
const result = await createGoogleGenerateObject(mockClient as any, payload);
|
|
@@ -178,11 +210,11 @@ describe('Google generateObject', () => {
|
|
|
178
210
|
config: expect.objectContaining({
|
|
179
211
|
responseMimeType: 'application/json',
|
|
180
212
|
responseSchema: expect.objectContaining({
|
|
181
|
-
type: SchemaType.OBJECT,
|
|
182
213
|
properties: expect.objectContaining({
|
|
183
|
-
name: { type: SchemaType.STRING },
|
|
184
214
|
age: { type: SchemaType.NUMBER },
|
|
215
|
+
name: { type: SchemaType.STRING },
|
|
185
216
|
}),
|
|
217
|
+
type: SchemaType.OBJECT,
|
|
186
218
|
}),
|
|
187
219
|
safetySettings: expect.any(Array),
|
|
188
220
|
}),
|
|
@@ -190,7 +222,7 @@ describe('Google generateObject', () => {
|
|
|
190
222
|
model: 'gemini-2.5-flash',
|
|
191
223
|
});
|
|
192
224
|
|
|
193
|
-
expect(result).toEqual({ name: 'John'
|
|
225
|
+
expect(result).toEqual({ age: 30, name: 'John' });
|
|
194
226
|
});
|
|
195
227
|
|
|
196
228
|
it('should handle options correctly', async () => {
|
|
@@ -202,12 +234,18 @@ describe('Google generateObject', () => {
|
|
|
202
234
|
},
|
|
203
235
|
};
|
|
204
236
|
|
|
205
|
-
const contents = [{
|
|
237
|
+
const contents = [{ parts: [{ text: 'Generate status' }], role: 'user' }];
|
|
206
238
|
|
|
207
239
|
const payload = {
|
|
208
240
|
contents,
|
|
209
|
-
schema: { type: 'object', properties: { status: { type: 'string' } } },
|
|
210
241
|
model: 'gemini-2.5-flash',
|
|
242
|
+
schema: {
|
|
243
|
+
name: 'status',
|
|
244
|
+
schema: {
|
|
245
|
+
properties: { status: { type: 'string' } },
|
|
246
|
+
type: 'object' as const,
|
|
247
|
+
},
|
|
248
|
+
},
|
|
211
249
|
};
|
|
212
250
|
|
|
213
251
|
const options = {
|
|
@@ -221,10 +259,10 @@ describe('Google generateObject', () => {
|
|
|
221
259
|
abortSignal: options.signal,
|
|
222
260
|
responseMimeType: 'application/json',
|
|
223
261
|
responseSchema: expect.objectContaining({
|
|
224
|
-
type: SchemaType.OBJECT,
|
|
225
262
|
properties: expect.objectContaining({
|
|
226
263
|
status: { type: SchemaType.STRING },
|
|
227
264
|
}),
|
|
265
|
+
type: SchemaType.OBJECT,
|
|
228
266
|
}),
|
|
229
267
|
}),
|
|
230
268
|
contents,
|
|
@@ -248,8 +286,14 @@ describe('Google generateObject', () => {
|
|
|
248
286
|
|
|
249
287
|
const payload = {
|
|
250
288
|
contents,
|
|
251
|
-
schema: { type: 'object' },
|
|
252
289
|
model: 'gemini-2.5-flash',
|
|
290
|
+
schema: {
|
|
291
|
+
name: 'test',
|
|
292
|
+
schema: {
|
|
293
|
+
properties: {},
|
|
294
|
+
type: 'object' as const,
|
|
295
|
+
},
|
|
296
|
+
},
|
|
253
297
|
};
|
|
254
298
|
|
|
255
299
|
const result = await createGoogleGenerateObject(mockClient as any, payload);
|
|
@@ -273,31 +317,37 @@ describe('Google generateObject', () => {
|
|
|
273
317
|
|
|
274
318
|
const payload = {
|
|
275
319
|
contents,
|
|
320
|
+
model: 'gemini-2.5-flash',
|
|
276
321
|
schema: {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
type: 'object',
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
322
|
+
name: 'user_data',
|
|
323
|
+
schema: {
|
|
324
|
+
properties: {
|
|
325
|
+
metadata: { type: 'object' },
|
|
326
|
+
user: {
|
|
327
|
+
properties: {
|
|
328
|
+
name: { type: 'string' },
|
|
329
|
+
profile: {
|
|
330
|
+
properties: {
|
|
331
|
+
age: { type: 'number' },
|
|
332
|
+
preferences: { items: { type: 'string' }, type: 'array' },
|
|
333
|
+
},
|
|
334
|
+
type: 'object',
|
|
288
335
|
},
|
|
289
336
|
},
|
|
337
|
+
type: 'object',
|
|
290
338
|
},
|
|
291
339
|
},
|
|
292
|
-
|
|
340
|
+
type: 'object' as const,
|
|
293
341
|
},
|
|
294
342
|
},
|
|
295
|
-
model: 'gemini-2.5-flash',
|
|
296
343
|
};
|
|
297
344
|
|
|
298
345
|
const result = await createGoogleGenerateObject(mockClient as any, payload);
|
|
299
346
|
|
|
300
347
|
expect(result).toEqual({
|
|
348
|
+
metadata: {
|
|
349
|
+
created: '2024-01-01',
|
|
350
|
+
},
|
|
301
351
|
user: {
|
|
302
352
|
name: 'Alice',
|
|
303
353
|
profile: {
|
|
@@ -305,9 +355,6 @@ describe('Google generateObject', () => {
|
|
|
305
355
|
preferences: ['music', 'sports'],
|
|
306
356
|
},
|
|
307
357
|
},
|
|
308
|
-
metadata: {
|
|
309
|
-
created: '2024-01-01',
|
|
310
|
-
},
|
|
311
358
|
});
|
|
312
359
|
});
|
|
313
360
|
|
|
@@ -324,8 +371,14 @@ describe('Google generateObject', () => {
|
|
|
324
371
|
|
|
325
372
|
const payload = {
|
|
326
373
|
contents,
|
|
327
|
-
schema: { type: 'object' },
|
|
328
374
|
model: 'gemini-2.5-flash',
|
|
375
|
+
schema: {
|
|
376
|
+
name: 'test',
|
|
377
|
+
schema: {
|
|
378
|
+
properties: {},
|
|
379
|
+
type: 'object' as const,
|
|
380
|
+
},
|
|
381
|
+
},
|
|
329
382
|
};
|
|
330
383
|
|
|
331
384
|
await expect(createGoogleGenerateObject(mockClient as any, payload)).rejects.toThrow();
|
|
@@ -345,8 +398,14 @@ describe('Google generateObject', () => {
|
|
|
345
398
|
|
|
346
399
|
const payload = {
|
|
347
400
|
contents,
|
|
348
|
-
schema: { type: 'object' },
|
|
349
401
|
model: 'gemini-2.5-flash',
|
|
402
|
+
schema: {
|
|
403
|
+
name: 'test',
|
|
404
|
+
schema: {
|
|
405
|
+
properties: {},
|
|
406
|
+
type: 'object' as const,
|
|
407
|
+
},
|
|
408
|
+
},
|
|
350
409
|
};
|
|
351
410
|
|
|
352
411
|
const options = {
|
|
@@ -358,4 +417,450 @@ describe('Google generateObject', () => {
|
|
|
358
417
|
).rejects.toThrow();
|
|
359
418
|
});
|
|
360
419
|
});
|
|
420
|
+
|
|
421
|
+
describe('createGoogleGenerateObjectWithTools', () => {
|
|
422
|
+
it('should return function calls on successful API call with tools', async () => {
|
|
423
|
+
const mockClient = {
|
|
424
|
+
models: {
|
|
425
|
+
generateContent: vi.fn().mockResolvedValue({
|
|
426
|
+
candidates: [
|
|
427
|
+
{
|
|
428
|
+
content: {
|
|
429
|
+
parts: [
|
|
430
|
+
{
|
|
431
|
+
functionCall: {
|
|
432
|
+
args: { city: 'New York', unit: 'celsius' },
|
|
433
|
+
name: 'get_weather',
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
],
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
}),
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const contents = [{ parts: [{ text: 'What is the weather in New York?' }], role: 'user' }];
|
|
445
|
+
|
|
446
|
+
const payload = {
|
|
447
|
+
contents,
|
|
448
|
+
model: 'gemini-2.5-flash',
|
|
449
|
+
tools: [
|
|
450
|
+
{
|
|
451
|
+
function: {
|
|
452
|
+
description: 'Get weather information',
|
|
453
|
+
name: 'get_weather',
|
|
454
|
+
parameters: {
|
|
455
|
+
properties: {
|
|
456
|
+
city: { type: 'string' },
|
|
457
|
+
unit: { type: 'string' },
|
|
458
|
+
},
|
|
459
|
+
required: ['city'],
|
|
460
|
+
type: 'object' as const,
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
type: 'function' as const,
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const result = await createGoogleGenerateObjectWithTools(mockClient as any, payload);
|
|
469
|
+
|
|
470
|
+
expect(mockClient.models.generateContent).toHaveBeenCalledWith({
|
|
471
|
+
config: expect.objectContaining({
|
|
472
|
+
safetySettings: expect.any(Array),
|
|
473
|
+
toolConfig: {
|
|
474
|
+
functionCallingConfig: {
|
|
475
|
+
mode: 'ANY',
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
tools: [
|
|
479
|
+
{
|
|
480
|
+
functionDeclarations: [
|
|
481
|
+
{
|
|
482
|
+
description: 'Get weather information',
|
|
483
|
+
name: 'get_weather',
|
|
484
|
+
parameters: {
|
|
485
|
+
description: undefined,
|
|
486
|
+
properties: {
|
|
487
|
+
city: { type: 'string' },
|
|
488
|
+
unit: { type: 'string' },
|
|
489
|
+
},
|
|
490
|
+
required: ['city'],
|
|
491
|
+
type: SchemaType.OBJECT,
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
],
|
|
495
|
+
},
|
|
496
|
+
],
|
|
497
|
+
}),
|
|
498
|
+
contents,
|
|
499
|
+
model: 'gemini-2.5-flash',
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
expect(result).toEqual([
|
|
503
|
+
{ arguments: { city: 'New York', unit: 'celsius' }, name: 'get_weather' },
|
|
504
|
+
]);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('should handle multiple function calls', async () => {
|
|
508
|
+
const mockClient = {
|
|
509
|
+
models: {
|
|
510
|
+
generateContent: vi.fn().mockResolvedValue({
|
|
511
|
+
candidates: [
|
|
512
|
+
{
|
|
513
|
+
content: {
|
|
514
|
+
parts: [
|
|
515
|
+
{
|
|
516
|
+
functionCall: {
|
|
517
|
+
args: { city: 'New York', unit: 'celsius' },
|
|
518
|
+
name: 'get_weather',
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
functionCall: {
|
|
523
|
+
args: { timezone: 'America/New_York' },
|
|
524
|
+
name: 'get_time',
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
],
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
],
|
|
531
|
+
}),
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const contents: any[] = [];
|
|
536
|
+
|
|
537
|
+
const payload = {
|
|
538
|
+
contents,
|
|
539
|
+
model: 'gemini-2.5-flash',
|
|
540
|
+
tools: [
|
|
541
|
+
{
|
|
542
|
+
function: {
|
|
543
|
+
description: 'Get weather information',
|
|
544
|
+
name: 'get_weather',
|
|
545
|
+
parameters: {
|
|
546
|
+
properties: {
|
|
547
|
+
city: { type: 'string' },
|
|
548
|
+
unit: { type: 'string' },
|
|
549
|
+
},
|
|
550
|
+
required: ['city'],
|
|
551
|
+
type: 'object' as const,
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
type: 'function' as const,
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
function: {
|
|
558
|
+
description: 'Get current time',
|
|
559
|
+
name: 'get_time',
|
|
560
|
+
parameters: {
|
|
561
|
+
properties: {
|
|
562
|
+
timezone: { type: 'string' },
|
|
563
|
+
},
|
|
564
|
+
required: ['timezone'],
|
|
565
|
+
type: 'object' as const,
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
type: 'function' as const,
|
|
569
|
+
},
|
|
570
|
+
],
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const result = await createGoogleGenerateObjectWithTools(mockClient as any, payload);
|
|
574
|
+
|
|
575
|
+
expect(result).toEqual([
|
|
576
|
+
{ arguments: { city: 'New York', unit: 'celsius' }, name: 'get_weather' },
|
|
577
|
+
{ arguments: { timezone: 'America/New_York' }, name: 'get_time' },
|
|
578
|
+
]);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('should handle options correctly', async () => {
|
|
582
|
+
const mockClient = {
|
|
583
|
+
models: {
|
|
584
|
+
generateContent: vi.fn().mockResolvedValue({
|
|
585
|
+
candidates: [
|
|
586
|
+
{
|
|
587
|
+
content: {
|
|
588
|
+
parts: [
|
|
589
|
+
{
|
|
590
|
+
functionCall: {
|
|
591
|
+
args: { a: 5, b: 3, operation: 'add' },
|
|
592
|
+
name: 'calculate',
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
],
|
|
596
|
+
},
|
|
597
|
+
},
|
|
598
|
+
],
|
|
599
|
+
}),
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
const contents: any[] = [];
|
|
604
|
+
|
|
605
|
+
const payload = {
|
|
606
|
+
contents,
|
|
607
|
+
model: 'gemini-2.5-flash',
|
|
608
|
+
tools: [
|
|
609
|
+
{
|
|
610
|
+
function: {
|
|
611
|
+
description: 'Perform mathematical calculation',
|
|
612
|
+
name: 'calculate',
|
|
613
|
+
parameters: {
|
|
614
|
+
properties: {
|
|
615
|
+
a: { type: 'number' },
|
|
616
|
+
b: { type: 'number' },
|
|
617
|
+
operation: { type: 'string' },
|
|
618
|
+
},
|
|
619
|
+
required: ['operation', 'a', 'b'],
|
|
620
|
+
type: 'object' as const,
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
type: 'function' as const,
|
|
624
|
+
},
|
|
625
|
+
],
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const options = {
|
|
629
|
+
signal: new AbortController().signal,
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const result = await createGoogleGenerateObjectWithTools(mockClient as any, payload, options);
|
|
633
|
+
|
|
634
|
+
expect(mockClient.models.generateContent).toHaveBeenCalledWith({
|
|
635
|
+
config: expect.objectContaining({
|
|
636
|
+
abortSignal: options.signal,
|
|
637
|
+
}),
|
|
638
|
+
contents,
|
|
639
|
+
model: 'gemini-2.5-flash',
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
expect(result).toEqual([{ arguments: { a: 5, b: 3, operation: 'add' }, name: 'calculate' }]);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it('should return undefined when no function calls in response', async () => {
|
|
646
|
+
const mockClient = {
|
|
647
|
+
models: {
|
|
648
|
+
generateContent: vi.fn().mockResolvedValue({
|
|
649
|
+
candidates: [
|
|
650
|
+
{
|
|
651
|
+
content: {
|
|
652
|
+
parts: [
|
|
653
|
+
{
|
|
654
|
+
text: 'Some text response without function call',
|
|
655
|
+
},
|
|
656
|
+
],
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
],
|
|
660
|
+
}),
|
|
661
|
+
},
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
const contents: any[] = [];
|
|
665
|
+
|
|
666
|
+
const payload = {
|
|
667
|
+
contents,
|
|
668
|
+
model: 'gemini-2.5-flash',
|
|
669
|
+
tools: [
|
|
670
|
+
{
|
|
671
|
+
function: {
|
|
672
|
+
description: 'Test function',
|
|
673
|
+
name: 'test_function',
|
|
674
|
+
parameters: {
|
|
675
|
+
properties: {},
|
|
676
|
+
type: 'object' as const,
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
type: 'function' as const,
|
|
680
|
+
},
|
|
681
|
+
],
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const result = await createGoogleGenerateObjectWithTools(mockClient as any, payload);
|
|
685
|
+
|
|
686
|
+
expect(result).toBeUndefined();
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('should return undefined when no content parts in response', async () => {
|
|
690
|
+
const mockClient = {
|
|
691
|
+
models: {
|
|
692
|
+
generateContent: vi.fn().mockResolvedValue({
|
|
693
|
+
candidates: [
|
|
694
|
+
{
|
|
695
|
+
content: {},
|
|
696
|
+
},
|
|
697
|
+
],
|
|
698
|
+
}),
|
|
699
|
+
},
|
|
700
|
+
};
|
|
701
|
+
|
|
702
|
+
const contents: any[] = [];
|
|
703
|
+
|
|
704
|
+
const payload = {
|
|
705
|
+
contents,
|
|
706
|
+
model: 'gemini-2.5-flash',
|
|
707
|
+
tools: [
|
|
708
|
+
{
|
|
709
|
+
function: {
|
|
710
|
+
description: 'Test function',
|
|
711
|
+
name: 'test_function',
|
|
712
|
+
parameters: {
|
|
713
|
+
properties: {},
|
|
714
|
+
type: 'object' as const,
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
type: 'function' as const,
|
|
718
|
+
},
|
|
719
|
+
],
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
const result = await createGoogleGenerateObjectWithTools(mockClient as any, payload);
|
|
723
|
+
|
|
724
|
+
expect(result).toBeUndefined();
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it('should propagate API errors correctly', async () => {
|
|
728
|
+
const apiError = new Error('API Error: Model not found');
|
|
729
|
+
|
|
730
|
+
const mockClient = {
|
|
731
|
+
models: {
|
|
732
|
+
generateContent: vi.fn().mockRejectedValue(apiError),
|
|
733
|
+
},
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
const contents: any[] = [];
|
|
737
|
+
|
|
738
|
+
const payload = {
|
|
739
|
+
contents,
|
|
740
|
+
model: 'gemini-2.5-flash',
|
|
741
|
+
tools: [
|
|
742
|
+
{
|
|
743
|
+
function: {
|
|
744
|
+
description: 'Test function',
|
|
745
|
+
name: 'test_function',
|
|
746
|
+
parameters: {
|
|
747
|
+
properties: {},
|
|
748
|
+
type: 'object' as const,
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
type: 'function' as const,
|
|
752
|
+
},
|
|
753
|
+
],
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
await expect(createGoogleGenerateObjectWithTools(mockClient as any, payload)).rejects.toThrow(
|
|
757
|
+
'API Error: Model not found',
|
|
758
|
+
);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it('should handle abort signals correctly', async () => {
|
|
762
|
+
const apiError = new Error('Request was cancelled');
|
|
763
|
+
apiError.name = 'AbortError';
|
|
764
|
+
|
|
765
|
+
const mockClient = {
|
|
766
|
+
models: {
|
|
767
|
+
generateContent: vi.fn().mockRejectedValue(apiError),
|
|
768
|
+
},
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
const contents: any[] = [];
|
|
772
|
+
|
|
773
|
+
const payload = {
|
|
774
|
+
contents,
|
|
775
|
+
model: 'gemini-2.5-flash',
|
|
776
|
+
tools: [
|
|
777
|
+
{
|
|
778
|
+
function: {
|
|
779
|
+
description: 'Test function',
|
|
780
|
+
name: 'test_function',
|
|
781
|
+
parameters: {
|
|
782
|
+
properties: {},
|
|
783
|
+
type: 'object' as const,
|
|
784
|
+
},
|
|
785
|
+
},
|
|
786
|
+
type: 'function' as const,
|
|
787
|
+
},
|
|
788
|
+
],
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
const options = {
|
|
792
|
+
signal: new AbortController().signal,
|
|
793
|
+
};
|
|
794
|
+
|
|
795
|
+
await expect(
|
|
796
|
+
createGoogleGenerateObjectWithTools(mockClient as any, payload, options),
|
|
797
|
+
).rejects.toThrow();
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
it('should handle tools with empty parameters', async () => {
|
|
801
|
+
const mockClient = {
|
|
802
|
+
models: {
|
|
803
|
+
generateContent: vi.fn().mockResolvedValue({
|
|
804
|
+
candidates: [
|
|
805
|
+
{
|
|
806
|
+
content: {
|
|
807
|
+
parts: [
|
|
808
|
+
{
|
|
809
|
+
functionCall: {
|
|
810
|
+
args: {},
|
|
811
|
+
name: 'simple_function',
|
|
812
|
+
},
|
|
813
|
+
},
|
|
814
|
+
],
|
|
815
|
+
},
|
|
816
|
+
},
|
|
817
|
+
],
|
|
818
|
+
}),
|
|
819
|
+
},
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
const contents: any[] = [];
|
|
823
|
+
|
|
824
|
+
const payload = {
|
|
825
|
+
contents,
|
|
826
|
+
model: 'gemini-2.5-flash',
|
|
827
|
+
tools: [
|
|
828
|
+
{
|
|
829
|
+
function: {
|
|
830
|
+
description: 'A simple function with no parameters',
|
|
831
|
+
name: 'simple_function',
|
|
832
|
+
parameters: {
|
|
833
|
+
properties: {},
|
|
834
|
+
type: 'object' as const,
|
|
835
|
+
},
|
|
836
|
+
},
|
|
837
|
+
type: 'function' as const,
|
|
838
|
+
},
|
|
839
|
+
],
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
const result = await createGoogleGenerateObjectWithTools(mockClient as any, payload);
|
|
843
|
+
|
|
844
|
+
// Should use dummy property for empty parameters
|
|
845
|
+
expect(mockClient.models.generateContent).toHaveBeenCalledWith({
|
|
846
|
+
config: expect.objectContaining({
|
|
847
|
+
tools: [
|
|
848
|
+
{
|
|
849
|
+
functionDeclarations: [
|
|
850
|
+
expect.objectContaining({
|
|
851
|
+
parameters: expect.objectContaining({
|
|
852
|
+
properties: { dummy: { type: 'string' } },
|
|
853
|
+
}),
|
|
854
|
+
}),
|
|
855
|
+
],
|
|
856
|
+
},
|
|
857
|
+
],
|
|
858
|
+
}),
|
|
859
|
+
contents,
|
|
860
|
+
model: 'gemini-2.5-flash',
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
expect(result).toEqual([{ arguments: {}, name: 'simple_function' }]);
|
|
864
|
+
});
|
|
865
|
+
});
|
|
361
866
|
});
|