@layer-ai/core 2.0.17 → 2.0.19
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/routes/tests/test-chat-streaming.js +160 -0
- package/dist/services/providers/google-adapter.d.ts +2 -0
- package/dist/services/providers/google-adapter.d.ts.map +1 -1
- package/dist/services/providers/google-adapter.js +186 -5
- package/dist/services/providers/mistral-adapter.d.ts +2 -0
- package/dist/services/providers/mistral-adapter.d.ts.map +1 -1
- package/dist/services/providers/mistral-adapter.js +205 -0
- package/dist/services/providers/tests/test-google-streaming.d.ts +2 -0
- package/dist/services/providers/tests/test-google-streaming.d.ts.map +1 -0
- package/dist/services/providers/tests/test-google-streaming.js +139 -0
- package/dist/services/providers/tests/test-mistral-streaming.d.ts +2 -0
- package/dist/services/providers/tests/test-mistral-streaming.d.ts.map +1 -0
- package/dist/services/providers/tests/test-mistral-streaming.js +139 -0
- package/package.json +1 -1
|
@@ -314,6 +314,162 @@ async function testMultiProviderFallback() {
|
|
|
314
314
|
console.log(' ✅ Multi-provider fallback test passed\n');
|
|
315
315
|
}
|
|
316
316
|
}
|
|
317
|
+
// Test 8: Google/Gemini streaming
|
|
318
|
+
async function testGeminiStreaming() {
|
|
319
|
+
console.log('Test 8: Google/Gemini Streaming');
|
|
320
|
+
console.log('-'.repeat(80));
|
|
321
|
+
const request = {
|
|
322
|
+
gateId: 'test-gate',
|
|
323
|
+
model: 'gemini-2.0-flash',
|
|
324
|
+
type: 'chat',
|
|
325
|
+
data: {
|
|
326
|
+
messages: [
|
|
327
|
+
{ role: 'user', content: 'Say "gemini test passed" and nothing else.' }
|
|
328
|
+
],
|
|
329
|
+
maxTokens: 20,
|
|
330
|
+
stream: true,
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
let chunkCount = 0;
|
|
334
|
+
let fullContent = '';
|
|
335
|
+
for await (const chunk of callAdapterStream(request)) {
|
|
336
|
+
chunkCount++;
|
|
337
|
+
if (chunk.content) {
|
|
338
|
+
fullContent += chunk.content;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
console.log(` Chunks received: ${chunkCount}`);
|
|
342
|
+
console.log(` Content: ${fullContent.trim()}`);
|
|
343
|
+
console.log(' ✅ Gemini streaming test passed\n');
|
|
344
|
+
}
|
|
345
|
+
// Test 9: Gemini with tool calls streaming
|
|
346
|
+
async function testGeminiToolCallsStreaming() {
|
|
347
|
+
console.log('Test 9: Gemini Tool Calls Streaming');
|
|
348
|
+
console.log('-'.repeat(80));
|
|
349
|
+
const request = {
|
|
350
|
+
gateId: 'test-gate',
|
|
351
|
+
model: 'gemini-2.0-flash',
|
|
352
|
+
type: 'chat',
|
|
353
|
+
data: {
|
|
354
|
+
messages: [
|
|
355
|
+
{ role: 'user', content: 'What is the weather in London?' }
|
|
356
|
+
],
|
|
357
|
+
tools: [
|
|
358
|
+
{
|
|
359
|
+
type: 'function',
|
|
360
|
+
function: {
|
|
361
|
+
name: 'get_weather',
|
|
362
|
+
description: 'Get weather for a location',
|
|
363
|
+
parameters: {
|
|
364
|
+
type: 'object',
|
|
365
|
+
properties: {
|
|
366
|
+
location: { type: 'string' },
|
|
367
|
+
},
|
|
368
|
+
required: ['location'],
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
stream: true,
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
let toolCallsFound = false;
|
|
377
|
+
let finishReason = null;
|
|
378
|
+
for await (const chunk of callAdapterStream(request)) {
|
|
379
|
+
if (chunk.toolCalls && chunk.toolCalls.length > 0) {
|
|
380
|
+
toolCallsFound = true;
|
|
381
|
+
}
|
|
382
|
+
if (chunk.finishReason) {
|
|
383
|
+
finishReason = chunk.finishReason;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
console.log(` Tool calls found: ${toolCallsFound}`);
|
|
387
|
+
console.log(` Finish reason: ${finishReason}`);
|
|
388
|
+
if (toolCallsFound) {
|
|
389
|
+
console.log(' ✅ Gemini tool calls streaming test passed\n');
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
console.log(' ⚠️ Tool calls may not have been invoked (model chose not to use tools)\n');
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// Test 10: Mistral streaming
|
|
396
|
+
async function testMistralStreaming() {
|
|
397
|
+
console.log('Test 10: Mistral Streaming');
|
|
398
|
+
console.log('-'.repeat(80));
|
|
399
|
+
const request = {
|
|
400
|
+
gateId: 'test-gate',
|
|
401
|
+
model: 'mistral-small-2501',
|
|
402
|
+
type: 'chat',
|
|
403
|
+
data: {
|
|
404
|
+
messages: [
|
|
405
|
+
{ role: 'user', content: 'Say "mistral test passed" and nothing else.' }
|
|
406
|
+
],
|
|
407
|
+
maxTokens: 20,
|
|
408
|
+
stream: true,
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
let chunkCount = 0;
|
|
412
|
+
let fullContent = '';
|
|
413
|
+
for await (const chunk of callAdapterStream(request)) {
|
|
414
|
+
chunkCount++;
|
|
415
|
+
if (chunk.content) {
|
|
416
|
+
fullContent += chunk.content;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
console.log(` Chunks received: ${chunkCount}`);
|
|
420
|
+
console.log(` Content: ${fullContent.trim()}`);
|
|
421
|
+
console.log(' ✅ Mistral streaming test passed\n');
|
|
422
|
+
}
|
|
423
|
+
// Test 11: Mistral with tool calls streaming
|
|
424
|
+
async function testMistralToolCallsStreaming() {
|
|
425
|
+
console.log('Test 11: Mistral Tool Calls Streaming');
|
|
426
|
+
console.log('-'.repeat(80));
|
|
427
|
+
const request = {
|
|
428
|
+
gateId: 'test-gate',
|
|
429
|
+
model: 'mistral-small-2501',
|
|
430
|
+
type: 'chat',
|
|
431
|
+
data: {
|
|
432
|
+
messages: [
|
|
433
|
+
{ role: 'user', content: 'What is the weather in Berlin?' }
|
|
434
|
+
],
|
|
435
|
+
tools: [
|
|
436
|
+
{
|
|
437
|
+
type: 'function',
|
|
438
|
+
function: {
|
|
439
|
+
name: 'get_weather',
|
|
440
|
+
description: 'Get weather for a location',
|
|
441
|
+
parameters: {
|
|
442
|
+
type: 'object',
|
|
443
|
+
properties: {
|
|
444
|
+
location: { type: 'string' },
|
|
445
|
+
},
|
|
446
|
+
required: ['location'],
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
stream: true,
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
let toolCallsFound = false;
|
|
455
|
+
let finishReason = null;
|
|
456
|
+
for await (const chunk of callAdapterStream(request)) {
|
|
457
|
+
if (chunk.toolCalls && chunk.toolCalls.length > 0) {
|
|
458
|
+
toolCallsFound = true;
|
|
459
|
+
}
|
|
460
|
+
if (chunk.finishReason) {
|
|
461
|
+
finishReason = chunk.finishReason;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
console.log(` Tool calls found: ${toolCallsFound}`);
|
|
465
|
+
console.log(` Finish reason: ${finishReason}`);
|
|
466
|
+
if (toolCallsFound) {
|
|
467
|
+
console.log(' ✅ Mistral tool calls streaming test passed\n');
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
console.log(' ⚠️ Tool calls may not have been invoked (model chose not to use tools)\n');
|
|
471
|
+
}
|
|
472
|
+
}
|
|
317
473
|
// Run all tests
|
|
318
474
|
(async () => {
|
|
319
475
|
try {
|
|
@@ -324,6 +480,10 @@ async function testMultiProviderFallback() {
|
|
|
324
480
|
await testClaudeStreaming();
|
|
325
481
|
await testClaudeToolCallsStreaming();
|
|
326
482
|
await testMultiProviderFallback();
|
|
483
|
+
await testGeminiStreaming();
|
|
484
|
+
await testGeminiToolCallsStreaming();
|
|
485
|
+
await testMistralStreaming();
|
|
486
|
+
await testMistralToolCallsStreaming();
|
|
327
487
|
console.log('='.repeat(80));
|
|
328
488
|
console.log('✅ ALL STREAMING ROUTE TESTS PASSED');
|
|
329
489
|
console.log('='.repeat(80));
|
|
@@ -12,7 +12,9 @@ export declare class GoogleAdapter extends BaseProviderAdapter {
|
|
|
12
12
|
resolution: string;
|
|
13
13
|
}>;
|
|
14
14
|
call(request: LayerRequest, userId?: string): Promise<LayerResponse>;
|
|
15
|
+
callStream(request: LayerRequest, userId?: string): AsyncIterable<LayerResponse>;
|
|
15
16
|
private handleChat;
|
|
17
|
+
private handleChatStream;
|
|
16
18
|
private handleImageGeneration;
|
|
17
19
|
private handleEmbeddings;
|
|
18
20
|
private handleVideoGeneration;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"google-adapter.d.ts","sourceRoot":"","sources":["../../../src/services/providers/google-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,yBAAyB,EAI1B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,YAAY,EACZ,aAAa,EACb,IAAI,EACJ,YAAY,EACZ,SAAS,EACV,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAExD,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"google-adapter.d.ts","sourceRoot":"","sources":["../../../src/services/providers/google-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,yBAAyB,EAI1B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,YAAY,EACZ,aAAa,EACb,IAAI,EACJ,YAAY,EACZ,SAAS,EACV,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAExD,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAgB1E,qBAAa,aAAc,SAAQ,mBAAmB;IACpD,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAmB;IAE/C,SAAS,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAQ1C;IAEF,SAAS,CAAC,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAO1D;IAEF,SAAS,CAAC,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,yBAAyB,CAAC,CAIrE;IAEF,SAAS,CAAC,eAAe,EAAE,MAAM,CAC/B,SAAS,EACT;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAC5C,CAKC;IAEI,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAmBnE,UAAU,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC,aAAa,CAAC;YAYzE,UAAU;YAwLT,gBAAgB;YAmNjB,qBAAqB;YAqCrB,gBAAgB;YAsChB,qBAAqB;YAsHrB,kBAAkB;IA2ChC,OAAO,CAAC,KAAK;CAGd"}
|
|
@@ -5,11 +5,9 @@ import { PROVIDER } from "../../lib/provider-constants.js";
|
|
|
5
5
|
import { resolveApiKey } from '../../lib/key-resolver.js';
|
|
6
6
|
let client = null;
|
|
7
7
|
function getGoogleClient(apiKey) {
|
|
8
|
-
// If custom API key provided, create new client
|
|
9
8
|
if (apiKey) {
|
|
10
9
|
return new GoogleGenAI({ apiKey });
|
|
11
10
|
}
|
|
12
|
-
// Otherwise use singleton with platform key
|
|
13
11
|
if (!client) {
|
|
14
12
|
client = new GoogleGenAI({ apiKey: process.env.GOOGLE_API_KEY || '' });
|
|
15
13
|
}
|
|
@@ -28,7 +26,6 @@ export class GoogleAdapter extends BaseProviderAdapter {
|
|
|
28
26
|
model: 'model',
|
|
29
27
|
developer: 'system',
|
|
30
28
|
};
|
|
31
|
-
// Map Google finish reasons to Layer finish reasons
|
|
32
29
|
this.finishReasonMappings = {
|
|
33
30
|
STOP: 'completed',
|
|
34
31
|
MAX_TOKENS: 'length_limit',
|
|
@@ -42,7 +39,6 @@ export class GoogleAdapter extends BaseProviderAdapter {
|
|
|
42
39
|
none: FunctionCallingConfigMode.NONE,
|
|
43
40
|
required: FunctionCallingConfigMode.ANY,
|
|
44
41
|
};
|
|
45
|
-
// Map Layer VideoSize to Veo aspect ratio and resolution
|
|
46
42
|
this.videoSizeConfig = {
|
|
47
43
|
'720x1280': { aspectRatio: '9:16', resolution: '720p' },
|
|
48
44
|
'1280x720': { aspectRatio: '16:9', resolution: '720p' },
|
|
@@ -51,7 +47,6 @@ export class GoogleAdapter extends BaseProviderAdapter {
|
|
|
51
47
|
};
|
|
52
48
|
}
|
|
53
49
|
async call(request, userId) {
|
|
54
|
-
// Resolve API key (BYOK → Platform key)
|
|
55
50
|
const resolved = await resolveApiKey(this.provider, userId, process.env.GOOGLE_API_KEY);
|
|
56
51
|
switch (request.type) {
|
|
57
52
|
case 'chat':
|
|
@@ -68,6 +63,16 @@ export class GoogleAdapter extends BaseProviderAdapter {
|
|
|
68
63
|
throw new Error(`Unknown modality: ${request.type}`);
|
|
69
64
|
}
|
|
70
65
|
}
|
|
66
|
+
async *callStream(request, userId) {
|
|
67
|
+
const resolved = await resolveApiKey(this.provider, userId, process.env.GOOGLE_API_KEY);
|
|
68
|
+
switch (request.type) {
|
|
69
|
+
case 'chat':
|
|
70
|
+
yield* this.handleChatStream(request, resolved.key, resolved.usedPlatformKey);
|
|
71
|
+
break;
|
|
72
|
+
default:
|
|
73
|
+
throw new Error(`Streaming not supported for type: ${request.type}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
71
76
|
async handleChat(request, apiKey, usedPlatformKey) {
|
|
72
77
|
const startTime = Date.now();
|
|
73
78
|
const client = getGoogleClient(apiKey);
|
|
@@ -224,6 +229,182 @@ export class GoogleAdapter extends BaseProviderAdapter {
|
|
|
224
229
|
raw: response,
|
|
225
230
|
};
|
|
226
231
|
}
|
|
232
|
+
async *handleChatStream(request, apiKey, usedPlatformKey) {
|
|
233
|
+
const startTime = Date.now();
|
|
234
|
+
const client = getGoogleClient(apiKey);
|
|
235
|
+
const { data: chat, model } = request;
|
|
236
|
+
if (!model) {
|
|
237
|
+
throw new Error('Model is required for chat completion');
|
|
238
|
+
}
|
|
239
|
+
const contents = [];
|
|
240
|
+
let systemInstruction;
|
|
241
|
+
if (chat.systemPrompt) {
|
|
242
|
+
systemInstruction = chat.systemPrompt;
|
|
243
|
+
}
|
|
244
|
+
for (const msg of chat.messages) {
|
|
245
|
+
const role = this.mapRole(msg.role);
|
|
246
|
+
if (role === 'system') {
|
|
247
|
+
systemInstruction = systemInstruction
|
|
248
|
+
? `${systemInstruction}\n${msg.content}`
|
|
249
|
+
: msg.content;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const parts = [];
|
|
253
|
+
if (msg.content) {
|
|
254
|
+
parts.push({ text: msg.content });
|
|
255
|
+
}
|
|
256
|
+
if (msg.images && msg.images.length > 0) {
|
|
257
|
+
for (const image of msg.images) {
|
|
258
|
+
if (image.base64) {
|
|
259
|
+
parts.push({
|
|
260
|
+
inlineData: {
|
|
261
|
+
mimeType: image.mimeType || 'image/jpeg',
|
|
262
|
+
data: image.base64,
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
else if (image.url) {
|
|
267
|
+
parts.push({
|
|
268
|
+
fileData: {
|
|
269
|
+
mimeType: image.mimeType || 'image/jpeg',
|
|
270
|
+
fileUri: image.url,
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (msg.toolCallId && msg.role === 'tool') {
|
|
277
|
+
if (!msg.name) {
|
|
278
|
+
throw new Error('Tool response messages must include the function name');
|
|
279
|
+
}
|
|
280
|
+
parts.push({
|
|
281
|
+
functionResponse: {
|
|
282
|
+
name: msg.name || msg.toolCallId,
|
|
283
|
+
response: { result: msg.content },
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
288
|
+
for (const toolCall of msg.toolCalls) {
|
|
289
|
+
parts.push({
|
|
290
|
+
functionCall: {
|
|
291
|
+
name: toolCall.function.name,
|
|
292
|
+
args: JSON.parse(toolCall.function.arguments),
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (parts.length > 0) {
|
|
298
|
+
contents.push({
|
|
299
|
+
role: role === 'model' ? 'model' : 'user',
|
|
300
|
+
parts,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
let googleTools;
|
|
305
|
+
if (chat.tools && chat.tools.length > 0) {
|
|
306
|
+
googleTools = [
|
|
307
|
+
{
|
|
308
|
+
functionDeclarations: chat.tools.map((tool) => ({
|
|
309
|
+
name: tool.function.name,
|
|
310
|
+
description: tool.function.description,
|
|
311
|
+
parametersJsonSchema: tool.function.parameters,
|
|
312
|
+
})),
|
|
313
|
+
},
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
let toolConfig;
|
|
317
|
+
if (chat.toolChoice) {
|
|
318
|
+
const mode = this.mapToolChoice(chat.toolChoice);
|
|
319
|
+
if (typeof mode === 'string') {
|
|
320
|
+
toolConfig = {
|
|
321
|
+
functionCallingConfig: { mode: mode },
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const stream = await client.models.generateContentStream({
|
|
326
|
+
model: model,
|
|
327
|
+
contents,
|
|
328
|
+
config: {
|
|
329
|
+
...(systemInstruction && { systemInstruction }),
|
|
330
|
+
...(googleTools && { tools: googleTools }),
|
|
331
|
+
...(toolConfig && { toolConfig }),
|
|
332
|
+
...(chat.temperature !== undefined && { temperature: chat.temperature }),
|
|
333
|
+
...(chat.maxTokens !== undefined && { maxOutputTokens: chat.maxTokens }),
|
|
334
|
+
...(chat.topP !== undefined && { topP: chat.topP }),
|
|
335
|
+
...(chat.stopSequences !== undefined && { stopSequences: chat.stopSequences }),
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
let promptTokens = 0;
|
|
339
|
+
let completionTokens = 0;
|
|
340
|
+
let totalTokens = 0;
|
|
341
|
+
let fullContent = '';
|
|
342
|
+
let currentToolCalls = [];
|
|
343
|
+
let finishReason = null;
|
|
344
|
+
let modelVersion;
|
|
345
|
+
for await (const chunk of stream) {
|
|
346
|
+
const candidate = chunk.candidates?.[0];
|
|
347
|
+
const content = candidate?.content;
|
|
348
|
+
const textChunk = content?.parts
|
|
349
|
+
?.filter((part) => 'text' in part)
|
|
350
|
+
.map((part) => part.text)
|
|
351
|
+
.join('');
|
|
352
|
+
if (textChunk) {
|
|
353
|
+
fullContent += textChunk;
|
|
354
|
+
yield {
|
|
355
|
+
content: textChunk,
|
|
356
|
+
model: model,
|
|
357
|
+
stream: true,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
const toolCallParts = content?.parts?.filter((part) => 'functionCall' in part);
|
|
361
|
+
if (toolCallParts && toolCallParts.length > 0) {
|
|
362
|
+
for (const part of toolCallParts) {
|
|
363
|
+
const fc = part.functionCall;
|
|
364
|
+
const existingCall = currentToolCalls.find(tc => tc.function.name === fc.name);
|
|
365
|
+
if (!existingCall) {
|
|
366
|
+
currentToolCalls.push({
|
|
367
|
+
id: `call_${currentToolCalls.length}_${fc.name}`,
|
|
368
|
+
type: 'function',
|
|
369
|
+
function: {
|
|
370
|
+
name: fc.name,
|
|
371
|
+
arguments: JSON.stringify(fc.args),
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (chunk.usageMetadata) {
|
|
378
|
+
promptTokens = chunk.usageMetadata.promptTokenCount || 0;
|
|
379
|
+
completionTokens = chunk.usageMetadata.candidatesTokenCount || 0;
|
|
380
|
+
totalTokens = chunk.usageMetadata.totalTokenCount || 0;
|
|
381
|
+
}
|
|
382
|
+
if (candidate?.finishReason) {
|
|
383
|
+
finishReason = candidate.finishReason;
|
|
384
|
+
}
|
|
385
|
+
if (chunk.modelVersion) {
|
|
386
|
+
modelVersion = chunk.modelVersion;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const cost = this.calculateCost(model, promptTokens, completionTokens);
|
|
390
|
+
const latencyMs = Date.now() - startTime;
|
|
391
|
+
yield {
|
|
392
|
+
content: '',
|
|
393
|
+
model: modelVersion || model,
|
|
394
|
+
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : undefined,
|
|
395
|
+
usage: {
|
|
396
|
+
promptTokens,
|
|
397
|
+
completionTokens,
|
|
398
|
+
totalTokens,
|
|
399
|
+
},
|
|
400
|
+
cost,
|
|
401
|
+
latencyMs,
|
|
402
|
+
usedPlatformKey,
|
|
403
|
+
stream: true,
|
|
404
|
+
finishReason: this.mapFinishReason(finishReason || 'STOP'),
|
|
405
|
+
rawFinishReason: finishReason || undefined,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
227
408
|
async handleImageGeneration(request, apiKey, usedPlatformKey) {
|
|
228
409
|
const startTime = Date.now();
|
|
229
410
|
const client = getGoogleClient(apiKey);
|
|
@@ -7,7 +7,9 @@ export declare class MistralAdapter extends BaseProviderAdapter {
|
|
|
7
7
|
protected finishReasonMappings: Record<string, FinishReason>;
|
|
8
8
|
protected toolChoiceMappings: Record<string, string>;
|
|
9
9
|
call(request: LayerRequest, userId?: string): Promise<LayerResponse>;
|
|
10
|
+
callStream(request: LayerRequest, userId?: string): AsyncIterable<LayerResponse>;
|
|
10
11
|
private handleChat;
|
|
12
|
+
private handleChatStream;
|
|
11
13
|
private handleEmbeddings;
|
|
12
14
|
private handleOCR;
|
|
13
15
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mistral-adapter.d.ts","sourceRoot":"","sources":["../../../src/services/providers/mistral-adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EACL,YAAY,EACZ,aAAa,EACb,IAAI,EACJ,YAAY,EAEb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAoB1E,qBAAa,cAAe,SAAQ,mBAAmB;IACrD,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAoB;IAEhD,SAAS,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAQ1C;IAGF,SAAS,CAAC,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAM1D;IAEF,SAAS,CAAC,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAIlD;IAEI,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"mistral-adapter.d.ts","sourceRoot":"","sources":["../../../src/services/providers/mistral-adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EACL,YAAY,EACZ,aAAa,EACb,IAAI,EACJ,YAAY,EAEb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAoB1E,qBAAa,cAAe,SAAQ,mBAAmB;IACrD,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAoB;IAEhD,SAAS,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAQ1C;IAGF,SAAS,CAAC,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAM1D;IAEF,SAAS,CAAC,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAIlD;IAEI,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IAsBnE,UAAU,CAAC,OAAO,EAAE,YAAY,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAAC,aAAa,CAAC;YAYzE,UAAU;YAiMT,gBAAgB;YAgPjB,gBAAgB;YA0ChB,SAAS;CA8ExB"}
|
|
@@ -64,6 +64,16 @@ export class MistralAdapter extends BaseProviderAdapter {
|
|
|
64
64
|
throw new Error(`Unknown modality: ${request.type}`);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
async *callStream(request, userId) {
|
|
68
|
+
const resolved = await resolveApiKey(this.provider, userId, process.env.MISTRAL_API_KEY);
|
|
69
|
+
switch (request.type) {
|
|
70
|
+
case 'chat':
|
|
71
|
+
yield* this.handleChatStream(request, resolved.key, resolved.usedPlatformKey);
|
|
72
|
+
break;
|
|
73
|
+
default:
|
|
74
|
+
throw new Error(`Streaming not supported for type: ${request.type}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
67
77
|
async handleChat(request, apiKey, usedPlatformKey) {
|
|
68
78
|
const startTime = Date.now();
|
|
69
79
|
const mistral = getMistralClient(apiKey);
|
|
@@ -218,6 +228,201 @@ export class MistralAdapter extends BaseProviderAdapter {
|
|
|
218
228
|
raw: response,
|
|
219
229
|
};
|
|
220
230
|
}
|
|
231
|
+
async *handleChatStream(request, apiKey, usedPlatformKey) {
|
|
232
|
+
const startTime = Date.now();
|
|
233
|
+
const mistral = getMistralClient(apiKey);
|
|
234
|
+
const { data: chat, model } = request;
|
|
235
|
+
if (!model) {
|
|
236
|
+
throw new Error('Model is required for chat completion');
|
|
237
|
+
}
|
|
238
|
+
// Build messages array (same as non-streaming)
|
|
239
|
+
const messages = [];
|
|
240
|
+
// Handle system prompt
|
|
241
|
+
if (chat.systemPrompt) {
|
|
242
|
+
messages.push({ role: 'system', content: chat.systemPrompt });
|
|
243
|
+
}
|
|
244
|
+
// Convert messages to Mistral format
|
|
245
|
+
for (const msg of chat.messages) {
|
|
246
|
+
const role = this.mapRole(msg.role);
|
|
247
|
+
// Handle vision messages (content + images)
|
|
248
|
+
if (msg.images && msg.images.length > 0 && role === 'user') {
|
|
249
|
+
const content = [];
|
|
250
|
+
if (msg.content) {
|
|
251
|
+
content.push({ type: 'text', text: msg.content });
|
|
252
|
+
}
|
|
253
|
+
for (const image of msg.images) {
|
|
254
|
+
const imageUrl = image.url || `data:${image.mimeType || 'image/jpeg'};base64,${image.base64}`;
|
|
255
|
+
content.push({
|
|
256
|
+
type: 'image_url',
|
|
257
|
+
imageUrl: imageUrl,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
messages.push({ role, content });
|
|
261
|
+
}
|
|
262
|
+
// Handle tool responses
|
|
263
|
+
else if (msg.toolCallId && role === 'tool') {
|
|
264
|
+
messages.push({
|
|
265
|
+
role: 'tool',
|
|
266
|
+
content: msg.content || '',
|
|
267
|
+
toolCallId: msg.toolCallId,
|
|
268
|
+
name: msg.name,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
// Handle assistant messages with tool calls
|
|
272
|
+
else if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
273
|
+
messages.push({
|
|
274
|
+
role: 'assistant',
|
|
275
|
+
content: msg.content || '',
|
|
276
|
+
toolCalls: msg.toolCalls.map((tc) => ({
|
|
277
|
+
id: tc.id,
|
|
278
|
+
type: 'function',
|
|
279
|
+
function: {
|
|
280
|
+
name: tc.function.name,
|
|
281
|
+
arguments: tc.function.arguments,
|
|
282
|
+
},
|
|
283
|
+
})),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
// Handle regular text messages
|
|
287
|
+
else {
|
|
288
|
+
messages.push({
|
|
289
|
+
role,
|
|
290
|
+
content: msg.content || '',
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Convert tools to Mistral format
|
|
295
|
+
let tools;
|
|
296
|
+
if (chat.tools && chat.tools.length > 0) {
|
|
297
|
+
tools = chat.tools.map((tool) => ({
|
|
298
|
+
type: 'function',
|
|
299
|
+
function: {
|
|
300
|
+
name: tool.function.name,
|
|
301
|
+
description: tool.function.description,
|
|
302
|
+
parameters: tool.function.parameters || {},
|
|
303
|
+
},
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
// Map tool choice
|
|
307
|
+
let toolChoice;
|
|
308
|
+
if (chat.toolChoice) {
|
|
309
|
+
if (typeof chat.toolChoice === 'object') {
|
|
310
|
+
toolChoice = chat.toolChoice;
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
const mapped = this.mapToolChoice(chat.toolChoice);
|
|
314
|
+
if (mapped === 'auto' || mapped === 'none' || mapped === 'any' || mapped === 'required') {
|
|
315
|
+
toolChoice = mapped;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const stream = await mistral.chat.stream({
|
|
320
|
+
model,
|
|
321
|
+
messages: messages,
|
|
322
|
+
...(chat.temperature !== undefined && { temperature: chat.temperature }),
|
|
323
|
+
...(chat.maxTokens !== undefined && { maxTokens: chat.maxTokens }),
|
|
324
|
+
...(chat.topP !== undefined && { topP: chat.topP }),
|
|
325
|
+
...(chat.stopSequences !== undefined && { stop: chat.stopSequences }),
|
|
326
|
+
...(chat.frequencyPenalty !== undefined && { frequencyPenalty: chat.frequencyPenalty }),
|
|
327
|
+
...(chat.presencePenalty !== undefined && { presencePenalty: chat.presencePenalty }),
|
|
328
|
+
...(chat.seed !== undefined && { randomSeed: chat.seed }),
|
|
329
|
+
...(tools && { tools }),
|
|
330
|
+
...(toolChoice && { toolChoice }),
|
|
331
|
+
...(chat.responseFormat && {
|
|
332
|
+
responseFormat: typeof chat.responseFormat === 'string'
|
|
333
|
+
? { type: chat.responseFormat }
|
|
334
|
+
: chat.responseFormat,
|
|
335
|
+
}),
|
|
336
|
+
});
|
|
337
|
+
let promptTokens = 0;
|
|
338
|
+
let completionTokens = 0;
|
|
339
|
+
let totalTokens = 0;
|
|
340
|
+
let fullContent = '';
|
|
341
|
+
let currentToolCalls = [];
|
|
342
|
+
let finishReason = null;
|
|
343
|
+
let modelVersion;
|
|
344
|
+
for await (const chunk of stream) {
|
|
345
|
+
// Mistral CompletionEvent can be of type 'chunk' or 'usage'
|
|
346
|
+
const event = chunk;
|
|
347
|
+
// Handle chunk events with choices
|
|
348
|
+
if (event.data?.choices) {
|
|
349
|
+
const choice = event.data.choices[0];
|
|
350
|
+
const delta = choice?.delta;
|
|
351
|
+
// Handle text content
|
|
352
|
+
if (delta?.content) {
|
|
353
|
+
const contentStr = typeof delta.content === 'string'
|
|
354
|
+
? delta.content
|
|
355
|
+
: Array.isArray(delta.content)
|
|
356
|
+
? delta.content.map((c) => c.text || c.content || '').join('')
|
|
357
|
+
: '';
|
|
358
|
+
if (contentStr) {
|
|
359
|
+
fullContent += contentStr;
|
|
360
|
+
yield {
|
|
361
|
+
content: contentStr,
|
|
362
|
+
model: model,
|
|
363
|
+
stream: true,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// Handle tool calls
|
|
368
|
+
if (delta?.toolCalls && delta.toolCalls.length > 0) {
|
|
369
|
+
for (const tc of delta.toolCalls) {
|
|
370
|
+
const existingCall = currentToolCalls.find(call => call.id === tc.id);
|
|
371
|
+
if (existingCall) {
|
|
372
|
+
// Append to existing tool call arguments
|
|
373
|
+
if (tc.function?.arguments) {
|
|
374
|
+
existingCall.function.arguments += tc.function.arguments;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
// New tool call
|
|
379
|
+
currentToolCalls.push({
|
|
380
|
+
id: tc.id || `call_${currentToolCalls.length}`,
|
|
381
|
+
type: 'function',
|
|
382
|
+
function: {
|
|
383
|
+
name: tc.function?.name || '',
|
|
384
|
+
arguments: tc.function?.arguments || '',
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
// Handle finish reason
|
|
391
|
+
if (choice?.finishReason) {
|
|
392
|
+
finishReason = choice.finishReason;
|
|
393
|
+
}
|
|
394
|
+
// Handle model version
|
|
395
|
+
if (event.data.model) {
|
|
396
|
+
modelVersion = event.data.model;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Handle usage events
|
|
400
|
+
if (event.data?.usage) {
|
|
401
|
+
promptTokens = event.data.usage.promptTokens || 0;
|
|
402
|
+
completionTokens = event.data.usage.completionTokens || 0;
|
|
403
|
+
totalTokens = event.data.usage.totalTokens || 0;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const cost = this.calculateCost(model, promptTokens, completionTokens);
|
|
407
|
+
const latencyMs = Date.now() - startTime;
|
|
408
|
+
// Yield final chunk with metadata
|
|
409
|
+
yield {
|
|
410
|
+
content: '',
|
|
411
|
+
model: modelVersion || model,
|
|
412
|
+
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : undefined,
|
|
413
|
+
usage: {
|
|
414
|
+
promptTokens,
|
|
415
|
+
completionTokens,
|
|
416
|
+
totalTokens,
|
|
417
|
+
},
|
|
418
|
+
cost,
|
|
419
|
+
latencyMs,
|
|
420
|
+
usedPlatformKey,
|
|
421
|
+
stream: true,
|
|
422
|
+
finishReason: this.mapFinishReason(finishReason || 'stop'),
|
|
423
|
+
rawFinishReason: finishReason || undefined,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
221
426
|
async handleEmbeddings(request, apiKey, usedPlatformKey) {
|
|
222
427
|
const startTime = Date.now();
|
|
223
428
|
const mistral = getMistralClient(apiKey);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-google-streaming.d.ts","sourceRoot":"","sources":["../../../../src/services/providers/tests/test-google-streaming.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { GoogleAdapter } from '../google-adapter.js';
|
|
2
|
+
const adapter = new GoogleAdapter();
|
|
3
|
+
async function testChatStreamingBasic() {
|
|
4
|
+
console.log('Testing basic chat streaming...');
|
|
5
|
+
const request = {
|
|
6
|
+
gateId: 'test-gate',
|
|
7
|
+
model: 'gemini-2.0-flash',
|
|
8
|
+
type: 'chat',
|
|
9
|
+
data: {
|
|
10
|
+
messages: [
|
|
11
|
+
{ role: 'user', content: 'Count from 1 to 5 slowly, one number per line.' }
|
|
12
|
+
],
|
|
13
|
+
temperature: 0.7,
|
|
14
|
+
maxTokens: 50,
|
|
15
|
+
stream: true,
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
if (!adapter.callStream) {
|
|
19
|
+
throw new Error('callStream method not available');
|
|
20
|
+
}
|
|
21
|
+
let chunkCount = 0;
|
|
22
|
+
let fullContent = '';
|
|
23
|
+
let finalUsage = null;
|
|
24
|
+
let finalCost = null;
|
|
25
|
+
console.log('\nStreaming chunks:');
|
|
26
|
+
console.log('---');
|
|
27
|
+
for await (const chunk of adapter.callStream(request)) {
|
|
28
|
+
chunkCount++;
|
|
29
|
+
if (chunk.content) {
|
|
30
|
+
process.stdout.write(chunk.content);
|
|
31
|
+
fullContent += chunk.content;
|
|
32
|
+
}
|
|
33
|
+
if (chunk.usage) {
|
|
34
|
+
finalUsage = chunk.usage;
|
|
35
|
+
}
|
|
36
|
+
if (chunk.cost) {
|
|
37
|
+
finalCost = chunk.cost;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
console.log('\n---\n');
|
|
41
|
+
console.log('Total chunks received:', chunkCount);
|
|
42
|
+
console.log('Full content:', fullContent);
|
|
43
|
+
console.log('Final usage:', finalUsage);
|
|
44
|
+
console.log('Final cost:', finalCost);
|
|
45
|
+
console.log('✅ Basic streaming test passed\n');
|
|
46
|
+
}
|
|
47
|
+
async function testChatStreamingWithToolCalls() {
|
|
48
|
+
console.log('Testing chat streaming with tool calls...');
|
|
49
|
+
const request = {
|
|
50
|
+
gateId: 'test-gate',
|
|
51
|
+
model: 'gemini-2.0-flash',
|
|
52
|
+
type: 'chat',
|
|
53
|
+
data: {
|
|
54
|
+
messages: [
|
|
55
|
+
{ role: 'user', content: 'What is the weather in San Francisco?' }
|
|
56
|
+
],
|
|
57
|
+
tools: [
|
|
58
|
+
{
|
|
59
|
+
type: 'function',
|
|
60
|
+
function: {
|
|
61
|
+
name: 'get_weather',
|
|
62
|
+
description: 'Get the current weather for a location',
|
|
63
|
+
parameters: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
location: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
description: 'The city and state, e.g. San Francisco, CA',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
required: ['location'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
toolChoice: 'auto',
|
|
77
|
+
stream: true,
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
if (!adapter.callStream) {
|
|
81
|
+
throw new Error('callStream method not available');
|
|
82
|
+
}
|
|
83
|
+
let toolCallsReceived = false;
|
|
84
|
+
for await (const chunk of adapter.callStream(request)) {
|
|
85
|
+
if (chunk.toolCalls && chunk.toolCalls.length > 0) {
|
|
86
|
+
console.log('Tool calls received:', JSON.stringify(chunk.toolCalls, null, 2));
|
|
87
|
+
toolCallsReceived = true;
|
|
88
|
+
}
|
|
89
|
+
if (chunk.finishReason === 'tool_call') {
|
|
90
|
+
console.log('Finish reason: tool_call');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!toolCallsReceived) {
|
|
94
|
+
console.warn('⚠️ No tool calls received (model may have chosen not to use tools)');
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.log('✅ Tool calls streaming test passed\n');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function testChatStreamingError() {
|
|
101
|
+
console.log('Testing streaming with invalid model (error handling)...');
|
|
102
|
+
const request = {
|
|
103
|
+
gateId: 'test-gate',
|
|
104
|
+
model: 'invalid-google-model-name',
|
|
105
|
+
type: 'chat',
|
|
106
|
+
data: {
|
|
107
|
+
messages: [
|
|
108
|
+
{ role: 'user', content: 'Hello' }
|
|
109
|
+
],
|
|
110
|
+
stream: true,
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
if (!adapter.callStream) {
|
|
114
|
+
throw new Error('callStream method not available');
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
for await (const chunk of adapter.callStream(request)) {
|
|
118
|
+
console.log('Received chunk:', chunk);
|
|
119
|
+
}
|
|
120
|
+
console.error('❌ Should have thrown an error for invalid model');
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
console.log('✅ Correctly threw error:', error instanceof Error ? error.message : error);
|
|
124
|
+
console.log('✅ Error handling test passed\n');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Run all tests
|
|
128
|
+
(async () => {
|
|
129
|
+
try {
|
|
130
|
+
await testChatStreamingBasic();
|
|
131
|
+
await testChatStreamingWithToolCalls();
|
|
132
|
+
await testChatStreamingError();
|
|
133
|
+
console.log('✅ All streaming tests completed successfully!');
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
console.error('❌ Test failed:', error);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-mistral-streaming.d.ts","sourceRoot":"","sources":["../../../../src/services/providers/tests/test-mistral-streaming.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { MistralAdapter } from '../mistral-adapter.js';
|
|
2
|
+
const adapter = new MistralAdapter();
|
|
3
|
+
async function testChatStreamingBasic() {
|
|
4
|
+
console.log('Testing basic chat streaming...');
|
|
5
|
+
const request = {
|
|
6
|
+
gateId: 'test-gate',
|
|
7
|
+
model: 'mistral-small-2501',
|
|
8
|
+
type: 'chat',
|
|
9
|
+
data: {
|
|
10
|
+
messages: [
|
|
11
|
+
{ role: 'user', content: 'Count from 1 to 5 slowly, one number per line.' }
|
|
12
|
+
],
|
|
13
|
+
temperature: 0.7,
|
|
14
|
+
maxTokens: 50,
|
|
15
|
+
stream: true,
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
if (!adapter.callStream) {
|
|
19
|
+
throw new Error('callStream method not available');
|
|
20
|
+
}
|
|
21
|
+
let chunkCount = 0;
|
|
22
|
+
let fullContent = '';
|
|
23
|
+
let finalUsage = null;
|
|
24
|
+
let finalCost = null;
|
|
25
|
+
console.log('\nStreaming chunks:');
|
|
26
|
+
console.log('---');
|
|
27
|
+
for await (const chunk of adapter.callStream(request)) {
|
|
28
|
+
chunkCount++;
|
|
29
|
+
if (chunk.content) {
|
|
30
|
+
process.stdout.write(chunk.content);
|
|
31
|
+
fullContent += chunk.content;
|
|
32
|
+
}
|
|
33
|
+
if (chunk.usage) {
|
|
34
|
+
finalUsage = chunk.usage;
|
|
35
|
+
}
|
|
36
|
+
if (chunk.cost) {
|
|
37
|
+
finalCost = chunk.cost;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
console.log('\n---\n');
|
|
41
|
+
console.log('Total chunks received:', chunkCount);
|
|
42
|
+
console.log('Full content:', fullContent);
|
|
43
|
+
console.log('Final usage:', finalUsage);
|
|
44
|
+
console.log('Final cost:', finalCost);
|
|
45
|
+
console.log('✅ Basic streaming test passed\n');
|
|
46
|
+
}
|
|
47
|
+
async function testChatStreamingWithToolCalls() {
|
|
48
|
+
console.log('Testing chat streaming with tool calls...');
|
|
49
|
+
const request = {
|
|
50
|
+
gateId: 'test-gate',
|
|
51
|
+
model: 'mistral-small-2501',
|
|
52
|
+
type: 'chat',
|
|
53
|
+
data: {
|
|
54
|
+
messages: [
|
|
55
|
+
{ role: 'user', content: 'What is the weather in Paris?' }
|
|
56
|
+
],
|
|
57
|
+
tools: [
|
|
58
|
+
{
|
|
59
|
+
type: 'function',
|
|
60
|
+
function: {
|
|
61
|
+
name: 'get_weather',
|
|
62
|
+
description: 'Get the current weather for a location',
|
|
63
|
+
parameters: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
location: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
description: 'The city and state, e.g. Paris, France',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
required: ['location'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
toolChoice: 'auto',
|
|
77
|
+
stream: true,
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
if (!adapter.callStream) {
|
|
81
|
+
throw new Error('callStream method not available');
|
|
82
|
+
}
|
|
83
|
+
let toolCallsReceived = false;
|
|
84
|
+
for await (const chunk of adapter.callStream(request)) {
|
|
85
|
+
if (chunk.toolCalls && chunk.toolCalls.length > 0) {
|
|
86
|
+
console.log('Tool calls received:', JSON.stringify(chunk.toolCalls, null, 2));
|
|
87
|
+
toolCallsReceived = true;
|
|
88
|
+
}
|
|
89
|
+
if (chunk.finishReason === 'tool_call') {
|
|
90
|
+
console.log('Finish reason: tool_call');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (!toolCallsReceived) {
|
|
94
|
+
console.warn('⚠️ No tool calls received (model may have chosen not to use tools)');
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.log('✅ Tool calls streaming test passed\n');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function testChatStreamingError() {
|
|
101
|
+
console.log('Testing streaming with invalid model (error handling)...');
|
|
102
|
+
const request = {
|
|
103
|
+
gateId: 'test-gate',
|
|
104
|
+
model: 'invalid-mistral-model-name',
|
|
105
|
+
type: 'chat',
|
|
106
|
+
data: {
|
|
107
|
+
messages: [
|
|
108
|
+
{ role: 'user', content: 'Hello' }
|
|
109
|
+
],
|
|
110
|
+
stream: true,
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
if (!adapter.callStream) {
|
|
114
|
+
throw new Error('callStream method not available');
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
for await (const chunk of adapter.callStream(request)) {
|
|
118
|
+
console.log('Received chunk:', chunk);
|
|
119
|
+
}
|
|
120
|
+
console.error('❌ Should have thrown an error for invalid model');
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
console.log('✅ Correctly threw error:', error instanceof Error ? error.message : error);
|
|
124
|
+
console.log('✅ Error handling test passed\n');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Run all tests
|
|
128
|
+
(async () => {
|
|
129
|
+
try {
|
|
130
|
+
await testChatStreamingBasic();
|
|
131
|
+
await testChatStreamingWithToolCalls();
|
|
132
|
+
await testChatStreamingError();
|
|
133
|
+
console.log('✅ All streaming tests completed successfully!');
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
console.error('❌ Test failed:', error);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
})();
|