@layer-ai/core 2.0.16 → 2.0.17
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 +122 -0
- package/dist/services/providers/anthropic-adapter.d.ts +2 -0
- package/dist/services/providers/anthropic-adapter.d.ts.map +1 -1
- package/dist/services/providers/anthropic-adapter.js +163 -3
- package/dist/services/providers/tests/test-anthropic-streaming.d.ts +2 -0
- package/dist/services/providers/tests/test-anthropic-streaming.d.ts.map +1 -0
- package/dist/services/providers/tests/test-anthropic-streaming.js +139 -0
- package/package.json +1 -1
|
@@ -195,6 +195,125 @@ async function testStreamingWithTools() {
|
|
|
195
195
|
console.log(' ⚠️ Tool calls may not have been invoked (model chose not to use tools)\n');
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
|
+
// Test 5: Claude/Anthropic streaming
|
|
199
|
+
async function testClaudeStreaming() {
|
|
200
|
+
console.log('Test 5: Claude/Anthropic Streaming');
|
|
201
|
+
console.log('-'.repeat(80));
|
|
202
|
+
const request = {
|
|
203
|
+
gateId: 'test-gate',
|
|
204
|
+
model: 'claude-3-7-sonnet-20250219',
|
|
205
|
+
type: 'chat',
|
|
206
|
+
data: {
|
|
207
|
+
messages: [
|
|
208
|
+
{ role: 'user', content: 'Say "claude test passed" and nothing else.' }
|
|
209
|
+
],
|
|
210
|
+
maxTokens: 20,
|
|
211
|
+
stream: true,
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
let chunkCount = 0;
|
|
215
|
+
let fullContent = '';
|
|
216
|
+
for await (const chunk of callAdapterStream(request)) {
|
|
217
|
+
chunkCount++;
|
|
218
|
+
if (chunk.content) {
|
|
219
|
+
fullContent += chunk.content;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
console.log(` Chunks received: ${chunkCount}`);
|
|
223
|
+
console.log(` Content: ${fullContent.trim()}`);
|
|
224
|
+
console.log(' ✅ Claude streaming test passed\n');
|
|
225
|
+
}
|
|
226
|
+
// Test 6: Claude with tool calls streaming
|
|
227
|
+
async function testClaudeToolCallsStreaming() {
|
|
228
|
+
console.log('Test 6: Claude Tool Calls Streaming');
|
|
229
|
+
console.log('-'.repeat(80));
|
|
230
|
+
const request = {
|
|
231
|
+
gateId: 'test-gate',
|
|
232
|
+
model: 'claude-3-7-sonnet-20250219',
|
|
233
|
+
type: 'chat',
|
|
234
|
+
data: {
|
|
235
|
+
messages: [
|
|
236
|
+
{ role: 'user', content: 'What is the weather in Tokyo?' }
|
|
237
|
+
],
|
|
238
|
+
tools: [
|
|
239
|
+
{
|
|
240
|
+
type: 'function',
|
|
241
|
+
function: {
|
|
242
|
+
name: 'get_weather',
|
|
243
|
+
description: 'Get weather for a location',
|
|
244
|
+
parameters: {
|
|
245
|
+
type: 'object',
|
|
246
|
+
properties: {
|
|
247
|
+
location: { type: 'string' },
|
|
248
|
+
},
|
|
249
|
+
required: ['location'],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
stream: true,
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
let toolCallsFound = false;
|
|
258
|
+
let finishReason = null;
|
|
259
|
+
for await (const chunk of callAdapterStream(request)) {
|
|
260
|
+
if (chunk.toolCalls && chunk.toolCalls.length > 0) {
|
|
261
|
+
toolCallsFound = true;
|
|
262
|
+
}
|
|
263
|
+
if (chunk.finishReason) {
|
|
264
|
+
finishReason = chunk.finishReason;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
console.log(` Tool calls found: ${toolCallsFound}`);
|
|
268
|
+
console.log(` Finish reason: ${finishReason}`);
|
|
269
|
+
if (toolCallsFound && finishReason === 'tool_call') {
|
|
270
|
+
console.log(' ✅ Claude tool calls streaming test passed\n');
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
console.log(' ⚠️ Tool calls may not have been invoked (model chose not to use tools)\n');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Test 7: Multi-provider fallback with streaming (OpenAI -> Claude)
|
|
277
|
+
async function testMultiProviderFallback() {
|
|
278
|
+
console.log('Test 7: Multi-Provider Fallback (OpenAI -> Claude) with Streaming');
|
|
279
|
+
console.log('-'.repeat(80));
|
|
280
|
+
const request = {
|
|
281
|
+
gateId: 'test-gate',
|
|
282
|
+
model: 'invalid-openai-model',
|
|
283
|
+
type: 'chat',
|
|
284
|
+
data: {
|
|
285
|
+
messages: [
|
|
286
|
+
{ role: 'user', content: 'Say "multi-provider fallback worked" and nothing else.' }
|
|
287
|
+
],
|
|
288
|
+
maxTokens: 15,
|
|
289
|
+
stream: true,
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
const modelsToTry = [
|
|
293
|
+
'invalid-openai-model',
|
|
294
|
+
'claude-3-7-sonnet-20250219',
|
|
295
|
+
];
|
|
296
|
+
let chunkCount = 0;
|
|
297
|
+
let fullContent = '';
|
|
298
|
+
let succeeded = false;
|
|
299
|
+
try {
|
|
300
|
+
for await (const chunk of executeWithFallbackStream(request, modelsToTry)) {
|
|
301
|
+
chunkCount++;
|
|
302
|
+
if (chunk.content) {
|
|
303
|
+
fullContent += chunk.content;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
succeeded = true;
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
console.error(' ❌ Multi-provider fallback failed:', error instanceof Error ? error.message : error);
|
|
310
|
+
}
|
|
311
|
+
if (succeeded) {
|
|
312
|
+
console.log(` Chunks received: ${chunkCount}`);
|
|
313
|
+
console.log(` Content: ${fullContent.trim()}`);
|
|
314
|
+
console.log(' ✅ Multi-provider fallback test passed\n');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
198
317
|
// Run all tests
|
|
199
318
|
(async () => {
|
|
200
319
|
try {
|
|
@@ -202,6 +321,9 @@ async function testStreamingWithTools() {
|
|
|
202
321
|
await testFallbackRouting();
|
|
203
322
|
await testRoundRobinRouting();
|
|
204
323
|
await testStreamingWithTools();
|
|
324
|
+
await testClaudeStreaming();
|
|
325
|
+
await testClaudeToolCallsStreaming();
|
|
326
|
+
await testMultiProviderFallback();
|
|
205
327
|
console.log('='.repeat(80));
|
|
206
328
|
console.log('✅ ALL STREAMING ROUTE TESTS PASSED');
|
|
207
329
|
console.log('='.repeat(80));
|
|
@@ -8,6 +8,8 @@ export declare class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
8
8
|
protected finishReasonMappings: Record<string, FinishReason>;
|
|
9
9
|
protected mapToolChoice(choice: ToolChoice): string | object | undefined;
|
|
10
10
|
call(request: LayerRequest, userId?: string): Promise<LayerResponse>;
|
|
11
|
+
callStream(request: LayerRequest, userId?: string): AsyncIterable<LayerResponse>;
|
|
11
12
|
private handleChat;
|
|
13
|
+
private handleChatStream;
|
|
12
14
|
}
|
|
13
15
|
//# sourceMappingURL=anthropic-adapter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anthropic-adapter.d.ts","sourceRoot":"","sources":["../../../src/services/providers/anthropic-adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAmB,MAAM,mBAAmB,CAAC;AACzE,OAAO,EACL,YAAY,EACZ,aAAa,EACb,IAAI,EACJ,YAAY,EACZ,UAAU,EACX,MAAM,eAAe,CAAC;AACvB,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"anthropic-adapter.d.ts","sourceRoot":"","sources":["../../../src/services/providers/anthropic-adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAmB,MAAM,mBAAmB,CAAC;AACzE,OAAO,EACL,YAAY,EACZ,aAAa,EACb,IAAI,EACJ,YAAY,EACZ,UAAU,EACX,MAAM,eAAe,CAAC;AACvB,OAAO,EAAY,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAkB1E,qBAAa,gBAAiB,SAAQ,mBAAmB;IACvD,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAsB;IAElD,SAAS,CAAC,YAAY,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAQ1C;IAEF,SAAS,CAAC,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAI3D;IAEF,SAAS,CAAC,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAM1D;IAEF,SAAS,CAAC,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS;IAalE,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;YA8JT,gBAAgB;CA0KhC"}
|
|
@@ -4,11 +4,9 @@ import { PROVIDER } from "../../lib/provider-constants.js";
|
|
|
4
4
|
import { resolveApiKey } from '../../lib/key-resolver.js';
|
|
5
5
|
let anthropic = null;
|
|
6
6
|
function getAnthropicClient(apiKey) {
|
|
7
|
-
// If custom API key provided, create new client
|
|
8
7
|
if (apiKey) {
|
|
9
8
|
return new Anthropic({ apiKey });
|
|
10
9
|
}
|
|
11
|
-
// Otherwise use singleton with platform key
|
|
12
10
|
if (!anthropic) {
|
|
13
11
|
anthropic = new Anthropic({
|
|
14
12
|
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
@@ -54,7 +52,6 @@ export class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
54
52
|
return super.mapToolChoice(choice);
|
|
55
53
|
}
|
|
56
54
|
async call(request, userId) {
|
|
57
|
-
// Resolve API key (BYOK → Platform key)
|
|
58
55
|
const resolved = await resolveApiKey(this.provider, userId, process.env.ANTHROPIC_API_KEY);
|
|
59
56
|
switch (request.type) {
|
|
60
57
|
case 'chat':
|
|
@@ -71,6 +68,16 @@ export class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
71
68
|
throw new Error(`Unknown modality: ${request.type}`);
|
|
72
69
|
}
|
|
73
70
|
}
|
|
71
|
+
async *callStream(request, userId) {
|
|
72
|
+
const resolved = await resolveApiKey(this.provider, userId, process.env.ANTHROPIC_API_KEY);
|
|
73
|
+
switch (request.type) {
|
|
74
|
+
case 'chat':
|
|
75
|
+
yield* this.handleChatStream(request, resolved.key, resolved.usedPlatformKey);
|
|
76
|
+
break;
|
|
77
|
+
default:
|
|
78
|
+
throw new Error(`Streaming not supported for type: ${request.type}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
74
81
|
async handleChat(request, apiKey, usedPlatformKey) {
|
|
75
82
|
const startTime = Date.now();
|
|
76
83
|
const client = getAnthropicClient(apiKey);
|
|
@@ -215,4 +222,157 @@ export class AnthropicAdapter extends BaseProviderAdapter {
|
|
|
215
222
|
raw: response,
|
|
216
223
|
};
|
|
217
224
|
}
|
|
225
|
+
async *handleChatStream(request, apiKey, usedPlatformKey) {
|
|
226
|
+
const startTime = Date.now();
|
|
227
|
+
const client = getAnthropicClient(apiKey);
|
|
228
|
+
const { data: chat, model } = request;
|
|
229
|
+
if (!model) {
|
|
230
|
+
throw new Error('Model is required for chat completions');
|
|
231
|
+
}
|
|
232
|
+
const systemPrompt = chat.systemPrompt || undefined;
|
|
233
|
+
const messages = [];
|
|
234
|
+
for (const msg of chat.messages) {
|
|
235
|
+
if (msg.role === 'system')
|
|
236
|
+
continue;
|
|
237
|
+
const role = this.mapRole(msg.role);
|
|
238
|
+
if (msg.toolCallId) {
|
|
239
|
+
messages.push({
|
|
240
|
+
role: 'user',
|
|
241
|
+
content: [{
|
|
242
|
+
type: 'tool_result',
|
|
243
|
+
tool_use_id: msg.toolCallId,
|
|
244
|
+
content: msg.content || '',
|
|
245
|
+
}],
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
else if (msg.images?.length || msg.toolCalls?.length) {
|
|
249
|
+
const content = [];
|
|
250
|
+
if (msg.content) {
|
|
251
|
+
content.push({ type: 'text', text: msg.content });
|
|
252
|
+
}
|
|
253
|
+
if (msg.images) {
|
|
254
|
+
for (const image of msg.images) {
|
|
255
|
+
if (image.url) {
|
|
256
|
+
content.push({
|
|
257
|
+
type: 'image',
|
|
258
|
+
source: {
|
|
259
|
+
type: 'url',
|
|
260
|
+
url: image.url,
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
else if (image.base64) {
|
|
265
|
+
content.push({
|
|
266
|
+
type: 'image',
|
|
267
|
+
source: {
|
|
268
|
+
type: 'base64',
|
|
269
|
+
media_type: image.mimeType || 'image/jpeg',
|
|
270
|
+
data: image.base64
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (msg.toolCalls) {
|
|
277
|
+
for (const toolCall of msg.toolCalls) {
|
|
278
|
+
content.push({
|
|
279
|
+
type: 'tool_use',
|
|
280
|
+
id: toolCall.id,
|
|
281
|
+
name: toolCall.function.name,
|
|
282
|
+
input: JSON.parse(toolCall.function.arguments),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const messageRole = msg.images?.length ? 'user' : (msg.toolCalls?.length ? 'assistant' : role);
|
|
287
|
+
messages.push({ role: messageRole, content });
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
messages.push({
|
|
291
|
+
role: role,
|
|
292
|
+
content: msg.content || '',
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const anthropicRequest = {
|
|
297
|
+
model: model,
|
|
298
|
+
messages,
|
|
299
|
+
max_tokens: chat.maxTokens || 4096,
|
|
300
|
+
stream: true,
|
|
301
|
+
...(systemPrompt && { system: systemPrompt }),
|
|
302
|
+
...(chat.temperature !== undefined && { temperature: chat.temperature }),
|
|
303
|
+
...(chat.temperature === undefined && chat.topP !== undefined && { top_p: chat.topP }),
|
|
304
|
+
...(chat.stopSequences && { stop_sequences: chat.stopSequences }),
|
|
305
|
+
...(chat.tools && {
|
|
306
|
+
tools: chat.tools.map(tool => ({
|
|
307
|
+
name: tool.function.name,
|
|
308
|
+
description: tool.function.description,
|
|
309
|
+
input_schema: tool.function.parameters || { type: 'object', properties: {} },
|
|
310
|
+
})),
|
|
311
|
+
...(chat.toolChoice && { tool_choice: this.mapToolChoice(chat.toolChoice) }),
|
|
312
|
+
}),
|
|
313
|
+
};
|
|
314
|
+
const stream = client.messages.stream(anthropicRequest);
|
|
315
|
+
let promptTokens = 0;
|
|
316
|
+
let completionTokens = 0;
|
|
317
|
+
let fullContent = '';
|
|
318
|
+
let currentToolCalls = [];
|
|
319
|
+
let stopReason = null;
|
|
320
|
+
for await (const event of stream) {
|
|
321
|
+
if (event.type === 'content_block_start') {
|
|
322
|
+
const block = event.content_block;
|
|
323
|
+
if (block.type === 'tool_use') {
|
|
324
|
+
currentToolCalls.push({
|
|
325
|
+
id: block.id,
|
|
326
|
+
type: 'function',
|
|
327
|
+
function: {
|
|
328
|
+
name: block.name,
|
|
329
|
+
arguments: '',
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
else if (event.type === 'content_block_delta') {
|
|
335
|
+
const delta = event.delta;
|
|
336
|
+
if (delta.type === 'text_delta') {
|
|
337
|
+
fullContent += delta.text;
|
|
338
|
+
yield {
|
|
339
|
+
content: delta.text,
|
|
340
|
+
model: model,
|
|
341
|
+
stream: true,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
else if (delta.type === 'input_json_delta') {
|
|
345
|
+
if (currentToolCalls.length > 0) {
|
|
346
|
+
const lastToolCall = currentToolCalls[currentToolCalls.length - 1];
|
|
347
|
+
lastToolCall.function.arguments += delta.partial_json;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
else if (event.type === 'message_start') {
|
|
352
|
+
promptTokens = event.message.usage.input_tokens;
|
|
353
|
+
}
|
|
354
|
+
else if (event.type === 'message_delta') {
|
|
355
|
+
completionTokens = event.usage.output_tokens;
|
|
356
|
+
stopReason = event.delta.stop_reason || stopReason;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const cost = this.calculateCost(model, promptTokens, completionTokens);
|
|
360
|
+
const latencyMs = Date.now() - startTime;
|
|
361
|
+
yield {
|
|
362
|
+
content: '',
|
|
363
|
+
model: model,
|
|
364
|
+
toolCalls: currentToolCalls.length > 0 ? currentToolCalls : undefined,
|
|
365
|
+
usage: {
|
|
366
|
+
promptTokens,
|
|
367
|
+
completionTokens,
|
|
368
|
+
totalTokens: promptTokens + completionTokens,
|
|
369
|
+
},
|
|
370
|
+
cost,
|
|
371
|
+
latencyMs,
|
|
372
|
+
usedPlatformKey,
|
|
373
|
+
stream: true,
|
|
374
|
+
finishReason: this.mapFinishReason(stopReason || 'end_turn'),
|
|
375
|
+
rawFinishReason: stopReason || undefined,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
218
378
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-anthropic-streaming.d.ts","sourceRoot":"","sources":["../../../../src/services/providers/tests/test-anthropic-streaming.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { AnthropicAdapter } from '../anthropic-adapter.js';
|
|
2
|
+
const adapter = new AnthropicAdapter();
|
|
3
|
+
async function testChatStreamingBasic() {
|
|
4
|
+
console.log('Testing basic chat streaming...');
|
|
5
|
+
const request = {
|
|
6
|
+
gateId: 'test-gate',
|
|
7
|
+
model: 'claude-3-7-sonnet-20250219',
|
|
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: 'claude-3-7-sonnet-20250219',
|
|
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-model-name-that-does-not-exist',
|
|
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
|
+
})();
|