@strands-agents/sdk 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/models/__tests__/gemini.test.js +432 -135
- package/dist/src/models/__tests__/gemini.test.js.map +1 -1
- package/dist/src/models/gemini/adapters.d.ts +2 -1
- package/dist/src/models/gemini/adapters.d.ts.map +1 -1
- package/dist/src/models/gemini/adapters.js +97 -10
- package/dist/src/models/gemini/adapters.js.map +1 -1
- package/dist/src/models/gemini/model.d.ts.map +1 -1
- package/dist/src/models/gemini/model.js +37 -1
- package/dist/src/models/gemini/model.js.map +1 -1
- package/dist/src/models/gemini/types.d.ts +9 -1
- package/dist/src/models/gemini/types.d.ts.map +1 -1
- package/dist/src/models/model.d.ts.map +1 -1
- package/dist/src/models/model.js +4 -0
- package/dist/src/models/model.js.map +1 -1
- package/dist/src/models/streaming.d.ts +5 -0
- package/dist/src/models/streaming.d.ts.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/src/types/messages.d.ts +10 -0
- package/dist/src/types/messages.d.ts.map +1 -1
- package/dist/src/types/messages.js +8 -0
- package/dist/src/types/messages.js.map +1 -1
- package/dist/src/vended-tools/bash/__tests__/bash.test.node.d.ts +2 -0
- package/dist/src/vended-tools/bash/__tests__/bash.test.node.d.ts.map +1 -0
- package/dist/src/vended-tools/bash/__tests__/{bash.test.js → bash.test.node.js} +3 -4
- package/dist/src/vended-tools/bash/__tests__/bash.test.node.js.map +1 -0
- package/dist/src/vended-tools/file_editor/__tests__/file-editor.test.node.d.ts +2 -0
- package/dist/src/vended-tools/file_editor/__tests__/file-editor.test.node.d.ts.map +1 -0
- package/dist/src/vended-tools/file_editor/__tests__/{file-editor.test.js → file-editor.test.node.js} +1 -1
- package/dist/src/vended-tools/file_editor/__tests__/file-editor.test.node.js.map +1 -0
- package/package.json +1 -1
- package/dist/src/vended-tools/bash/__tests__/bash.test.d.ts +0 -2
- package/dist/src/vended-tools/bash/__tests__/bash.test.d.ts.map +0 -1
- package/dist/src/vended-tools/bash/__tests__/bash.test.js.map +0 -1
- package/dist/src/vended-tools/file_editor/__tests__/file-editor.test.d.ts +0 -2
- package/dist/src/vended-tools/file_editor/__tests__/file-editor.test.d.ts.map +0 -1
- package/dist/src/vended-tools/file_editor/__tests__/file-editor.test.js.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { GoogleGenAI } from '@google/genai';
|
|
2
|
+
import { GoogleGenAI, FunctionCallingConfigMode } from '@google/genai';
|
|
3
3
|
import { collectIterator } from '../../__fixtures__/model-test-helpers.js';
|
|
4
4
|
import { GeminiModel } from '../gemini/model.js';
|
|
5
5
|
import { ContextWindowOverflowError } from '../../errors.js';
|
|
6
|
-
import { CachePointBlock, GuardContentBlock, ReasoningBlock, ToolUseBlock } from '../../types/messages.js';
|
|
7
|
-
import { formatMessages } from '../gemini/adapters.js';
|
|
6
|
+
import { CachePointBlock, GuardContentBlock, ReasoningBlock, TextBlock, ToolResultBlock, ToolUseBlock, } from '../../types/messages.js';
|
|
7
|
+
import { formatMessages, mapChunkToEvents } from '../gemini/adapters.js';
|
|
8
8
|
import { ImageBlock, DocumentBlock, VideoBlock } from '../../types/media.js';
|
|
9
9
|
/**
|
|
10
10
|
* Helper to create a mock Gemini client with streaming support
|
|
@@ -16,6 +16,49 @@ function createMockClient(streamGenerator) {
|
|
|
16
16
|
},
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Helper to create a mock Gemini client that captures the request parameters.
|
|
21
|
+
* Returns the client and a captured object with `config` and `contents` fields
|
|
22
|
+
* populated after a stream call.
|
|
23
|
+
*/
|
|
24
|
+
function createMockClientWithCapture() {
|
|
25
|
+
const captured = {};
|
|
26
|
+
const client = {
|
|
27
|
+
models: {
|
|
28
|
+
generateContentStream: vi.fn(async (params) => {
|
|
29
|
+
Object.assign(captured, params);
|
|
30
|
+
return (async function* () {
|
|
31
|
+
yield { candidates: [{ finishReason: 'STOP' }] };
|
|
32
|
+
})();
|
|
33
|
+
}),
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
return { client, captured };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Helper to set up a capture-based test with provider, captured params, and a default user message.
|
|
40
|
+
*/
|
|
41
|
+
function setupCaptureTest() {
|
|
42
|
+
const { client, captured } = createMockClientWithCapture();
|
|
43
|
+
const provider = new GeminiModel({ client });
|
|
44
|
+
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
45
|
+
return { provider, captured, messages };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Helper to set up a stream-based test with a mock client, provider, and default user message.
|
|
49
|
+
*/
|
|
50
|
+
function setupStreamTest(streamGenerator) {
|
|
51
|
+
const client = createMockClient(streamGenerator);
|
|
52
|
+
const provider = new GeminiModel({ client });
|
|
53
|
+
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
54
|
+
return { provider, messages };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Helper to format a single content block via formatMessages.
|
|
58
|
+
*/
|
|
59
|
+
function formatBlock(block, role = 'user') {
|
|
60
|
+
return formatMessages([{ type: 'message', role, content: [block] }]);
|
|
61
|
+
}
|
|
19
62
|
describe('GeminiModel', () => {
|
|
20
63
|
beforeEach(() => {
|
|
21
64
|
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
|
@@ -66,7 +109,7 @@ describe('GeminiModel', () => {
|
|
|
66
109
|
await expect(collectIterator(provider.stream([]))).rejects.toThrow('At least one message is required');
|
|
67
110
|
});
|
|
68
111
|
it('emits message start and stop events', async () => {
|
|
69
|
-
const
|
|
112
|
+
const { provider, messages } = setupStreamTest(async function* () {
|
|
70
113
|
yield {
|
|
71
114
|
candidates: [
|
|
72
115
|
{
|
|
@@ -76,14 +119,12 @@ describe('GeminiModel', () => {
|
|
|
76
119
|
};
|
|
77
120
|
yield { candidates: [{ finishReason: 'STOP' }] };
|
|
78
121
|
});
|
|
79
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
80
|
-
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
81
122
|
const events = await collectIterator(provider.stream(messages));
|
|
82
123
|
expect(events[0]).toEqual({ type: 'modelMessageStartEvent', role: 'assistant' });
|
|
83
124
|
expect(events[events.length - 1]).toEqual({ type: 'modelMessageStopEvent', stopReason: 'endTurn' });
|
|
84
125
|
});
|
|
85
126
|
it('emits text content block events', async () => {
|
|
86
|
-
const
|
|
127
|
+
const { provider, messages } = setupStreamTest(async function* () {
|
|
87
128
|
yield {
|
|
88
129
|
candidates: [
|
|
89
130
|
{
|
|
@@ -100,8 +141,6 @@ describe('GeminiModel', () => {
|
|
|
100
141
|
};
|
|
101
142
|
yield { candidates: [{ finishReason: 'STOP' }] };
|
|
102
143
|
});
|
|
103
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
104
|
-
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
105
144
|
const events = await collectIterator(provider.stream(messages));
|
|
106
145
|
expect(events).toHaveLength(6);
|
|
107
146
|
expect(events[0]).toEqual({ type: 'modelMessageStartEvent', role: 'assistant' });
|
|
@@ -118,7 +157,7 @@ describe('GeminiModel', () => {
|
|
|
118
157
|
expect(events[5]).toEqual({ type: 'modelMessageStopEvent', stopReason: 'endTurn' });
|
|
119
158
|
});
|
|
120
159
|
it('emits usage metadata when available', async () => {
|
|
121
|
-
const
|
|
160
|
+
const { provider, messages } = setupStreamTest(async function* () {
|
|
122
161
|
yield {
|
|
123
162
|
candidates: [
|
|
124
163
|
{
|
|
@@ -132,11 +171,8 @@ describe('GeminiModel', () => {
|
|
|
132
171
|
};
|
|
133
172
|
yield { candidates: [{ finishReason: 'STOP' }] };
|
|
134
173
|
});
|
|
135
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
136
|
-
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
137
174
|
const events = await collectIterator(provider.stream(messages));
|
|
138
175
|
const metadataEvent = events.find((e) => e.type === 'modelMetadataEvent');
|
|
139
|
-
expect(metadataEvent).toBeDefined();
|
|
140
176
|
expect(metadataEvent).toEqual({
|
|
141
177
|
type: 'modelMetadataEvent',
|
|
142
178
|
usage: {
|
|
@@ -147,7 +183,7 @@ describe('GeminiModel', () => {
|
|
|
147
183
|
});
|
|
148
184
|
});
|
|
149
185
|
it('handles MAX_TOKENS finish reason', async () => {
|
|
150
|
-
const
|
|
186
|
+
const { provider, messages } = setupStreamTest(async function* () {
|
|
151
187
|
yield {
|
|
152
188
|
candidates: [
|
|
153
189
|
{
|
|
@@ -157,8 +193,6 @@ describe('GeminiModel', () => {
|
|
|
157
193
|
};
|
|
158
194
|
yield { candidates: [{ finishReason: 'MAX_TOKENS' }] };
|
|
159
195
|
});
|
|
160
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
161
|
-
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
162
196
|
const events = await collectIterator(provider.stream(messages));
|
|
163
197
|
const stopEvent = events.find((e) => e.type === 'modelMessageStopEvent');
|
|
164
198
|
expect(stopEvent).toBeDefined();
|
|
@@ -197,81 +231,37 @@ describe('GeminiModel', () => {
|
|
|
197
231
|
});
|
|
198
232
|
});
|
|
199
233
|
describe('system prompt', () => {
|
|
200
|
-
/**
|
|
201
|
-
* Helper to create a mock client that captures the request config
|
|
202
|
-
*/
|
|
203
|
-
function createMockClientWithCapture(captureContainer) {
|
|
204
|
-
return {
|
|
205
|
-
models: {
|
|
206
|
-
generateContentStream: vi.fn(async ({ config }) => {
|
|
207
|
-
captureContainer.config = config;
|
|
208
|
-
return (async function* () {
|
|
209
|
-
yield { candidates: [{ finishReason: 'STOP' }] };
|
|
210
|
-
})();
|
|
211
|
-
}),
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
234
|
it('passes string system prompt to config', async () => {
|
|
216
|
-
const captured
|
|
217
|
-
const mockClient = createMockClientWithCapture(captured);
|
|
218
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
219
|
-
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
235
|
+
const { provider, captured, messages } = setupCaptureTest();
|
|
220
236
|
await collectIterator(provider.stream(messages, { systemPrompt: 'You are a helpful assistant' }));
|
|
221
|
-
expect(captured.config).toBeDefined();
|
|
222
237
|
const config = captured.config;
|
|
223
238
|
expect(config.systemInstruction).toBe('You are a helpful assistant');
|
|
224
239
|
});
|
|
225
240
|
it('ignores empty string system prompt', async () => {
|
|
226
|
-
const captured
|
|
227
|
-
const mockClient = createMockClientWithCapture(captured);
|
|
228
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
229
|
-
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
241
|
+
const { provider, captured, messages } = setupCaptureTest();
|
|
230
242
|
await collectIterator(provider.stream(messages, { systemPrompt: ' ' }));
|
|
231
|
-
expect(captured.config).toBeDefined();
|
|
232
243
|
const config = captured.config;
|
|
233
244
|
expect(config.systemInstruction).toBeUndefined();
|
|
234
245
|
});
|
|
235
246
|
});
|
|
236
247
|
describe('message formatting', () => {
|
|
237
|
-
/**
|
|
238
|
-
* Helper to create a mock client that captures the request contents
|
|
239
|
-
*/
|
|
240
|
-
function createMockClientWithCapture(captureContainer) {
|
|
241
|
-
return {
|
|
242
|
-
models: {
|
|
243
|
-
generateContentStream: vi.fn(async ({ contents }) => {
|
|
244
|
-
captureContainer.contents = contents;
|
|
245
|
-
return (async function* () {
|
|
246
|
-
yield { candidates: [{ finishReason: 'STOP' }] };
|
|
247
|
-
})();
|
|
248
|
-
}),
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
248
|
it('formats user messages correctly', async () => {
|
|
253
|
-
const
|
|
254
|
-
const mockClient = createMockClientWithCapture(captured);
|
|
255
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
249
|
+
const { provider, captured } = setupCaptureTest();
|
|
256
250
|
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hello' }] }];
|
|
257
251
|
await collectIterator(provider.stream(messages));
|
|
258
|
-
expect(captured.contents).toBeDefined();
|
|
259
252
|
const contents = captured.contents;
|
|
260
253
|
expect(contents).toHaveLength(1);
|
|
261
254
|
expect(contents[0]?.role).toBe('user');
|
|
262
255
|
expect(contents[0]?.parts[0]?.text).toBe('Hello');
|
|
263
256
|
});
|
|
264
257
|
it('formats assistant messages correctly', async () => {
|
|
265
|
-
const
|
|
266
|
-
const mockClient = createMockClientWithCapture(captured);
|
|
267
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
258
|
+
const { provider, captured } = setupCaptureTest();
|
|
268
259
|
const messages = [
|
|
269
260
|
{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] },
|
|
270
261
|
{ type: 'message', role: 'assistant', content: [{ type: 'textBlock', text: 'Hello!' }] },
|
|
271
262
|
{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'How are you?' }] },
|
|
272
263
|
];
|
|
273
264
|
await collectIterator(provider.stream(messages));
|
|
274
|
-
expect(captured.contents).toBeDefined();
|
|
275
265
|
const contents = captured.contents;
|
|
276
266
|
expect(contents).toHaveLength(3);
|
|
277
267
|
expect(contents[0]?.role).toBe('user');
|
|
@@ -286,25 +276,20 @@ describe('GeminiModel', () => {
|
|
|
286
276
|
format: 'png',
|
|
287
277
|
source: { bytes: new Uint8Array([0x89, 0x50, 0x4e, 0x47]) },
|
|
288
278
|
});
|
|
289
|
-
const
|
|
290
|
-
const contents = formatMessages(messages);
|
|
279
|
+
const contents = formatBlock(imageBlock);
|
|
291
280
|
expect(contents).toHaveLength(1);
|
|
292
|
-
|
|
293
|
-
expect(part).toHaveProperty('inlineData');
|
|
294
|
-
expect(part.inlineData.mimeType).toBe('image/png');
|
|
281
|
+
expect(contents[0].parts).toEqual([{ inlineData: { data: 'iVBORw==', mimeType: 'image/png' } }]);
|
|
295
282
|
});
|
|
296
283
|
it('formats image with URL source as fileData', () => {
|
|
297
284
|
const imageBlock = new ImageBlock({
|
|
298
285
|
format: 'jpeg',
|
|
299
286
|
source: { url: 'https://example.com/image.jpg' },
|
|
300
287
|
});
|
|
301
|
-
const
|
|
302
|
-
const contents = formatMessages(messages);
|
|
288
|
+
const contents = formatBlock(imageBlock);
|
|
303
289
|
expect(contents).toHaveLength(1);
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
expect(part.fileData.mimeType).toBe('image/jpeg');
|
|
290
|
+
expect(contents[0].parts).toEqual([
|
|
291
|
+
{ fileData: { fileUri: 'https://example.com/image.jpg', mimeType: 'image/jpeg' } },
|
|
292
|
+
]);
|
|
308
293
|
});
|
|
309
294
|
it('skips image with S3 source and logs warning', () => {
|
|
310
295
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
@@ -312,8 +297,7 @@ describe('GeminiModel', () => {
|
|
|
312
297
|
format: 'png',
|
|
313
298
|
source: { s3Location: { uri: 's3://test/image.png' } },
|
|
314
299
|
});
|
|
315
|
-
const
|
|
316
|
-
const contents = formatMessages(messages);
|
|
300
|
+
const contents = formatBlock(imageBlock);
|
|
317
301
|
// Message with no valid parts is not included
|
|
318
302
|
expect(contents).toHaveLength(0);
|
|
319
303
|
expect(warnSpy).toHaveBeenCalled();
|
|
@@ -327,12 +311,9 @@ describe('GeminiModel', () => {
|
|
|
327
311
|
format: 'pdf',
|
|
328
312
|
source: { bytes: new Uint8Array([0x25, 0x50, 0x44, 0x46]) },
|
|
329
313
|
});
|
|
330
|
-
const
|
|
331
|
-
const contents = formatMessages(messages);
|
|
314
|
+
const contents = formatBlock(docBlock);
|
|
332
315
|
expect(contents).toHaveLength(1);
|
|
333
|
-
|
|
334
|
-
expect(part).toHaveProperty('inlineData');
|
|
335
|
-
expect(part.inlineData.mimeType).toBe('application/pdf');
|
|
316
|
+
expect(contents[0].parts).toEqual([{ inlineData: { data: 'JVBERg==', mimeType: 'application/pdf' } }]);
|
|
336
317
|
});
|
|
337
318
|
it('formats document with text source as inlineData bytes', () => {
|
|
338
319
|
const docBlock = new DocumentBlock({
|
|
@@ -340,12 +321,11 @@ describe('GeminiModel', () => {
|
|
|
340
321
|
format: 'txt',
|
|
341
322
|
source: { text: 'Document content here' },
|
|
342
323
|
});
|
|
343
|
-
const
|
|
344
|
-
const contents = formatMessages(messages);
|
|
324
|
+
const contents = formatBlock(docBlock);
|
|
345
325
|
expect(contents).toHaveLength(1);
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
326
|
+
expect(contents[0].parts).toEqual([
|
|
327
|
+
{ inlineData: { data: 'RG9jdW1lbnQgY29udGVudCBoZXJl', mimeType: 'text/plain' } },
|
|
328
|
+
]);
|
|
349
329
|
});
|
|
350
330
|
it('formats document with content block source as separate text parts', () => {
|
|
351
331
|
const docBlock = new DocumentBlock({
|
|
@@ -353,8 +333,7 @@ describe('GeminiModel', () => {
|
|
|
353
333
|
format: 'txt',
|
|
354
334
|
source: { content: [{ text: 'Line 1' }, { text: 'Line 2' }] },
|
|
355
335
|
});
|
|
356
|
-
const
|
|
357
|
-
const contents = formatMessages(messages);
|
|
336
|
+
const contents = formatBlock(docBlock);
|
|
358
337
|
expect(contents).toHaveLength(1);
|
|
359
338
|
expect(contents[0].parts).toEqual([{ text: 'Line 1' }, { text: 'Line 2' }]);
|
|
360
339
|
});
|
|
@@ -365,68 +344,56 @@ describe('GeminiModel', () => {
|
|
|
365
344
|
format: 'mp4',
|
|
366
345
|
source: { bytes: new Uint8Array([0x00, 0x00, 0x00, 0x1c]) },
|
|
367
346
|
});
|
|
368
|
-
const
|
|
369
|
-
const contents = formatMessages(messages);
|
|
347
|
+
const contents = formatBlock(videoBlock);
|
|
370
348
|
expect(contents).toHaveLength(1);
|
|
371
|
-
|
|
372
|
-
expect(part).toHaveProperty('inlineData');
|
|
373
|
-
expect(part.inlineData.mimeType).toBe('video/mp4');
|
|
349
|
+
expect(contents[0].parts).toEqual([{ inlineData: { data: 'AAAAHA==', mimeType: 'video/mp4' } }]);
|
|
374
350
|
});
|
|
375
351
|
});
|
|
376
352
|
describe('reasoning content', () => {
|
|
377
353
|
it('formats reasoning block with thought flag', () => {
|
|
378
354
|
const reasoningBlock = new ReasoningBlock({ text: 'Let me think about this...' });
|
|
379
|
-
const
|
|
380
|
-
const contents = formatMessages(messages);
|
|
355
|
+
const contents = formatBlock(reasoningBlock, 'assistant');
|
|
381
356
|
expect(contents).toHaveLength(1);
|
|
382
|
-
|
|
383
|
-
expect(part).toHaveProperty('text', 'Let me think about this...');
|
|
384
|
-
expect(part).toHaveProperty('thought', true);
|
|
357
|
+
expect(contents[0].parts).toEqual([{ text: 'Let me think about this...', thought: true }]);
|
|
385
358
|
});
|
|
386
359
|
it('includes thought signature when present', () => {
|
|
387
360
|
const reasoningBlock = new ReasoningBlock({ text: 'Thinking...', signature: 'sig123' });
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
expect(part.thoughtSignature).toBe('sig123');
|
|
361
|
+
const contents = formatBlock(reasoningBlock, 'assistant');
|
|
362
|
+
expect(contents).toHaveLength(1);
|
|
363
|
+
expect(contents[0].parts).toEqual([{ text: 'Thinking...', thought: true, thoughtSignature: 'sig123' }]);
|
|
392
364
|
});
|
|
393
365
|
it('skips reasoning block with empty text', () => {
|
|
394
366
|
const reasoningBlock = new ReasoningBlock({ text: '' });
|
|
395
|
-
const
|
|
396
|
-
const contents = formatMessages(messages);
|
|
367
|
+
const contents = formatBlock(reasoningBlock, 'assistant');
|
|
397
368
|
expect(contents).toHaveLength(0);
|
|
398
369
|
});
|
|
399
370
|
});
|
|
400
371
|
describe('unsupported content types', () => {
|
|
401
|
-
it(
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
});
|
|
409
|
-
it('skips guard content blocks with warning', () => {
|
|
372
|
+
it.each([
|
|
373
|
+
{ name: 'cache point', block: new CachePointBlock({ cacheType: 'default' }) },
|
|
374
|
+
{
|
|
375
|
+
name: 'guard content',
|
|
376
|
+
block: new GuardContentBlock({ text: { qualifiers: ['guard_content'], text: 'test' } }),
|
|
377
|
+
},
|
|
378
|
+
])('skips $name blocks with warning', ({ block }) => {
|
|
410
379
|
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
411
|
-
const
|
|
412
|
-
const messages = [{ type: 'message', role: 'user', content: [guardBlock] }];
|
|
413
|
-
const contents = formatMessages(messages);
|
|
380
|
+
const contents = formatBlock(block);
|
|
414
381
|
expect(contents).toHaveLength(0);
|
|
415
382
|
warnSpy.mockRestore();
|
|
416
383
|
});
|
|
417
|
-
it('
|
|
418
|
-
const
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
384
|
+
it('formats tool use blocks as function calls', () => {
|
|
385
|
+
const toolUseBlock = new ToolUseBlock({ toolUseId: 'test-id', name: 'testTool', input: { key: 'value' } });
|
|
386
|
+
const contents = formatBlock(toolUseBlock, 'assistant');
|
|
387
|
+
expect(contents).toHaveLength(1);
|
|
388
|
+
expect(contents[0].parts).toEqual([
|
|
389
|
+
{ functionCall: { id: 'test-id', name: 'testTool', args: { key: 'value' } } },
|
|
390
|
+
]);
|
|
424
391
|
});
|
|
425
392
|
});
|
|
426
393
|
});
|
|
427
394
|
describe('reasoning content streaming', () => {
|
|
428
395
|
it('emits reasoning content delta events for thought parts', async () => {
|
|
429
|
-
const
|
|
396
|
+
const { provider, messages } = setupStreamTest(async function* () {
|
|
430
397
|
yield {
|
|
431
398
|
candidates: [
|
|
432
399
|
{
|
|
@@ -436,8 +403,6 @@ describe('GeminiModel', () => {
|
|
|
436
403
|
};
|
|
437
404
|
yield { candidates: [{ finishReason: 'STOP' }] };
|
|
438
405
|
});
|
|
439
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
440
|
-
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
441
406
|
const events = await collectIterator(provider.stream(messages));
|
|
442
407
|
expect(events).toHaveLength(5);
|
|
443
408
|
expect(events[0]).toEqual({ type: 'modelMessageStartEvent', role: 'assistant' });
|
|
@@ -450,7 +415,7 @@ describe('GeminiModel', () => {
|
|
|
450
415
|
expect(events[4]).toEqual({ type: 'modelMessageStopEvent', stopReason: 'endTurn' });
|
|
451
416
|
});
|
|
452
417
|
it('handles transition from reasoning to text content', async () => {
|
|
453
|
-
const
|
|
418
|
+
const { provider, messages } = setupStreamTest(async function* () {
|
|
454
419
|
yield {
|
|
455
420
|
candidates: [
|
|
456
421
|
{
|
|
@@ -467,8 +432,6 @@ describe('GeminiModel', () => {
|
|
|
467
432
|
};
|
|
468
433
|
yield { candidates: [{ finishReason: 'STOP' }] };
|
|
469
434
|
});
|
|
470
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
471
|
-
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
472
435
|
const events = await collectIterator(provider.stream(messages));
|
|
473
436
|
// Should have: messageStart, blockStart (reasoning), delta (reasoning), blockStop,
|
|
474
437
|
// blockStart (text), delta (text), blockStop, messageStop
|
|
@@ -490,7 +453,7 @@ describe('GeminiModel', () => {
|
|
|
490
453
|
expect(events[7]).toEqual({ type: 'modelMessageStopEvent', stopReason: 'endTurn' });
|
|
491
454
|
});
|
|
492
455
|
it('includes signature in reasoning delta when present', async () => {
|
|
493
|
-
const
|
|
456
|
+
const { provider, messages } = setupStreamTest(async function* () {
|
|
494
457
|
yield {
|
|
495
458
|
candidates: [
|
|
496
459
|
{
|
|
@@ -508,12 +471,346 @@ describe('GeminiModel', () => {
|
|
|
508
471
|
};
|
|
509
472
|
yield { candidates: [{ finishReason: 'STOP' }] };
|
|
510
473
|
});
|
|
511
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
512
|
-
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
513
474
|
const events = await collectIterator(provider.stream(messages));
|
|
514
475
|
const deltaEvent = events.find((e) => e.type === 'modelContentBlockDeltaEvent' && e.delta.type === 'reasoningContentDelta');
|
|
515
|
-
expect(deltaEvent).
|
|
516
|
-
|
|
476
|
+
expect(deltaEvent).toEqual({
|
|
477
|
+
type: 'modelContentBlockDeltaEvent',
|
|
478
|
+
delta: { type: 'reasoningContentDelta', text: 'Thinking...', signature: 'sig456' },
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
describe('tool configuration', () => {
|
|
483
|
+
it('passes tool specs as functionDeclarations', async () => {
|
|
484
|
+
const { provider, captured, messages } = setupCaptureTest();
|
|
485
|
+
await collectIterator(provider.stream(messages, {
|
|
486
|
+
toolSpecs: [
|
|
487
|
+
{
|
|
488
|
+
name: 'get_weather',
|
|
489
|
+
description: 'Get the weather',
|
|
490
|
+
inputSchema: { type: 'object', properties: { city: { type: 'string' } } },
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
}));
|
|
494
|
+
const config = captured.config;
|
|
495
|
+
expect(config.tools).toEqual([
|
|
496
|
+
{
|
|
497
|
+
functionDeclarations: [
|
|
498
|
+
{
|
|
499
|
+
name: 'get_weather',
|
|
500
|
+
description: 'Get the weather',
|
|
501
|
+
parametersJsonSchema: { type: 'object', properties: { city: { type: 'string' } } },
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
},
|
|
505
|
+
]);
|
|
506
|
+
});
|
|
507
|
+
it.each([
|
|
508
|
+
{
|
|
509
|
+
name: 'auto to AUTO',
|
|
510
|
+
toolChoice: { auto: {} },
|
|
511
|
+
expectedMode: FunctionCallingConfigMode.AUTO,
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: 'any to ANY',
|
|
515
|
+
toolChoice: { any: {} },
|
|
516
|
+
expectedMode: FunctionCallingConfigMode.ANY,
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
name: 'tool to ANY with allowedFunctionNames',
|
|
520
|
+
toolChoice: { tool: { name: 'get_weather' } },
|
|
521
|
+
expectedMode: FunctionCallingConfigMode.ANY,
|
|
522
|
+
expectedAllowedFunctionNames: ['get_weather'],
|
|
523
|
+
},
|
|
524
|
+
])('maps toolChoice $name', async ({ toolChoice, expectedMode, expectedAllowedFunctionNames }) => {
|
|
525
|
+
const { provider, captured, messages } = setupCaptureTest();
|
|
526
|
+
await collectIterator(provider.stream(messages, {
|
|
527
|
+
toolSpecs: [{ name: 'get_weather', description: 'test' }],
|
|
528
|
+
toolChoice,
|
|
529
|
+
}));
|
|
530
|
+
const config = captured.config;
|
|
531
|
+
expect(config.toolConfig?.functionCallingConfig?.mode).toBe(expectedMode);
|
|
532
|
+
if (expectedAllowedFunctionNames) {
|
|
533
|
+
expect(config.toolConfig?.functionCallingConfig?.allowedFunctionNames).toEqual(expectedAllowedFunctionNames);
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
it('does not add tools config when no toolSpecs provided', async () => {
|
|
537
|
+
const { provider, captured, messages } = setupCaptureTest();
|
|
538
|
+
await collectIterator(provider.stream(messages));
|
|
539
|
+
const config = captured.config;
|
|
540
|
+
expect(config.tools).toBeUndefined();
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
describe('built-in tools', () => {
|
|
544
|
+
it('appends geminiTools to config.tools alongside functionDeclarations', async () => {
|
|
545
|
+
const { client, captured } = createMockClientWithCapture();
|
|
546
|
+
const provider = new GeminiModel({ client, geminiTools: [{ googleSearch: {} }] });
|
|
547
|
+
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
548
|
+
await collectIterator(provider.stream(messages, {
|
|
549
|
+
toolSpecs: [
|
|
550
|
+
{
|
|
551
|
+
name: 'get_weather',
|
|
552
|
+
description: 'Get the weather',
|
|
553
|
+
inputSchema: { type: 'object', properties: { city: { type: 'string' } } },
|
|
554
|
+
},
|
|
555
|
+
],
|
|
556
|
+
}));
|
|
557
|
+
const config = captured.config;
|
|
558
|
+
expect(config.tools).toHaveLength(2);
|
|
559
|
+
expect(config.tools[0]).toEqual({
|
|
560
|
+
functionDeclarations: [
|
|
561
|
+
{
|
|
562
|
+
name: 'get_weather',
|
|
563
|
+
description: 'Get the weather',
|
|
564
|
+
parametersJsonSchema: { type: 'object', properties: { city: { type: 'string' } } },
|
|
565
|
+
},
|
|
566
|
+
],
|
|
567
|
+
});
|
|
568
|
+
expect(config.tools[1]).toEqual({ googleSearch: {} });
|
|
569
|
+
});
|
|
570
|
+
it('passes geminiTools when no toolSpecs provided', async () => {
|
|
571
|
+
const { client, captured } = createMockClientWithCapture();
|
|
572
|
+
const provider = new GeminiModel({ client, geminiTools: [{ codeExecution: {} }] });
|
|
573
|
+
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
574
|
+
await collectIterator(provider.stream(messages));
|
|
575
|
+
const config = captured.config;
|
|
576
|
+
expect(config.tools).toHaveLength(1);
|
|
577
|
+
expect(config.tools[0]).toEqual({ codeExecution: {} });
|
|
578
|
+
});
|
|
579
|
+
it('does not add tools when neither geminiTools nor toolSpecs provided', async () => {
|
|
580
|
+
const { client, captured } = createMockClientWithCapture();
|
|
581
|
+
const provider = new GeminiModel({ client });
|
|
582
|
+
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
583
|
+
await collectIterator(provider.stream(messages));
|
|
584
|
+
const config = captured.config;
|
|
585
|
+
expect(config.tools).toBeUndefined();
|
|
586
|
+
});
|
|
587
|
+
});
|
|
588
|
+
describe('tool use formatting', () => {
|
|
589
|
+
it('formats toolUseBlock with reasoningSignature as thoughtSignature', () => {
|
|
590
|
+
const toolUseBlock = new ToolUseBlock({
|
|
591
|
+
toolUseId: 'test-id',
|
|
592
|
+
name: 'testTool',
|
|
593
|
+
input: { key: 'value' },
|
|
594
|
+
reasoningSignature: 'sig789',
|
|
595
|
+
});
|
|
596
|
+
const contents = formatBlock(toolUseBlock, 'assistant');
|
|
597
|
+
expect(contents).toHaveLength(1);
|
|
598
|
+
expect(contents[0].parts).toEqual([
|
|
599
|
+
{
|
|
600
|
+
functionCall: { id: 'test-id', name: 'testTool', args: { key: 'value' } },
|
|
601
|
+
thoughtSignature: 'sig789',
|
|
602
|
+
},
|
|
603
|
+
]);
|
|
604
|
+
});
|
|
605
|
+
it('formats toolResultBlock as functionResponse', () => {
|
|
606
|
+
const toolUseBlock = new ToolUseBlock({ toolUseId: 'test-id', name: 'testTool', input: {} });
|
|
607
|
+
const toolResultBlock = new ToolResultBlock({
|
|
608
|
+
toolUseId: 'test-id',
|
|
609
|
+
status: 'success',
|
|
610
|
+
content: [new TextBlock('result text')],
|
|
611
|
+
});
|
|
612
|
+
const messages = [
|
|
613
|
+
{ type: 'message', role: 'assistant', content: [toolUseBlock] },
|
|
614
|
+
{ type: 'message', role: 'user', content: [toolResultBlock] },
|
|
615
|
+
];
|
|
616
|
+
const contents = formatMessages(messages);
|
|
617
|
+
expect(contents).toHaveLength(2);
|
|
618
|
+
expect(contents[1].parts[0]).toEqual({
|
|
619
|
+
functionResponse: {
|
|
620
|
+
id: 'test-id',
|
|
621
|
+
name: 'testTool',
|
|
622
|
+
response: { output: [{ text: 'result text' }] },
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
it('resolves tool name from toolUseId in toolResultBlock', () => {
|
|
627
|
+
const toolUseBlock = new ToolUseBlock({ toolUseId: 'abc-123', name: 'my_tool', input: {} });
|
|
628
|
+
const toolResultBlock = new ToolResultBlock({
|
|
629
|
+
toolUseId: 'abc-123',
|
|
630
|
+
status: 'success',
|
|
631
|
+
content: [new TextBlock('ok')],
|
|
632
|
+
});
|
|
633
|
+
const messages = [
|
|
634
|
+
{ type: 'message', role: 'assistant', content: [toolUseBlock] },
|
|
635
|
+
{ type: 'message', role: 'user', content: [toolResultBlock] },
|
|
636
|
+
];
|
|
637
|
+
const contents = formatMessages(messages);
|
|
638
|
+
const resultPart = contents[1].parts[0];
|
|
639
|
+
const fr = resultPart.functionResponse;
|
|
640
|
+
expect(fr.name).toBe('my_tool');
|
|
641
|
+
});
|
|
642
|
+
it('falls back to toolUseId when tool name mapping is not found', () => {
|
|
643
|
+
const toolResultBlock = new ToolResultBlock({
|
|
644
|
+
toolUseId: 'unknown-id',
|
|
645
|
+
status: 'success',
|
|
646
|
+
content: [new TextBlock('ok')],
|
|
647
|
+
});
|
|
648
|
+
const messages = [{ type: 'message', role: 'user', content: [toolResultBlock] }];
|
|
649
|
+
const contents = formatMessages(messages);
|
|
650
|
+
const resultPart = contents[0].parts[0];
|
|
651
|
+
const fr = resultPart.functionResponse;
|
|
652
|
+
expect(fr.name).toBe('unknown-id');
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
describe('tool use streaming', () => {
|
|
656
|
+
function createStreamState() {
|
|
657
|
+
return {
|
|
658
|
+
messageStarted: true,
|
|
659
|
+
textContentBlockStarted: false,
|
|
660
|
+
reasoningContentBlockStarted: false,
|
|
661
|
+
hasToolCalls: false,
|
|
662
|
+
inputTokens: 0,
|
|
663
|
+
outputTokens: 0,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
it('emits tool use events for function call in response', () => {
|
|
667
|
+
const streamState = createStreamState();
|
|
668
|
+
const chunk = {
|
|
669
|
+
candidates: [
|
|
670
|
+
{
|
|
671
|
+
content: {
|
|
672
|
+
parts: [{ functionCall: { id: 'tool-1', name: 'get_weather', args: { city: 'NYC' } } }],
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
],
|
|
676
|
+
};
|
|
677
|
+
const events = mapChunkToEvents(chunk, streamState);
|
|
678
|
+
expect(events).toEqual([
|
|
679
|
+
{
|
|
680
|
+
type: 'modelContentBlockStartEvent',
|
|
681
|
+
start: { type: 'toolUseStart', name: 'get_weather', toolUseId: 'tool-1' },
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
type: 'modelContentBlockDeltaEvent',
|
|
685
|
+
delta: { type: 'toolUseInputDelta', input: '{"city":"NYC"}' },
|
|
686
|
+
},
|
|
687
|
+
{ type: 'modelContentBlockStopEvent' },
|
|
688
|
+
]);
|
|
689
|
+
expect(streamState.hasToolCalls).toBe(true);
|
|
690
|
+
});
|
|
691
|
+
it('generates tool use ID when Gemini does not provide one', () => {
|
|
692
|
+
const streamState = createStreamState();
|
|
693
|
+
const chunk = {
|
|
694
|
+
candidates: [
|
|
695
|
+
{
|
|
696
|
+
content: {
|
|
697
|
+
parts: [{ functionCall: { name: 'testTool', args: {} } }],
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
],
|
|
701
|
+
};
|
|
702
|
+
const events = mapChunkToEvents(chunk, streamState);
|
|
703
|
+
const startEvent = events[0];
|
|
704
|
+
expect(startEvent.type).toBe('modelContentBlockStartEvent');
|
|
705
|
+
const start = startEvent.start;
|
|
706
|
+
expect(start.toolUseId).toMatch(/^tooluse_/);
|
|
707
|
+
});
|
|
708
|
+
it('includes reasoningSignature from thoughtSignature on function call', () => {
|
|
709
|
+
const streamState = createStreamState();
|
|
710
|
+
const chunk = {
|
|
711
|
+
candidates: [
|
|
712
|
+
{
|
|
713
|
+
content: {
|
|
714
|
+
parts: [
|
|
715
|
+
{
|
|
716
|
+
functionCall: { id: 'tool-1', name: 'testTool', args: {} },
|
|
717
|
+
thoughtSignature: 'sig-abc',
|
|
718
|
+
},
|
|
719
|
+
],
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
],
|
|
723
|
+
};
|
|
724
|
+
const events = mapChunkToEvents(chunk, streamState);
|
|
725
|
+
const startEvent = events[0];
|
|
726
|
+
const start = startEvent.start;
|
|
727
|
+
expect(start.reasoningSignature).toBe('sig-abc');
|
|
728
|
+
});
|
|
729
|
+
it('sets stop reason to toolUse when function calls are present', () => {
|
|
730
|
+
const streamState = createStreamState();
|
|
731
|
+
streamState.hasToolCalls = true;
|
|
732
|
+
const chunk = {
|
|
733
|
+
candidates: [{ finishReason: 'STOP' }],
|
|
734
|
+
};
|
|
735
|
+
const events = mapChunkToEvents(chunk, streamState);
|
|
736
|
+
expect(events).toEqual([{ type: 'modelMessageStopEvent', stopReason: 'toolUse' }]);
|
|
737
|
+
});
|
|
738
|
+
it.each([
|
|
739
|
+
{ blockType: 'reasoning', stateField: 'reasoningContentBlockStarted' },
|
|
740
|
+
{ blockType: 'text', stateField: 'textContentBlockStarted' },
|
|
741
|
+
])('closes $blockType block before tool use block', ({ stateField }) => {
|
|
742
|
+
const streamState = createStreamState();
|
|
743
|
+
streamState[stateField] = true;
|
|
744
|
+
const chunk = {
|
|
745
|
+
candidates: [
|
|
746
|
+
{
|
|
747
|
+
content: {
|
|
748
|
+
parts: [{ functionCall: { id: 'tool-1', name: 'testTool', args: {} } }],
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
],
|
|
752
|
+
};
|
|
753
|
+
const events = mapChunkToEvents(chunk, streamState);
|
|
754
|
+
expect(events[0]).toEqual({ type: 'modelContentBlockStopEvent' });
|
|
755
|
+
expect(events[1]).toEqual({
|
|
756
|
+
type: 'modelContentBlockStartEvent',
|
|
757
|
+
start: { type: 'toolUseStart', name: 'testTool', toolUseId: 'tool-1' },
|
|
758
|
+
});
|
|
759
|
+
expect(streamState[stateField]).toBe(false);
|
|
760
|
+
});
|
|
761
|
+
it('handles multiple function calls in a single response', () => {
|
|
762
|
+
const streamState = createStreamState();
|
|
763
|
+
const chunk = {
|
|
764
|
+
candidates: [
|
|
765
|
+
{
|
|
766
|
+
content: {
|
|
767
|
+
parts: [
|
|
768
|
+
{ functionCall: { id: 'tool-1', name: 'get_weather', args: { city: 'NYC' } } },
|
|
769
|
+
{ functionCall: { id: 'tool-2', name: 'get_time', args: { tz: 'EST' } } },
|
|
770
|
+
],
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
],
|
|
774
|
+
};
|
|
775
|
+
const events = mapChunkToEvents(chunk, streamState);
|
|
776
|
+
// Each function call: start + delta + stop = 3 events, x2 = 6
|
|
777
|
+
expect(events).toHaveLength(6);
|
|
778
|
+
expect(events[0]).toEqual({
|
|
779
|
+
type: 'modelContentBlockStartEvent',
|
|
780
|
+
start: { type: 'toolUseStart', name: 'get_weather', toolUseId: 'tool-1' },
|
|
781
|
+
});
|
|
782
|
+
expect(events[3]).toEqual({
|
|
783
|
+
type: 'modelContentBlockStartEvent',
|
|
784
|
+
start: { type: 'toolUseStart', name: 'get_time', toolUseId: 'tool-2' },
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
it('handles full tool use flow via stream method', async () => {
|
|
788
|
+
const { provider, messages } = setupStreamTest(async function* () {
|
|
789
|
+
yield {
|
|
790
|
+
candidates: [
|
|
791
|
+
{
|
|
792
|
+
content: {
|
|
793
|
+
parts: [{ functionCall: { id: 'call-1', name: 'get_weather', args: { city: 'NYC' } } }],
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
],
|
|
797
|
+
};
|
|
798
|
+
yield { candidates: [{ finishReason: 'STOP' }] };
|
|
799
|
+
});
|
|
800
|
+
const events = await collectIterator(provider.stream(messages));
|
|
801
|
+
// messageStart, blockStart (toolUse), delta (toolUseInput), blockStop, messageStop
|
|
802
|
+
expect(events).toHaveLength(5);
|
|
803
|
+
expect(events[0]).toEqual({ type: 'modelMessageStartEvent', role: 'assistant' });
|
|
804
|
+
expect(events[1]).toEqual({
|
|
805
|
+
type: 'modelContentBlockStartEvent',
|
|
806
|
+
start: { type: 'toolUseStart', name: 'get_weather', toolUseId: 'call-1' },
|
|
807
|
+
});
|
|
808
|
+
expect(events[2]).toEqual({
|
|
809
|
+
type: 'modelContentBlockDeltaEvent',
|
|
810
|
+
delta: { type: 'toolUseInputDelta', input: '{"city":"NYC"}' },
|
|
811
|
+
});
|
|
812
|
+
expect(events[3]).toEqual({ type: 'modelContentBlockStopEvent' });
|
|
813
|
+
expect(events[4]).toEqual({ type: 'modelMessageStopEvent', stopReason: 'toolUse' });
|
|
517
814
|
});
|
|
518
815
|
});
|
|
519
816
|
});
|