@lobehub/chat 1.55.0 → 1.55.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/changelog/v1.json +12 -0
- package/docs/self-hosting/platform/tencentcloud-lighthouse.mdx +33 -0
- package/docs/self-hosting/platform/tencentcloud-lighthouse.zh-CN.mdx +33 -0
- package/docs/self-hosting/start.zh-CN.mdx +3 -1
- package/package.json +1 -3
- package/src/config/aiModels/openrouter.ts +30 -0
- package/src/config/modelProviders/openrouter.ts +9 -0
- package/src/libs/agent-runtime/AgentRuntime.test.ts +1 -0
- package/src/libs/agent-runtime/azureOpenai/index.test.ts +47 -9
- package/src/libs/agent-runtime/azureOpenai/index.ts +35 -28
- package/src/libs/agent-runtime/utils/streams/index.ts +0 -1
- package/src/server/modules/AgentRuntime/index.test.ts +3 -1
- package/src/server/routers/lambda/aiModel.test.ts +240 -0
- package/src/store/aiInfra/slices/aiModel/selectors.test.ts +228 -0
- package/src/libs/agent-runtime/utils/streams/azureOpenai.test.ts +0 -536
- package/src/libs/agent-runtime/utils/streams/azureOpenai.ts +0 -83
@@ -1,536 +0,0 @@
|
|
1
|
-
import { describe, expect, it, vi } from 'vitest';
|
2
|
-
|
3
|
-
import { AzureOpenAIStream } from './azureOpenai';
|
4
|
-
|
5
|
-
describe('AzureOpenAIStream', () => {
|
6
|
-
it('should transform AzureOpenAI stream to protocol stream', async () => {
|
7
|
-
const mockOpenAIStream = new ReadableStream({
|
8
|
-
start(controller) {
|
9
|
-
controller.enqueue({
|
10
|
-
choices: [
|
11
|
-
{
|
12
|
-
delta: { content: 'Hello' },
|
13
|
-
index: 0,
|
14
|
-
},
|
15
|
-
],
|
16
|
-
id: '1',
|
17
|
-
});
|
18
|
-
controller.enqueue({
|
19
|
-
choices: [
|
20
|
-
{
|
21
|
-
delta: { content: ' world!' },
|
22
|
-
index: 1,
|
23
|
-
},
|
24
|
-
],
|
25
|
-
id: '1',
|
26
|
-
});
|
27
|
-
controller.enqueue({
|
28
|
-
choices: [
|
29
|
-
{
|
30
|
-
delta: null,
|
31
|
-
finishReason: 'stop',
|
32
|
-
index: 2,
|
33
|
-
},
|
34
|
-
],
|
35
|
-
id: '1',
|
36
|
-
});
|
37
|
-
|
38
|
-
controller.close();
|
39
|
-
},
|
40
|
-
});
|
41
|
-
|
42
|
-
const onStartMock = vi.fn();
|
43
|
-
const onTextMock = vi.fn();
|
44
|
-
const onTokenMock = vi.fn();
|
45
|
-
const onCompletionMock = vi.fn();
|
46
|
-
|
47
|
-
const protocolStream = AzureOpenAIStream(mockOpenAIStream, {
|
48
|
-
onStart: onStartMock,
|
49
|
-
onText: onTextMock,
|
50
|
-
onToken: onTokenMock,
|
51
|
-
onCompletion: onCompletionMock,
|
52
|
-
});
|
53
|
-
|
54
|
-
const decoder = new TextDecoder();
|
55
|
-
const chunks = [];
|
56
|
-
|
57
|
-
// @ts-ignore
|
58
|
-
for await (const chunk of protocolStream) {
|
59
|
-
chunks.push(decoder.decode(chunk, { stream: true }));
|
60
|
-
}
|
61
|
-
|
62
|
-
expect(chunks).toEqual([
|
63
|
-
'id: 1\n',
|
64
|
-
'event: text\n',
|
65
|
-
`data: "Hello"\n\n`,
|
66
|
-
'id: 1\n',
|
67
|
-
'event: text\n',
|
68
|
-
`data: " world!"\n\n`,
|
69
|
-
'id: 1\n',
|
70
|
-
'event: stop\n',
|
71
|
-
`data: "stop"\n\n`,
|
72
|
-
]);
|
73
|
-
|
74
|
-
expect(onStartMock).toHaveBeenCalledTimes(1);
|
75
|
-
expect(onTextMock).toHaveBeenNthCalledWith(1, '"Hello"');
|
76
|
-
expect(onTextMock).toHaveBeenNthCalledWith(2, '" world!"');
|
77
|
-
expect(onTokenMock).toHaveBeenCalledTimes(2);
|
78
|
-
expect(onCompletionMock).toHaveBeenCalledTimes(1);
|
79
|
-
});
|
80
|
-
|
81
|
-
it('should handle empty stream', async () => {
|
82
|
-
const mockStream = new ReadableStream({
|
83
|
-
start(controller) {
|
84
|
-
controller.close();
|
85
|
-
},
|
86
|
-
});
|
87
|
-
|
88
|
-
const protocolStream = AzureOpenAIStream(mockStream);
|
89
|
-
|
90
|
-
const decoder = new TextDecoder();
|
91
|
-
const chunks = [];
|
92
|
-
|
93
|
-
// @ts-ignore
|
94
|
-
for await (const chunk of protocolStream) {
|
95
|
-
chunks.push(decoder.decode(chunk, { stream: true }));
|
96
|
-
}
|
97
|
-
|
98
|
-
expect(chunks).toEqual([]);
|
99
|
-
});
|
100
|
-
|
101
|
-
it('should handle delta content null', async () => {
|
102
|
-
const mockOpenAIStream = new ReadableStream({
|
103
|
-
start(controller) {
|
104
|
-
controller.enqueue({
|
105
|
-
choices: [
|
106
|
-
{
|
107
|
-
delta: { content: null },
|
108
|
-
index: 0,
|
109
|
-
},
|
110
|
-
],
|
111
|
-
id: '3',
|
112
|
-
});
|
113
|
-
|
114
|
-
controller.close();
|
115
|
-
},
|
116
|
-
});
|
117
|
-
|
118
|
-
const protocolStream = AzureOpenAIStream(mockOpenAIStream);
|
119
|
-
|
120
|
-
const decoder = new TextDecoder();
|
121
|
-
const chunks = [];
|
122
|
-
|
123
|
-
// @ts-ignore
|
124
|
-
for await (const chunk of protocolStream) {
|
125
|
-
chunks.push(decoder.decode(chunk, { stream: true }));
|
126
|
-
}
|
127
|
-
|
128
|
-
expect(chunks).toEqual(['id: 3\n', 'event: data\n', `data: {"content":null}\n\n`]);
|
129
|
-
});
|
130
|
-
|
131
|
-
it('should handle other delta data', async () => {
|
132
|
-
const mockOpenAIStream = new ReadableStream({
|
133
|
-
start(controller) {
|
134
|
-
controller.enqueue({
|
135
|
-
choices: [
|
136
|
-
{
|
137
|
-
delta: { custom_field: 'custom_value' },
|
138
|
-
index: 0,
|
139
|
-
},
|
140
|
-
],
|
141
|
-
id: '4',
|
142
|
-
});
|
143
|
-
|
144
|
-
controller.close();
|
145
|
-
},
|
146
|
-
});
|
147
|
-
|
148
|
-
const protocolStream = AzureOpenAIStream(mockOpenAIStream);
|
149
|
-
|
150
|
-
const decoder = new TextDecoder();
|
151
|
-
const chunks = [];
|
152
|
-
|
153
|
-
// @ts-ignore
|
154
|
-
for await (const chunk of protocolStream) {
|
155
|
-
chunks.push(decoder.decode(chunk, { stream: true }));
|
156
|
-
}
|
157
|
-
|
158
|
-
expect(chunks).toEqual([
|
159
|
-
'id: 4\n',
|
160
|
-
'event: data\n',
|
161
|
-
`data: {"delta":{"custom_field":"custom_value"},"id":"4","index":0}\n\n`,
|
162
|
-
]);
|
163
|
-
});
|
164
|
-
|
165
|
-
describe('tool Calling', () => {
|
166
|
-
it('should handle tool calls', async () => {
|
167
|
-
const streams = [
|
168
|
-
{
|
169
|
-
id: 'chatcmpl-9eEBuv3ra8l4KKQhGj6ldhqfwV4Iy',
|
170
|
-
model: 'gpt-4o-2024-05-13',
|
171
|
-
object: 'chat.completion.chunk',
|
172
|
-
systemFingerprint: 'fp_abc28019ad',
|
173
|
-
created: '1970-01-20T21:36:14.698Z',
|
174
|
-
choices: [
|
175
|
-
{
|
176
|
-
delta: {
|
177
|
-
content: null,
|
178
|
-
role: 'assistant',
|
179
|
-
toolCalls: [
|
180
|
-
{
|
181
|
-
function: { arguments: '', name: 'realtime-weather____fetchCurrentWeather' },
|
182
|
-
id: 'call_1GT6no85IuAal06XHH2CZe8Q',
|
183
|
-
index: 0,
|
184
|
-
type: 'function',
|
185
|
-
},
|
186
|
-
],
|
187
|
-
},
|
188
|
-
index: 0,
|
189
|
-
logprobs: null,
|
190
|
-
finishReason: null,
|
191
|
-
contentFilterResults: {},
|
192
|
-
},
|
193
|
-
],
|
194
|
-
},
|
195
|
-
{
|
196
|
-
id: 'chatcmpl-9eEBuv3ra8l4KKQhGj6ldhqfwV4Iy',
|
197
|
-
model: 'gpt-4o-2024-05-13',
|
198
|
-
object: 'chat.completion.chunk',
|
199
|
-
systemFingerprint: 'fp_abc28019ad',
|
200
|
-
created: '1970-01-20T21:36:14.698Z',
|
201
|
-
choices: [
|
202
|
-
{
|
203
|
-
delta: { toolCalls: [{ function: { arguments: '{"' }, index: 0 }] },
|
204
|
-
index: 0,
|
205
|
-
logprobs: null,
|
206
|
-
finishReason: null,
|
207
|
-
contentFilterResults: {},
|
208
|
-
},
|
209
|
-
],
|
210
|
-
},
|
211
|
-
{
|
212
|
-
id: 'chatcmpl-9eEBuv3ra8l4KKQhGj6ldhqfwV4Iy',
|
213
|
-
model: 'gpt-4o-2024-05-13',
|
214
|
-
object: 'chat.completion.chunk',
|
215
|
-
systemFingerprint: 'fp_abc28019ad',
|
216
|
-
created: '1970-01-20T21:36:14.698Z',
|
217
|
-
choices: [
|
218
|
-
{
|
219
|
-
delta: { toolCalls: [{ function: { arguments: 'city' }, index: 0 }] },
|
220
|
-
index: 0,
|
221
|
-
logprobs: null,
|
222
|
-
finishReason: null,
|
223
|
-
contentFilterResults: {},
|
224
|
-
},
|
225
|
-
],
|
226
|
-
},
|
227
|
-
{
|
228
|
-
id: 'chatcmpl-9eEBuv3ra8l4KKQhGj6ldhqfwV4Iy',
|
229
|
-
model: 'gpt-4o-2024-05-13',
|
230
|
-
object: 'chat.completion.chunk',
|
231
|
-
systemFingerprint: 'fp_abc28019ad',
|
232
|
-
created: '1970-01-20T21:36:14.698Z',
|
233
|
-
choices: [
|
234
|
-
{
|
235
|
-
delta: { toolCalls: [{ function: { arguments: '":"' }, index: 0 }] },
|
236
|
-
index: 0,
|
237
|
-
logprobs: null,
|
238
|
-
finishReason: null,
|
239
|
-
contentFilterResults: {},
|
240
|
-
},
|
241
|
-
],
|
242
|
-
},
|
243
|
-
{
|
244
|
-
id: 'chatcmpl-9eEBuv3ra8l4KKQhGj6ldhqfwV4Iy',
|
245
|
-
model: 'gpt-4o-2024-05-13',
|
246
|
-
object: 'chat.completion.chunk',
|
247
|
-
systemFingerprint: 'fp_abc28019ad',
|
248
|
-
created: '1970-01-20T21:36:14.698Z',
|
249
|
-
choices: [
|
250
|
-
{
|
251
|
-
delta: { toolCalls: [{ function: { arguments: '杭州' }, index: 0 }] },
|
252
|
-
index: 0,
|
253
|
-
logprobs: null,
|
254
|
-
finishReason: null,
|
255
|
-
contentFilteesults: {},
|
256
|
-
},
|
257
|
-
],
|
258
|
-
},
|
259
|
-
{
|
260
|
-
id: 'chatcmpl-9eEBuv3ra8l4KKQhGj6ldhqfwV4Iy',
|
261
|
-
model: 'gpt-4o-2024-05-13',
|
262
|
-
object: 'chat.completion.chunk',
|
263
|
-
systemFingerprint: 'fp_abc28019ad',
|
264
|
-
created: '1970-01-20T21:36:14.698Z',
|
265
|
-
choices: [
|
266
|
-
{
|
267
|
-
delta: { toolCalls: [{ function: { arguments: '"}' }, index: 0 }] },
|
268
|
-
index: 0,
|
269
|
-
logprobs: null,
|
270
|
-
finishReason: null,
|
271
|
-
contentFilterResults: {},
|
272
|
-
},
|
273
|
-
],
|
274
|
-
},
|
275
|
-
{
|
276
|
-
id: 'chatcmpl-9eEBuv3ra8l4KKQhGj6ldhqfwV4Iy',
|
277
|
-
model: 'gpt-4o-2024-05-13',
|
278
|
-
object: 'chat.completion.chunk',
|
279
|
-
systemFingerprint: 'fp_abc28019ad',
|
280
|
-
created: '1970-01-20T21:36:14.698Z',
|
281
|
-
choices: [
|
282
|
-
{
|
283
|
-
delta: {},
|
284
|
-
index: 0,
|
285
|
-
logprobs: null,
|
286
|
-
finishReason: 'tool_calls',
|
287
|
-
contentFilterResults: {},
|
288
|
-
},
|
289
|
-
],
|
290
|
-
},
|
291
|
-
];
|
292
|
-
|
293
|
-
const mockReadableStream = new ReadableStream({
|
294
|
-
start(controller) {
|
295
|
-
streams.forEach((chunk) => {
|
296
|
-
controller.enqueue(chunk);
|
297
|
-
});
|
298
|
-
controller.close();
|
299
|
-
},
|
300
|
-
});
|
301
|
-
|
302
|
-
const onToolCallMock = vi.fn();
|
303
|
-
|
304
|
-
const protocolStream = AzureOpenAIStream(mockReadableStream, {
|
305
|
-
onToolCall: onToolCallMock,
|
306
|
-
});
|
307
|
-
|
308
|
-
const decoder = new TextDecoder();
|
309
|
-
const chunks = [];
|
310
|
-
|
311
|
-
// @ts-ignore
|
312
|
-
for await (const chunk of protocolStream) {
|
313
|
-
chunks.push(decoder.decode(chunk, { stream: true }));
|
314
|
-
}
|
315
|
-
|
316
|
-
expect(chunks).toEqual(
|
317
|
-
[
|
318
|
-
'id: chatcmpl-9eEBuv3ra8l4KKQhGj6ldhqfwV4Iy',
|
319
|
-
'event: tool_calls',
|
320
|
-
`data: [{"function":{"arguments":"","name":"realtime-weather____fetchCurrentWeather"},"id":"call_1GT6no85IuAal06XHH2CZe8Q","index":0,"type":"function"}]\n`,
|
321
|
-
'id: chatcmpl-9eEBuv3ra8l4KKQhGj6ldhqfwV4Iy',
|
322
|
-
'event: tool_calls',
|
323
|
-
`data: [{"function":{"arguments":"{\\""},"id":"call_1GT6no85IuAal06XHH2CZe8Q","index":0,"type":"function"}]\n`,
|
324
|
-
'id: chatcmpl-9eEBuv3ra8l4KKQhGj6ldhqfwV4Iy',
|
325
|
-
'event: tool_calls',
|
326
|
-
`data: [{"function":{"arguments":"city"},"id":"call_1GT6no85IuAal06XHH2CZe8Q","index":0,"type":"function"}]\n`,
|
327
|
-
'id: chatcmpl-9eEBuv3ra8l4KKQhGj6ldhqfwV4Iy',
|
328
|
-
'event: tool_calls',
|
329
|
-
`data: [{"function":{"arguments":"\\":\\""},"id":"call_1GT6no85IuAal06XHH2CZe8Q","index":0,"type":"function"}]\n`,
|
330
|
-
'id: chatcmpl-9eEBuv3ra8l4KKQhGj6ldhqfwV4Iy',
|
331
|
-
'event: tool_calls',
|
332
|
-
`data: [{"function":{"arguments":"杭州"},"id":"call_1GT6no85IuAal06XHH2CZe8Q","index":0,"type":"function"}]\n`,
|
333
|
-
'id: chatcmpl-9eEBuv3ra8l4KKQhGj6ldhqfwV4Iy',
|
334
|
-
'event: tool_calls',
|
335
|
-
`data: [{"function":{"arguments":"\\"}"},"id":"call_1GT6no85IuAal06XHH2CZe8Q","index":0,"type":"function"}]\n`,
|
336
|
-
'id: chatcmpl-9eEBuv3ra8l4KKQhGj6ldhqfwV4Iy',
|
337
|
-
'event: stop',
|
338
|
-
`data: "tool_calls"\n`,
|
339
|
-
].map((item) => `${item}\n`),
|
340
|
-
);
|
341
|
-
|
342
|
-
expect(onToolCallMock).toHaveBeenCalledTimes(6);
|
343
|
-
});
|
344
|
-
it('should handle parallel tools calling', async () => {
|
345
|
-
const streams = [
|
346
|
-
{
|
347
|
-
id: 'chatcmpl-9eEh9DtpidX5CyE4GcyIeyhU3pLir',
|
348
|
-
model: 'gpt-4o-2024-05-13',
|
349
|
-
object: 'chat.completion.chunk',
|
350
|
-
systemFingerprint: 'fp_abc28019ad',
|
351
|
-
created: '1970-01-20T21:36:16.635Z',
|
352
|
-
choices: [
|
353
|
-
{
|
354
|
-
delta: {
|
355
|
-
toolCalls: [
|
356
|
-
{
|
357
|
-
function: { arguments: '', name: 'realtime-weather____fetchCurrentWeather' },
|
358
|
-
id: 'call_cnQ80VjcWCS69wWKp4jz0nJd',
|
359
|
-
index: 0,
|
360
|
-
type: 'function',
|
361
|
-
},
|
362
|
-
],
|
363
|
-
},
|
364
|
-
index: 0,
|
365
|
-
logprobs: null,
|
366
|
-
finishReason: null,
|
367
|
-
contentFilterResults: {},
|
368
|
-
},
|
369
|
-
],
|
370
|
-
},
|
371
|
-
{
|
372
|
-
id: 'chatcmpl-9eEh9DtpidX5CyE4GcyIeyhU3pLir',
|
373
|
-
model: 'gpt-4o-2024-05-13',
|
374
|
-
object: 'chat.completion.chunk',
|
375
|
-
systemFingerprint: 'fp_abc28019ad',
|
376
|
-
created: '1970-01-20T21:36:16.635Z',
|
377
|
-
choices: [
|
378
|
-
{
|
379
|
-
delta: { toolCalls: [{ function: { arguments: '{"city": "杭州"}' }, index: 0 }] },
|
380
|
-
index: 0,
|
381
|
-
logprobs: null,
|
382
|
-
finishReason: null,
|
383
|
-
contentFilterResults: {},
|
384
|
-
},
|
385
|
-
],
|
386
|
-
},
|
387
|
-
{
|
388
|
-
id: 'chatcmpl-9eEh9DtpidX5CyE4GcyIeyhU3pLir',
|
389
|
-
model: 'gpt-4o-2024-05-13',
|
390
|
-
object: 'chat.completion.chunk',
|
391
|
-
systemFingerprint: 'fp_abc28019ad',
|
392
|
-
created: '1970-01-20T21:36:16.635Z',
|
393
|
-
choices: [
|
394
|
-
{
|
395
|
-
delta: {
|
396
|
-
toolCalls: [
|
397
|
-
{
|
398
|
-
function: { arguments: '', name: 'realtime-weather____fetchCurrentWeather' },
|
399
|
-
id: 'call_LHrpPTrT563QkP9chVddzXQk',
|
400
|
-
index: 1,
|
401
|
-
type: 'function',
|
402
|
-
},
|
403
|
-
],
|
404
|
-
},
|
405
|
-
index: 0,
|
406
|
-
logprobs: null,
|
407
|
-
finishReason: null,
|
408
|
-
contentFilterResults: {},
|
409
|
-
},
|
410
|
-
],
|
411
|
-
},
|
412
|
-
{
|
413
|
-
id: 'chatcmpl-9eEh9DtpidX5CyE4GcyIeyhU3pLir',
|
414
|
-
model: 'gpt-4o-2024-05-13',
|
415
|
-
object: 'chat.completion.chunk',
|
416
|
-
systemFingerprint: 'fp_abc28019ad',
|
417
|
-
created: '1970-01-20T21:36:16.635Z',
|
418
|
-
choices: [
|
419
|
-
{
|
420
|
-
delta: { toolCalls: [{ function: { arguments: '{"city": "北京"}' }, index: 1 }] },
|
421
|
-
index: 0,
|
422
|
-
logprobs: null,
|
423
|
-
finishReason: null,
|
424
|
-
contentFilterResults: {},
|
425
|
-
},
|
426
|
-
],
|
427
|
-
},
|
428
|
-
{
|
429
|
-
id: 'chatcmpl-9eEh9DtpidX5CyE4GcyIeyhU3pLir',
|
430
|
-
model: 'gpt-4o-2024-05-13',
|
431
|
-
object: 'chat.completion.chunk',
|
432
|
-
systemFingerprint: 'fp_abc28019ad',
|
433
|
-
created: '1970-01-20T21:36:16.635Z',
|
434
|
-
choices: [
|
435
|
-
{
|
436
|
-
delta: {},
|
437
|
-
index: 0,
|
438
|
-
logprobs: null,
|
439
|
-
finishReason: 'tool_calls',
|
440
|
-
contentFilterResults: {},
|
441
|
-
},
|
442
|
-
],
|
443
|
-
},
|
444
|
-
];
|
445
|
-
|
446
|
-
const mockReadableStream = new ReadableStream({
|
447
|
-
start(controller) {
|
448
|
-
streams.forEach((chunk) => {
|
449
|
-
controller.enqueue(chunk);
|
450
|
-
});
|
451
|
-
controller.close();
|
452
|
-
},
|
453
|
-
});
|
454
|
-
|
455
|
-
const onToolCallMock = vi.fn();
|
456
|
-
|
457
|
-
const protocolStream = AzureOpenAIStream(mockReadableStream, {
|
458
|
-
onToolCall: onToolCallMock,
|
459
|
-
});
|
460
|
-
|
461
|
-
const decoder = new TextDecoder();
|
462
|
-
const chunks = [];
|
463
|
-
|
464
|
-
// @ts-ignore
|
465
|
-
for await (const chunk of protocolStream) {
|
466
|
-
chunks.push(decoder.decode(chunk, { stream: true }));
|
467
|
-
}
|
468
|
-
|
469
|
-
expect(chunks).toEqual(
|
470
|
-
[
|
471
|
-
'id: chatcmpl-9eEh9DtpidX5CyE4GcyIeyhU3pLir',
|
472
|
-
'event: tool_calls',
|
473
|
-
`data: [{"function":{"arguments":"","name":"realtime-weather____fetchCurrentWeather"},"id":"call_cnQ80VjcWCS69wWKp4jz0nJd","index":0,"type":"function"}]\n`,
|
474
|
-
'id: chatcmpl-9eEh9DtpidX5CyE4GcyIeyhU3pLir',
|
475
|
-
'event: tool_calls',
|
476
|
-
`data: [{"function":{"arguments":"{\\"city\\": \\"杭州\\"}"},"id":"call_cnQ80VjcWCS69wWKp4jz0nJd","index":0,"type":"function"}]\n`,
|
477
|
-
'id: chatcmpl-9eEh9DtpidX5CyE4GcyIeyhU3pLir',
|
478
|
-
'event: tool_calls',
|
479
|
-
`data: [{"function":{"arguments":"","name":"realtime-weather____fetchCurrentWeather"},"id":"call_LHrpPTrT563QkP9chVddzXQk","index":1,"type":"function"}]\n`,
|
480
|
-
'id: chatcmpl-9eEh9DtpidX5CyE4GcyIeyhU3pLir',
|
481
|
-
'event: tool_calls',
|
482
|
-
`data: [{"function":{"arguments":"{\\"city\\": \\"北京\\"}"},"id":"call_LHrpPTrT563QkP9chVddzXQk","index":1,"type":"function"}]\n`,
|
483
|
-
'id: chatcmpl-9eEh9DtpidX5CyE4GcyIeyhU3pLir',
|
484
|
-
'event: stop',
|
485
|
-
`data: "tool_calls"\n`,
|
486
|
-
].map((item) => `${item}\n`),
|
487
|
-
);
|
488
|
-
|
489
|
-
expect(onToolCallMock).toHaveBeenCalledTimes(4);
|
490
|
-
});
|
491
|
-
it('should handle tool calls without index and type', async () => {
|
492
|
-
const mockOpenAIStream = new ReadableStream({
|
493
|
-
start(controller) {
|
494
|
-
controller.enqueue({
|
495
|
-
choices: [
|
496
|
-
{
|
497
|
-
delta: {
|
498
|
-
toolCalls: [
|
499
|
-
{
|
500
|
-
function: { name: 'tool1', arguments: '{}' },
|
501
|
-
id: 'call_1',
|
502
|
-
},
|
503
|
-
{
|
504
|
-
function: { name: 'tool2', arguments: '{}' },
|
505
|
-
id: 'call_2',
|
506
|
-
},
|
507
|
-
],
|
508
|
-
},
|
509
|
-
index: 0,
|
510
|
-
},
|
511
|
-
],
|
512
|
-
id: '5',
|
513
|
-
});
|
514
|
-
|
515
|
-
controller.close();
|
516
|
-
},
|
517
|
-
});
|
518
|
-
|
519
|
-
const protocolStream = AzureOpenAIStream(mockOpenAIStream);
|
520
|
-
|
521
|
-
const decoder = new TextDecoder();
|
522
|
-
const chunks = [];
|
523
|
-
|
524
|
-
// @ts-ignore
|
525
|
-
for await (const chunk of protocolStream) {
|
526
|
-
chunks.push(decoder.decode(chunk, { stream: true }));
|
527
|
-
}
|
528
|
-
|
529
|
-
expect(chunks).toEqual([
|
530
|
-
'id: 5\n',
|
531
|
-
'event: tool_calls\n',
|
532
|
-
`data: [{"function":{"name":"tool1","arguments":"{}"},"id":"call_1","index":0,"type":"function"},{"function":{"name":"tool2","arguments":"{}"},"id":"call_2","index":1,"type":"function"}]\n\n`,
|
533
|
-
]);
|
534
|
-
});
|
535
|
-
});
|
536
|
-
});
|
@@ -1,83 +0,0 @@
|
|
1
|
-
import { ChatCompletions, ChatCompletionsFunctionToolCall } from '@azure/openai';
|
2
|
-
import OpenAI from 'openai';
|
3
|
-
import type { Stream } from 'openai/streaming';
|
4
|
-
|
5
|
-
import { ChatStreamCallbacks } from '../../types';
|
6
|
-
import {
|
7
|
-
StreamProtocolChunk,
|
8
|
-
StreamProtocolToolCallChunk,
|
9
|
-
StreamStack,
|
10
|
-
StreamToolCallChunkData,
|
11
|
-
convertIterableToStream,
|
12
|
-
createCallbacksTransformer,
|
13
|
-
createSSEProtocolTransformer,
|
14
|
-
} from './protocol';
|
15
|
-
|
16
|
-
const transformOpenAIStream = (chunk: ChatCompletions, stack: StreamStack): StreamProtocolChunk => {
|
17
|
-
// maybe need another structure to add support for multiple choices
|
18
|
-
|
19
|
-
const item = chunk.choices[0];
|
20
|
-
if (!item) {
|
21
|
-
return { data: chunk, id: chunk.id, type: 'data' };
|
22
|
-
}
|
23
|
-
|
24
|
-
if (typeof item.delta?.content === 'string') {
|
25
|
-
return { data: item.delta.content, id: chunk.id, type: 'text' };
|
26
|
-
}
|
27
|
-
|
28
|
-
if (item.delta?.toolCalls) {
|
29
|
-
return {
|
30
|
-
data: item.delta.toolCalls.map((value, index): StreamToolCallChunkData => {
|
31
|
-
const func = (value as ChatCompletionsFunctionToolCall).function;
|
32
|
-
|
33
|
-
// at first time, set tool id
|
34
|
-
if (!stack.tool) {
|
35
|
-
stack.tool = { id: value.id, index, name: func.name };
|
36
|
-
} else {
|
37
|
-
// in the parallel tool calling, set the new tool id
|
38
|
-
if (value.id && stack.tool.id !== value.id) {
|
39
|
-
stack.tool = { id: value.id, index, name: func.name };
|
40
|
-
}
|
41
|
-
}
|
42
|
-
|
43
|
-
return {
|
44
|
-
function: func,
|
45
|
-
id: value.id || stack.tool?.id,
|
46
|
-
index: value.index || index,
|
47
|
-
type: value.type || 'function',
|
48
|
-
};
|
49
|
-
}),
|
50
|
-
id: chunk.id,
|
51
|
-
type: 'tool_calls',
|
52
|
-
} as StreamProtocolToolCallChunk;
|
53
|
-
}
|
54
|
-
|
55
|
-
// 给定结束原因
|
56
|
-
if (item.finishReason) {
|
57
|
-
return { data: item.finishReason, id: chunk.id, type: 'stop' };
|
58
|
-
}
|
59
|
-
|
60
|
-
if (item.delta?.content === null) {
|
61
|
-
return { data: item.delta, id: chunk.id, type: 'data' };
|
62
|
-
}
|
63
|
-
|
64
|
-
// 其余情况下,返回 delta 和 index
|
65
|
-
return {
|
66
|
-
data: { delta: item.delta, id: chunk.id, index: item.index },
|
67
|
-
id: chunk.id,
|
68
|
-
type: 'data',
|
69
|
-
};
|
70
|
-
};
|
71
|
-
|
72
|
-
export const AzureOpenAIStream = (
|
73
|
-
stream: Stream<OpenAI.ChatCompletionChunk> | ReadableStream,
|
74
|
-
callbacks?: ChatStreamCallbacks,
|
75
|
-
) => {
|
76
|
-
const stack: StreamStack = { id: '' };
|
77
|
-
const readableStream =
|
78
|
-
stream instanceof ReadableStream ? stream : convertIterableToStream(stream);
|
79
|
-
|
80
|
-
return readableStream
|
81
|
-
.pipeThrough(createSSEProtocolTransformer(transformOpenAIStream, stack))
|
82
|
-
.pipeThrough(createCallbacksTransformer(callbacks));
|
83
|
-
};
|