@strands-agents/sdk 0.2.1 → 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/__fixtures__/mock-hook-provider.d.ts.map +1 -1
- package/dist/src/__fixtures__/mock-hook-provider.js +2 -1
- package/dist/src/__fixtures__/mock-hook-provider.js.map +1 -1
- package/dist/src/__tests__/errors.test.js +33 -1
- package/dist/src/__tests__/errors.test.js.map +1 -1
- package/dist/src/agent/__tests__/agent.hook.test.js +25 -23
- package/dist/src/agent/__tests__/agent.hook.test.js.map +1 -1
- package/dist/src/agent/agent.d.ts.map +1 -1
- package/dist/src/agent/agent.js +2 -1
- package/dist/src/agent/agent.js.map +1 -1
- package/dist/src/errors.d.ts +16 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +19 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/hooks/__tests__/events.test.js +18 -1
- package/dist/src/hooks/__tests__/events.test.js.map +1 -1
- package/dist/src/hooks/events.d.ts +11 -0
- package/dist/src/hooks/events.d.ts.map +1 -1
- package/dist/src/hooks/events.js +12 -0
- package/dist/src/hooks/events.js.map +1 -1
- package/dist/src/hooks/index.d.ts +1 -1
- package/dist/src/hooks/index.d.ts.map +1 -1
- package/dist/src/hooks/index.js +1 -1
- package/dist/src/hooks/index.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/models/__tests__/anthropic.test.d.ts +2 -0
- package/dist/src/models/__tests__/anthropic.test.d.ts.map +1 -0
- package/dist/src/models/__tests__/anthropic.test.js +481 -0
- package/dist/src/models/__tests__/anthropic.test.js.map +1 -0
- package/dist/src/models/__tests__/bedrock.test.js +86 -1
- package/dist/src/models/__tests__/bedrock.test.js.map +1 -1
- package/dist/src/models/__tests__/gemini.test.js +599 -62
- package/dist/src/models/__tests__/gemini.test.js.map +1 -1
- package/dist/src/models/__tests__/openai.test.js +104 -1
- package/dist/src/models/__tests__/openai.test.js.map +1 -1
- package/dist/src/models/anthropic.d.ts +28 -0
- package/dist/src/models/anthropic.d.ts.map +1 -0
- package/dist/src/models/anthropic.js +419 -0
- package/dist/src/models/anthropic.js.map +1 -0
- package/dist/src/models/bedrock.d.ts +6 -0
- package/dist/src/models/bedrock.d.ts.map +1 -1
- package/dist/src/models/bedrock.js +31 -4
- package/dist/src/models/bedrock.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 +259 -14
- 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 +38 -1
- package/dist/src/models/gemini/model.js.map +1 -1
- package/dist/src/models/gemini/types.d.ts +10 -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/openai.d.ts.map +1 -1
- package/dist/src/models/openai.js +20 -3
- package/dist/src/models/openai.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/media.d.ts +1 -1
- package/dist/src/types/media.d.ts.map +1 -1
- package/dist/src/types/media.js +18 -4
- package/dist/src/types/media.js.map +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 +11 -2
- 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,8 +1,11 @@
|
|
|
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, TextBlock, ToolResultBlock, ToolUseBlock, } from '../../types/messages.js';
|
|
7
|
+
import { formatMessages, mapChunkToEvents } from '../gemini/adapters.js';
|
|
8
|
+
import { ImageBlock, DocumentBlock, VideoBlock } from '../../types/media.js';
|
|
6
9
|
/**
|
|
7
10
|
* Helper to create a mock Gemini client with streaming support
|
|
8
11
|
*/
|
|
@@ -13,6 +16,49 @@ function createMockClient(streamGenerator) {
|
|
|
13
16
|
},
|
|
14
17
|
};
|
|
15
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
|
+
}
|
|
16
62
|
describe('GeminiModel', () => {
|
|
17
63
|
beforeEach(() => {
|
|
18
64
|
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
|
@@ -63,7 +109,7 @@ describe('GeminiModel', () => {
|
|
|
63
109
|
await expect(collectIterator(provider.stream([]))).rejects.toThrow('At least one message is required');
|
|
64
110
|
});
|
|
65
111
|
it('emits message start and stop events', async () => {
|
|
66
|
-
const
|
|
112
|
+
const { provider, messages } = setupStreamTest(async function* () {
|
|
67
113
|
yield {
|
|
68
114
|
candidates: [
|
|
69
115
|
{
|
|
@@ -73,14 +119,12 @@ describe('GeminiModel', () => {
|
|
|
73
119
|
};
|
|
74
120
|
yield { candidates: [{ finishReason: 'STOP' }] };
|
|
75
121
|
});
|
|
76
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
77
|
-
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
78
122
|
const events = await collectIterator(provider.stream(messages));
|
|
79
123
|
expect(events[0]).toEqual({ type: 'modelMessageStartEvent', role: 'assistant' });
|
|
80
124
|
expect(events[events.length - 1]).toEqual({ type: 'modelMessageStopEvent', stopReason: 'endTurn' });
|
|
81
125
|
});
|
|
82
126
|
it('emits text content block events', async () => {
|
|
83
|
-
const
|
|
127
|
+
const { provider, messages } = setupStreamTest(async function* () {
|
|
84
128
|
yield {
|
|
85
129
|
candidates: [
|
|
86
130
|
{
|
|
@@ -97,8 +141,6 @@ describe('GeminiModel', () => {
|
|
|
97
141
|
};
|
|
98
142
|
yield { candidates: [{ finishReason: 'STOP' }] };
|
|
99
143
|
});
|
|
100
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
101
|
-
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
102
144
|
const events = await collectIterator(provider.stream(messages));
|
|
103
145
|
expect(events).toHaveLength(6);
|
|
104
146
|
expect(events[0]).toEqual({ type: 'modelMessageStartEvent', role: 'assistant' });
|
|
@@ -115,7 +157,7 @@ describe('GeminiModel', () => {
|
|
|
115
157
|
expect(events[5]).toEqual({ type: 'modelMessageStopEvent', stopReason: 'endTurn' });
|
|
116
158
|
});
|
|
117
159
|
it('emits usage metadata when available', async () => {
|
|
118
|
-
const
|
|
160
|
+
const { provider, messages } = setupStreamTest(async function* () {
|
|
119
161
|
yield {
|
|
120
162
|
candidates: [
|
|
121
163
|
{
|
|
@@ -129,11 +171,8 @@ describe('GeminiModel', () => {
|
|
|
129
171
|
};
|
|
130
172
|
yield { candidates: [{ finishReason: 'STOP' }] };
|
|
131
173
|
});
|
|
132
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
133
|
-
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
134
174
|
const events = await collectIterator(provider.stream(messages));
|
|
135
175
|
const metadataEvent = events.find((e) => e.type === 'modelMetadataEvent');
|
|
136
|
-
expect(metadataEvent).toBeDefined();
|
|
137
176
|
expect(metadataEvent).toEqual({
|
|
138
177
|
type: 'modelMetadataEvent',
|
|
139
178
|
usage: {
|
|
@@ -144,7 +183,7 @@ describe('GeminiModel', () => {
|
|
|
144
183
|
});
|
|
145
184
|
});
|
|
146
185
|
it('handles MAX_TOKENS finish reason', async () => {
|
|
147
|
-
const
|
|
186
|
+
const { provider, messages } = setupStreamTest(async function* () {
|
|
148
187
|
yield {
|
|
149
188
|
candidates: [
|
|
150
189
|
{
|
|
@@ -154,8 +193,6 @@ describe('GeminiModel', () => {
|
|
|
154
193
|
};
|
|
155
194
|
yield { candidates: [{ finishReason: 'MAX_TOKENS' }] };
|
|
156
195
|
});
|
|
157
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
158
|
-
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
159
196
|
const events = await collectIterator(provider.stream(messages));
|
|
160
197
|
const stopEvent = events.find((e) => e.type === 'modelMessageStopEvent');
|
|
161
198
|
expect(stopEvent).toBeDefined();
|
|
@@ -194,81 +231,37 @@ describe('GeminiModel', () => {
|
|
|
194
231
|
});
|
|
195
232
|
});
|
|
196
233
|
describe('system prompt', () => {
|
|
197
|
-
/**
|
|
198
|
-
* Helper to create a mock client that captures the request config
|
|
199
|
-
*/
|
|
200
|
-
function createMockClientWithCapture(captureContainer) {
|
|
201
|
-
return {
|
|
202
|
-
models: {
|
|
203
|
-
generateContentStream: vi.fn(async ({ config }) => {
|
|
204
|
-
captureContainer.config = config;
|
|
205
|
-
return (async function* () {
|
|
206
|
-
yield { candidates: [{ finishReason: 'STOP' }] };
|
|
207
|
-
})();
|
|
208
|
-
}),
|
|
209
|
-
},
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
234
|
it('passes string system prompt to config', async () => {
|
|
213
|
-
const captured
|
|
214
|
-
const mockClient = createMockClientWithCapture(captured);
|
|
215
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
216
|
-
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
235
|
+
const { provider, captured, messages } = setupCaptureTest();
|
|
217
236
|
await collectIterator(provider.stream(messages, { systemPrompt: 'You are a helpful assistant' }));
|
|
218
|
-
expect(captured.config).toBeDefined();
|
|
219
237
|
const config = captured.config;
|
|
220
238
|
expect(config.systemInstruction).toBe('You are a helpful assistant');
|
|
221
239
|
});
|
|
222
240
|
it('ignores empty string system prompt', async () => {
|
|
223
|
-
const captured
|
|
224
|
-
const mockClient = createMockClientWithCapture(captured);
|
|
225
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
226
|
-
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] }];
|
|
241
|
+
const { provider, captured, messages } = setupCaptureTest();
|
|
227
242
|
await collectIterator(provider.stream(messages, { systemPrompt: ' ' }));
|
|
228
|
-
expect(captured.config).toBeDefined();
|
|
229
243
|
const config = captured.config;
|
|
230
244
|
expect(config.systemInstruction).toBeUndefined();
|
|
231
245
|
});
|
|
232
246
|
});
|
|
233
247
|
describe('message formatting', () => {
|
|
234
|
-
/**
|
|
235
|
-
* Helper to create a mock client that captures the request contents
|
|
236
|
-
*/
|
|
237
|
-
function createMockClientWithCapture(captureContainer) {
|
|
238
|
-
return {
|
|
239
|
-
models: {
|
|
240
|
-
generateContentStream: vi.fn(async ({ contents }) => {
|
|
241
|
-
captureContainer.contents = contents;
|
|
242
|
-
return (async function* () {
|
|
243
|
-
yield { candidates: [{ finishReason: 'STOP' }] };
|
|
244
|
-
})();
|
|
245
|
-
}),
|
|
246
|
-
},
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
248
|
it('formats user messages correctly', async () => {
|
|
250
|
-
const
|
|
251
|
-
const mockClient = createMockClientWithCapture(captured);
|
|
252
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
249
|
+
const { provider, captured } = setupCaptureTest();
|
|
253
250
|
const messages = [{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hello' }] }];
|
|
254
251
|
await collectIterator(provider.stream(messages));
|
|
255
|
-
expect(captured.contents).toBeDefined();
|
|
256
252
|
const contents = captured.contents;
|
|
257
253
|
expect(contents).toHaveLength(1);
|
|
258
254
|
expect(contents[0]?.role).toBe('user');
|
|
259
255
|
expect(contents[0]?.parts[0]?.text).toBe('Hello');
|
|
260
256
|
});
|
|
261
257
|
it('formats assistant messages correctly', async () => {
|
|
262
|
-
const
|
|
263
|
-
const mockClient = createMockClientWithCapture(captured);
|
|
264
|
-
const provider = new GeminiModel({ client: mockClient });
|
|
258
|
+
const { provider, captured } = setupCaptureTest();
|
|
265
259
|
const messages = [
|
|
266
260
|
{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'Hi' }] },
|
|
267
261
|
{ type: 'message', role: 'assistant', content: [{ type: 'textBlock', text: 'Hello!' }] },
|
|
268
262
|
{ type: 'message', role: 'user', content: [{ type: 'textBlock', text: 'How are you?' }] },
|
|
269
263
|
];
|
|
270
264
|
await collectIterator(provider.stream(messages));
|
|
271
|
-
expect(captured.contents).toBeDefined();
|
|
272
265
|
const contents = captured.contents;
|
|
273
266
|
expect(contents).toHaveLength(3);
|
|
274
267
|
expect(contents[0]?.role).toBe('user');
|
|
@@ -276,5 +269,549 @@ describe('GeminiModel', () => {
|
|
|
276
269
|
expect(contents[2]?.role).toBe('user');
|
|
277
270
|
});
|
|
278
271
|
});
|
|
272
|
+
describe('content type formatting', () => {
|
|
273
|
+
describe('image content', () => {
|
|
274
|
+
it('formats image with bytes source as inlineData', () => {
|
|
275
|
+
const imageBlock = new ImageBlock({
|
|
276
|
+
format: 'png',
|
|
277
|
+
source: { bytes: new Uint8Array([0x89, 0x50, 0x4e, 0x47]) },
|
|
278
|
+
});
|
|
279
|
+
const contents = formatBlock(imageBlock);
|
|
280
|
+
expect(contents).toHaveLength(1);
|
|
281
|
+
expect(contents[0].parts).toEqual([{ inlineData: { data: 'iVBORw==', mimeType: 'image/png' } }]);
|
|
282
|
+
});
|
|
283
|
+
it('formats image with URL source as fileData', () => {
|
|
284
|
+
const imageBlock = new ImageBlock({
|
|
285
|
+
format: 'jpeg',
|
|
286
|
+
source: { url: 'https://example.com/image.jpg' },
|
|
287
|
+
});
|
|
288
|
+
const contents = formatBlock(imageBlock);
|
|
289
|
+
expect(contents).toHaveLength(1);
|
|
290
|
+
expect(contents[0].parts).toEqual([
|
|
291
|
+
{ fileData: { fileUri: 'https://example.com/image.jpg', mimeType: 'image/jpeg' } },
|
|
292
|
+
]);
|
|
293
|
+
});
|
|
294
|
+
it('skips image with S3 source and logs warning', () => {
|
|
295
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
296
|
+
const imageBlock = new ImageBlock({
|
|
297
|
+
format: 'png',
|
|
298
|
+
source: { s3Location: { uri: 's3://test/image.png' } },
|
|
299
|
+
});
|
|
300
|
+
const contents = formatBlock(imageBlock);
|
|
301
|
+
// Message with no valid parts is not included
|
|
302
|
+
expect(contents).toHaveLength(0);
|
|
303
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
304
|
+
warnSpy.mockRestore();
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
describe('document content', () => {
|
|
308
|
+
it('formats document with bytes source as inlineData', () => {
|
|
309
|
+
const docBlock = new DocumentBlock({
|
|
310
|
+
name: 'test.pdf',
|
|
311
|
+
format: 'pdf',
|
|
312
|
+
source: { bytes: new Uint8Array([0x25, 0x50, 0x44, 0x46]) },
|
|
313
|
+
});
|
|
314
|
+
const contents = formatBlock(docBlock);
|
|
315
|
+
expect(contents).toHaveLength(1);
|
|
316
|
+
expect(contents[0].parts).toEqual([{ inlineData: { data: 'JVBERg==', mimeType: 'application/pdf' } }]);
|
|
317
|
+
});
|
|
318
|
+
it('formats document with text source as inlineData bytes', () => {
|
|
319
|
+
const docBlock = new DocumentBlock({
|
|
320
|
+
name: 'test.txt',
|
|
321
|
+
format: 'txt',
|
|
322
|
+
source: { text: 'Document content here' },
|
|
323
|
+
});
|
|
324
|
+
const contents = formatBlock(docBlock);
|
|
325
|
+
expect(contents).toHaveLength(1);
|
|
326
|
+
expect(contents[0].parts).toEqual([
|
|
327
|
+
{ inlineData: { data: 'RG9jdW1lbnQgY29udGVudCBoZXJl', mimeType: 'text/plain' } },
|
|
328
|
+
]);
|
|
329
|
+
});
|
|
330
|
+
it('formats document with content block source as separate text parts', () => {
|
|
331
|
+
const docBlock = new DocumentBlock({
|
|
332
|
+
name: 'test.txt',
|
|
333
|
+
format: 'txt',
|
|
334
|
+
source: { content: [{ text: 'Line 1' }, { text: 'Line 2' }] },
|
|
335
|
+
});
|
|
336
|
+
const contents = formatBlock(docBlock);
|
|
337
|
+
expect(contents).toHaveLength(1);
|
|
338
|
+
expect(contents[0].parts).toEqual([{ text: 'Line 1' }, { text: 'Line 2' }]);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
describe('video content', () => {
|
|
342
|
+
it('formats video with bytes source as inlineData', () => {
|
|
343
|
+
const videoBlock = new VideoBlock({
|
|
344
|
+
format: 'mp4',
|
|
345
|
+
source: { bytes: new Uint8Array([0x00, 0x00, 0x00, 0x1c]) },
|
|
346
|
+
});
|
|
347
|
+
const contents = formatBlock(videoBlock);
|
|
348
|
+
expect(contents).toHaveLength(1);
|
|
349
|
+
expect(contents[0].parts).toEqual([{ inlineData: { data: 'AAAAHA==', mimeType: 'video/mp4' } }]);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
describe('reasoning content', () => {
|
|
353
|
+
it('formats reasoning block with thought flag', () => {
|
|
354
|
+
const reasoningBlock = new ReasoningBlock({ text: 'Let me think about this...' });
|
|
355
|
+
const contents = formatBlock(reasoningBlock, 'assistant');
|
|
356
|
+
expect(contents).toHaveLength(1);
|
|
357
|
+
expect(contents[0].parts).toEqual([{ text: 'Let me think about this...', thought: true }]);
|
|
358
|
+
});
|
|
359
|
+
it('includes thought signature when present', () => {
|
|
360
|
+
const reasoningBlock = new ReasoningBlock({ text: 'Thinking...', signature: 'sig123' });
|
|
361
|
+
const contents = formatBlock(reasoningBlock, 'assistant');
|
|
362
|
+
expect(contents).toHaveLength(1);
|
|
363
|
+
expect(contents[0].parts).toEqual([{ text: 'Thinking...', thought: true, thoughtSignature: 'sig123' }]);
|
|
364
|
+
});
|
|
365
|
+
it('skips reasoning block with empty text', () => {
|
|
366
|
+
const reasoningBlock = new ReasoningBlock({ text: '' });
|
|
367
|
+
const contents = formatBlock(reasoningBlock, 'assistant');
|
|
368
|
+
expect(contents).toHaveLength(0);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
describe('unsupported content types', () => {
|
|
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 }) => {
|
|
379
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
380
|
+
const contents = formatBlock(block);
|
|
381
|
+
expect(contents).toHaveLength(0);
|
|
382
|
+
warnSpy.mockRestore();
|
|
383
|
+
});
|
|
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
|
+
]);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
describe('reasoning content streaming', () => {
|
|
395
|
+
it('emits reasoning content delta events for thought parts', async () => {
|
|
396
|
+
const { provider, messages } = setupStreamTest(async function* () {
|
|
397
|
+
yield {
|
|
398
|
+
candidates: [
|
|
399
|
+
{
|
|
400
|
+
content: { parts: [{ text: 'Thinking...', thought: true }] },
|
|
401
|
+
},
|
|
402
|
+
],
|
|
403
|
+
};
|
|
404
|
+
yield { candidates: [{ finishReason: 'STOP' }] };
|
|
405
|
+
});
|
|
406
|
+
const events = await collectIterator(provider.stream(messages));
|
|
407
|
+
expect(events).toHaveLength(5);
|
|
408
|
+
expect(events[0]).toEqual({ type: 'modelMessageStartEvent', role: 'assistant' });
|
|
409
|
+
expect(events[1]).toEqual({ type: 'modelContentBlockStartEvent' });
|
|
410
|
+
expect(events[2]).toEqual({
|
|
411
|
+
type: 'modelContentBlockDeltaEvent',
|
|
412
|
+
delta: { type: 'reasoningContentDelta', text: 'Thinking...' },
|
|
413
|
+
});
|
|
414
|
+
expect(events[3]).toEqual({ type: 'modelContentBlockStopEvent' });
|
|
415
|
+
expect(events[4]).toEqual({ type: 'modelMessageStopEvent', stopReason: 'endTurn' });
|
|
416
|
+
});
|
|
417
|
+
it('handles transition from reasoning to text content', async () => {
|
|
418
|
+
const { provider, messages } = setupStreamTest(async function* () {
|
|
419
|
+
yield {
|
|
420
|
+
candidates: [
|
|
421
|
+
{
|
|
422
|
+
content: { parts: [{ text: 'Let me think...', thought: true }] },
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
};
|
|
426
|
+
yield {
|
|
427
|
+
candidates: [
|
|
428
|
+
{
|
|
429
|
+
content: { parts: [{ text: 'Here is my answer' }] },
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
};
|
|
433
|
+
yield { candidates: [{ finishReason: 'STOP' }] };
|
|
434
|
+
});
|
|
435
|
+
const events = await collectIterator(provider.stream(messages));
|
|
436
|
+
// Should have: messageStart, blockStart (reasoning), delta (reasoning), blockStop,
|
|
437
|
+
// blockStart (text), delta (text), blockStop, messageStop
|
|
438
|
+
expect(events).toHaveLength(8);
|
|
439
|
+
// Reasoning block
|
|
440
|
+
expect(events[1]).toEqual({ type: 'modelContentBlockStartEvent' });
|
|
441
|
+
expect(events[2]).toEqual({
|
|
442
|
+
type: 'modelContentBlockDeltaEvent',
|
|
443
|
+
delta: { type: 'reasoningContentDelta', text: 'Let me think...' },
|
|
444
|
+
});
|
|
445
|
+
expect(events[3]).toEqual({ type: 'modelContentBlockStopEvent' });
|
|
446
|
+
// Text block
|
|
447
|
+
expect(events[4]).toEqual({ type: 'modelContentBlockStartEvent' });
|
|
448
|
+
expect(events[5]).toEqual({
|
|
449
|
+
type: 'modelContentBlockDeltaEvent',
|
|
450
|
+
delta: { type: 'textDelta', text: 'Here is my answer' },
|
|
451
|
+
});
|
|
452
|
+
expect(events[6]).toEqual({ type: 'modelContentBlockStopEvent' });
|
|
453
|
+
expect(events[7]).toEqual({ type: 'modelMessageStopEvent', stopReason: 'endTurn' });
|
|
454
|
+
});
|
|
455
|
+
it('includes signature in reasoning delta when present', async () => {
|
|
456
|
+
const { provider, messages } = setupStreamTest(async function* () {
|
|
457
|
+
yield {
|
|
458
|
+
candidates: [
|
|
459
|
+
{
|
|
460
|
+
content: {
|
|
461
|
+
parts: [
|
|
462
|
+
{
|
|
463
|
+
text: 'Thinking...',
|
|
464
|
+
thought: true,
|
|
465
|
+
thoughtSignature: 'sig456',
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
};
|
|
472
|
+
yield { candidates: [{ finishReason: 'STOP' }] };
|
|
473
|
+
});
|
|
474
|
+
const events = await collectIterator(provider.stream(messages));
|
|
475
|
+
const deltaEvent = events.find((e) => e.type === 'modelContentBlockDeltaEvent' && e.delta.type === 'reasoningContentDelta');
|
|
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' });
|
|
814
|
+
});
|
|
815
|
+
});
|
|
279
816
|
});
|
|
280
817
|
//# sourceMappingURL=gemini.test.js.map
|