@lobehub/chat 1.92.3 → 1.93.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.
Files changed (90) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +8 -8
  3. package/README.zh-CN.md +8 -8
  4. package/changelog/v1.json +18 -0
  5. package/docs/development/database-schema.dbml +51 -1
  6. package/locales/ar/modelProvider.json +4 -0
  7. package/locales/ar/models.json +64 -34
  8. package/locales/ar/providers.json +3 -0
  9. package/locales/bg-BG/modelProvider.json +4 -0
  10. package/locales/bg-BG/models.json +64 -34
  11. package/locales/bg-BG/providers.json +3 -0
  12. package/locales/de-DE/modelProvider.json +4 -0
  13. package/locales/de-DE/models.json +64 -34
  14. package/locales/de-DE/providers.json +3 -0
  15. package/locales/en-US/modelProvider.json +4 -0
  16. package/locales/en-US/models.json +64 -34
  17. package/locales/en-US/providers.json +3 -0
  18. package/locales/es-ES/modelProvider.json +4 -0
  19. package/locales/es-ES/models.json +64 -34
  20. package/locales/es-ES/providers.json +3 -0
  21. package/locales/fa-IR/modelProvider.json +4 -0
  22. package/locales/fa-IR/models.json +64 -34
  23. package/locales/fa-IR/providers.json +3 -0
  24. package/locales/fr-FR/modelProvider.json +4 -0
  25. package/locales/fr-FR/models.json +64 -34
  26. package/locales/fr-FR/providers.json +3 -0
  27. package/locales/it-IT/modelProvider.json +4 -0
  28. package/locales/it-IT/models.json +64 -34
  29. package/locales/it-IT/providers.json +3 -0
  30. package/locales/ja-JP/modelProvider.json +4 -0
  31. package/locales/ja-JP/models.json +64 -34
  32. package/locales/ja-JP/providers.json +3 -0
  33. package/locales/ko-KR/modelProvider.json +4 -0
  34. package/locales/ko-KR/models.json +64 -34
  35. package/locales/ko-KR/providers.json +3 -0
  36. package/locales/nl-NL/modelProvider.json +4 -0
  37. package/locales/nl-NL/models.json +64 -34
  38. package/locales/nl-NL/providers.json +3 -0
  39. package/locales/pl-PL/modelProvider.json +4 -0
  40. package/locales/pl-PL/models.json +64 -34
  41. package/locales/pl-PL/providers.json +3 -0
  42. package/locales/pt-BR/modelProvider.json +4 -0
  43. package/locales/pt-BR/models.json +64 -34
  44. package/locales/pt-BR/providers.json +3 -0
  45. package/locales/ru-RU/modelProvider.json +4 -0
  46. package/locales/ru-RU/models.json +63 -33
  47. package/locales/ru-RU/providers.json +3 -0
  48. package/locales/tr-TR/modelProvider.json +4 -0
  49. package/locales/tr-TR/models.json +64 -34
  50. package/locales/tr-TR/providers.json +3 -0
  51. package/locales/vi-VN/modelProvider.json +4 -0
  52. package/locales/vi-VN/models.json +64 -34
  53. package/locales/vi-VN/providers.json +3 -0
  54. package/locales/zh-CN/modelProvider.json +4 -0
  55. package/locales/zh-CN/models.json +59 -29
  56. package/locales/zh-CN/providers.json +3 -0
  57. package/locales/zh-TW/modelProvider.json +4 -0
  58. package/locales/zh-TW/models.json +64 -34
  59. package/locales/zh-TW/providers.json +3 -0
  60. package/package.json +1 -1
  61. package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/index.tsx +16 -0
  62. package/src/config/modelProviders/openai.ts +3 -1
  63. package/src/database/client/migrations.json +25 -0
  64. package/src/database/migrations/0025_add_provider_config.sql +1 -0
  65. package/src/database/migrations/meta/0025_snapshot.json +5703 -0
  66. package/src/database/migrations/meta/_journal.json +7 -0
  67. package/src/database/models/__tests__/aiProvider.test.ts +2 -0
  68. package/src/database/models/aiProvider.ts +5 -2
  69. package/src/database/repositories/tableViewer/index.test.ts +1 -1
  70. package/src/database/schemas/_helpers.ts +5 -1
  71. package/src/database/schemas/aiInfra.ts +5 -1
  72. package/src/libs/model-runtime/openai/index.ts +21 -2
  73. package/src/libs/model-runtime/types/chat.ts +6 -9
  74. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +79 -5
  75. package/src/libs/model-runtime/utils/openaiHelpers.test.ts +145 -1
  76. package/src/libs/model-runtime/utils/openaiHelpers.ts +59 -0
  77. package/src/libs/model-runtime/utils/streams/openai/__snapshots__/responsesStream.test.ts.snap +193 -0
  78. package/src/libs/model-runtime/utils/streams/openai/index.ts +2 -0
  79. package/src/libs/model-runtime/utils/streams/{openai.test.ts → openai/openai.test.ts} +1 -1
  80. package/src/libs/model-runtime/utils/streams/{openai.ts → openai/openai.ts} +5 -5
  81. package/src/libs/model-runtime/utils/streams/openai/responsesStream.test.ts +826 -0
  82. package/src/libs/model-runtime/utils/streams/openai/responsesStream.ts +166 -0
  83. package/src/libs/model-runtime/utils/streams/protocol.ts +4 -1
  84. package/src/libs/model-runtime/utils/streams/utils.ts +20 -0
  85. package/src/libs/model-runtime/utils/usageConverter.ts +59 -0
  86. package/src/locales/default/modelProvider.ts +4 -0
  87. package/src/services/__tests__/chat.test.ts +25 -0
  88. package/src/services/chat.ts +8 -2
  89. package/src/store/aiInfra/slices/aiProvider/selectors.ts +11 -0
  90. package/src/types/aiProvider.ts +13 -1
@@ -0,0 +1,826 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import { AgentRuntimeErrorType } from '@/libs/model-runtime';
4
+
5
+ import { FIRST_CHUNK_ERROR_KEY } from '../protocol';
6
+ import { createReadableStream, readStreamChunk } from '../utils';
7
+ import { OpenAIResponsesStream } from './responsesStream';
8
+
9
+ describe('OpenAIResponsesStream', () => {
10
+ it('should transform OpenAI stream to protocol stream', async () => {
11
+ const mockOpenAIStream = createReadableStream([
12
+ {
13
+ type: 'response.created',
14
+ response: {
15
+ id: 'resp_683e7b8ca3308190b6837f20d2c015cd0cf93af363cdcf58',
16
+ object: 'response',
17
+ created_at: 1748925324,
18
+ status: 'in_progress',
19
+ error: null,
20
+ incomplete_details: null,
21
+ instructions: null,
22
+ max_output_tokens: null,
23
+ model: 'o4-mini',
24
+ output: [],
25
+ parallel_tool_calls: true,
26
+ previous_response_id: null,
27
+ reasoning: { effort: 'medium', summary: null },
28
+ service_tier: 'auto',
29
+ store: false,
30
+ temperature: 1,
31
+ text: { format: { type: 'text' } },
32
+ tool_choice: 'auto',
33
+ tools: [
34
+ {
35
+ type: 'function',
36
+ description:
37
+ 'a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results',
38
+ name: 'lobe-web-browsing____search____builtin',
39
+ parameters: {
40
+ properties: {
41
+ query: { description: 'The search query', type: 'string' },
42
+ searchCategories: {
43
+ description: 'The search categories you can set:',
44
+ items: {
45
+ enum: ['general', 'images', 'news', 'science', 'videos'],
46
+ type: 'string',
47
+ },
48
+ type: 'array',
49
+ },
50
+ searchEngines: {
51
+ description: 'The search engines you can use:',
52
+ items: {
53
+ enum: [
54
+ 'google',
55
+ 'bilibili',
56
+ 'bing',
57
+ 'duckduckgo',
58
+ 'npm',
59
+ 'pypi',
60
+ 'github',
61
+ 'arxiv',
62
+ 'google scholar',
63
+ 'z-library',
64
+ 'reddit',
65
+ 'imdb',
66
+ 'brave',
67
+ 'wikipedia',
68
+ 'pinterest',
69
+ 'unsplash',
70
+ 'vimeo',
71
+ 'youtube',
72
+ ],
73
+ type: 'string',
74
+ },
75
+ type: 'array',
76
+ },
77
+ searchTimeRange: {
78
+ description: 'The time range you can set:',
79
+ enum: ['anytime', 'day', 'week', 'month', 'year'],
80
+ type: 'string',
81
+ },
82
+ },
83
+ required: ['query'],
84
+ type: 'object',
85
+ },
86
+ strict: true,
87
+ },
88
+ {
89
+ type: 'function',
90
+ description:
91
+ 'A crawler can visit page content. Output is a JSON object of title, content, url and website',
92
+ name: 'lobe-web-browsing____crawlSinglePage____builtin',
93
+ parameters: {
94
+ properties: { url: { description: 'The url need to be crawled', type: 'string' } },
95
+ required: ['url'],
96
+ type: 'object',
97
+ },
98
+ strict: true,
99
+ },
100
+ {
101
+ type: 'function',
102
+ description:
103
+ 'A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website',
104
+ name: 'lobe-web-browsing____crawlMultiPages____builtin',
105
+ parameters: {
106
+ properties: {
107
+ urls: {
108
+ items: { description: 'The urls need to be crawled', type: 'string' },
109
+ type: 'array',
110
+ },
111
+ },
112
+ required: ['urls'],
113
+ type: 'object',
114
+ },
115
+ strict: true,
116
+ },
117
+ ],
118
+ top_p: 1,
119
+ truncation: 'disabled',
120
+ usage: null,
121
+ user: null,
122
+ metadata: {},
123
+ },
124
+ },
125
+ {
126
+ type: 'response.in_progress',
127
+ response: {
128
+ id: 'resp_683e7b8ca3308190b6837f20d2c015cd0cf93af363cdcf58',
129
+ object: 'response',
130
+ created_at: 1748925324,
131
+ status: 'in_progress',
132
+ error: null,
133
+ incomplete_details: null,
134
+ instructions: null,
135
+ max_output_tokens: null,
136
+ model: 'o4-mini',
137
+ output: [],
138
+ parallel_tool_calls: true,
139
+ previous_response_id: null,
140
+ reasoning: { effort: 'medium', summary: null },
141
+ service_tier: 'auto',
142
+ store: false,
143
+ temperature: 1,
144
+ text: { format: { type: 'text' } },
145
+ tool_choice: 'auto',
146
+ tools: [
147
+ {
148
+ type: 'function',
149
+ description:
150
+ 'a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results',
151
+ name: 'lobe-web-browsing____search____builtin',
152
+ parameters: {
153
+ properties: {
154
+ query: { description: 'The search query', type: 'string' },
155
+ searchCategories: {
156
+ description: 'The search categories you can set:',
157
+ items: {
158
+ enum: ['general', 'images', 'news', 'science', 'videos'],
159
+ type: 'string',
160
+ },
161
+ type: 'array',
162
+ },
163
+ searchEngines: {
164
+ description: 'The search engines you can use:',
165
+ items: {
166
+ enum: [
167
+ 'google',
168
+ 'bilibili',
169
+ 'bing',
170
+ 'duckduckgo',
171
+ 'npm',
172
+ 'pypi',
173
+ 'github',
174
+ 'arxiv',
175
+ 'google scholar',
176
+ 'z-library',
177
+ 'reddit',
178
+ 'imdb',
179
+ 'brave',
180
+ 'wikipedia',
181
+ 'pinterest',
182
+ 'unsplash',
183
+ 'vimeo',
184
+ 'youtube',
185
+ ],
186
+ type: 'string',
187
+ },
188
+ type: 'array',
189
+ },
190
+ searchTimeRange: {
191
+ description: 'The time range you can set:',
192
+ enum: ['anytime', 'day', 'week', 'month', 'year'],
193
+ type: 'string',
194
+ },
195
+ },
196
+ required: ['query'],
197
+ type: 'object',
198
+ },
199
+ strict: true,
200
+ },
201
+ {
202
+ type: 'function',
203
+ description:
204
+ 'A crawler can visit page content. Output is a JSON object of title, content, url and website',
205
+ name: 'lobe-web-browsing____crawlSinglePage____builtin',
206
+ parameters: {
207
+ properties: { url: { description: 'The url need to be crawled', type: 'string' } },
208
+ required: ['url'],
209
+ type: 'object',
210
+ },
211
+ strict: true,
212
+ },
213
+ {
214
+ type: 'function',
215
+ description:
216
+ 'A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website',
217
+ name: 'lobe-web-browsing____crawlMultiPages____builtin',
218
+ parameters: {
219
+ properties: {
220
+ urls: {
221
+ items: { description: 'The urls need to be crawled', type: 'string' },
222
+ type: 'array',
223
+ },
224
+ },
225
+ required: ['urls'],
226
+ type: 'object',
227
+ },
228
+ strict: true,
229
+ },
230
+ ],
231
+ top_p: 1,
232
+ truncation: 'disabled',
233
+ usage: null,
234
+ user: null,
235
+ metadata: {},
236
+ },
237
+ },
238
+ {
239
+ type: 'response.output_item.added',
240
+ output_index: 0,
241
+ item: {
242
+ id: 'rs_683e7bc80a9c81908f6e3d61ad63cc1e0cf93af363cdcf58',
243
+ type: 'reasoning',
244
+ summary: [],
245
+ },
246
+ },
247
+ {
248
+ type: 'response.output_item.added',
249
+ output_index: 1,
250
+ item: {
251
+ id: 'msg_683e7bde8b0c8190970ab8c719c0fc1c0cf93af363cdcf58',
252
+ type: 'message',
253
+ status: 'in_progress',
254
+ content: [],
255
+ role: 'assistant',
256
+ },
257
+ },
258
+ {
259
+ type: 'response.content_part.added',
260
+ item_id: 'msg_683e7bde8b0c8190970ab8c719c0fc1c0cf93af363cdcf58',
261
+ output_index: 1,
262
+ content_index: 0,
263
+ part: { type: 'output_text', annotations: [], text: 'Hello' },
264
+ },
265
+ {
266
+ type: 'response.content_part.added',
267
+ item_id: 'msg_683e7bde8b0c8190970ab8c719c0fc1c0cf93af363cdcf58',
268
+ output_index: 1,
269
+ content_index: 0,
270
+ part: { type: 'output_text', annotations: [], text: ' world' },
271
+ },
272
+ ]);
273
+
274
+ const onStartMock = vi.fn();
275
+ const onTextMock = vi.fn();
276
+ const onCompletionMock = vi.fn();
277
+
278
+ const protocolStream = OpenAIResponsesStream(mockOpenAIStream, {
279
+ callbacks: {
280
+ onStart: onStartMock,
281
+ onText: onTextMock,
282
+ onCompletion: onCompletionMock,
283
+ },
284
+ });
285
+
286
+ const chunks = await readStreamChunk(protocolStream);
287
+
288
+ expect(chunks).toMatchSnapshot();
289
+
290
+ expect(onStartMock).toHaveBeenCalledTimes(1);
291
+ expect(onCompletionMock).toHaveBeenCalledTimes(1);
292
+ });
293
+ describe('Reasoning', () => {
294
+ it('summary', async () => {
295
+ const mockOpenAIStream = createReadableStream([
296
+ {
297
+ type: 'response.created',
298
+ response: {
299
+ id: 'resp_684313b89200819087f27686e0c822260b502bf083132d0d',
300
+ object: 'response',
301
+ created_at: 1749226424,
302
+ status: 'in_progress',
303
+ error: null,
304
+ incomplete_details: null,
305
+ instructions: null,
306
+ max_output_tokens: null,
307
+ model: 'o4-mini',
308
+ output: [],
309
+ parallel_tool_calls: true,
310
+ previous_response_id: null,
311
+ reasoning: { effort: 'medium', summary: 'detailed' },
312
+ service_tier: 'auto',
313
+ store: false,
314
+ temperature: 1,
315
+ text: { format: { type: 'text' } },
316
+ tool_choice: 'auto',
317
+ tools: [
318
+ {
319
+ type: 'function',
320
+ description:
321
+ 'a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results',
322
+ name: 'lobe-web-browsing____search____builtin',
323
+ parameters: {
324
+ properties: {
325
+ query: { description: 'The search query', type: 'string' },
326
+ searchCategories: {
327
+ description: 'The search categories you can set:',
328
+ items: {
329
+ enum: ['general', 'images', 'news', 'science', 'videos'],
330
+ type: 'string',
331
+ },
332
+ type: 'array',
333
+ },
334
+ searchEngines: {
335
+ description: 'The search engines you can use:',
336
+ items: {
337
+ enum: [
338
+ 'google',
339
+ 'bilibili',
340
+ 'bing',
341
+ 'duckduckgo',
342
+ 'npm',
343
+ 'pypi',
344
+ 'github',
345
+ 'arxiv',
346
+ 'google scholar',
347
+ 'z-library',
348
+ 'reddit',
349
+ 'imdb',
350
+ 'brave',
351
+ 'wikipedia',
352
+ 'pinterest',
353
+ 'unsplash',
354
+ 'vimeo',
355
+ 'youtube',
356
+ ],
357
+ type: 'string',
358
+ },
359
+ type: 'array',
360
+ },
361
+ searchTimeRange: {
362
+ description: 'The time range you can set:',
363
+ enum: ['anytime', 'day', 'week', 'month', 'year'],
364
+ type: 'string',
365
+ },
366
+ },
367
+ required: ['query'],
368
+ type: 'object',
369
+ },
370
+ strict: true,
371
+ },
372
+ {
373
+ type: 'function',
374
+ description:
375
+ 'A crawler can visit page content. Output is a JSON object of title, content, url and website',
376
+ name: 'lobe-web-browsing____crawlSinglePage____builtin',
377
+ parameters: {
378
+ properties: {
379
+ url: { description: 'The url need to be crawled', type: 'string' },
380
+ },
381
+ required: ['url'],
382
+ type: 'object',
383
+ },
384
+ strict: true,
385
+ },
386
+ {
387
+ type: 'function',
388
+ description:
389
+ 'A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website',
390
+ name: 'lobe-web-browsing____crawlMultiPages____builtin',
391
+ parameters: {
392
+ properties: {
393
+ urls: {
394
+ items: { description: 'The urls need to be crawled', type: 'string' },
395
+ type: 'array',
396
+ },
397
+ },
398
+ required: ['urls'],
399
+ type: 'object',
400
+ },
401
+ strict: true,
402
+ },
403
+ ],
404
+ top_p: 1,
405
+ truncation: 'disabled',
406
+ usage: null,
407
+ user: null,
408
+ metadata: {},
409
+ },
410
+ },
411
+ {
412
+ type: 'response.in_progress',
413
+ response: {
414
+ id: 'resp_684313b89200819087f27686e0c822260b502bf083132d0d',
415
+ object: 'response',
416
+ created_at: 1749226424,
417
+ status: 'in_progress',
418
+ error: null,
419
+ incomplete_details: null,
420
+ instructions: null,
421
+ max_output_tokens: null,
422
+ model: 'o4-mini',
423
+ output: [],
424
+ parallel_tool_calls: true,
425
+ previous_response_id: null,
426
+ reasoning: { effort: 'medium', summary: 'detailed' },
427
+ service_tier: 'auto',
428
+ store: false,
429
+ temperature: 1,
430
+ text: { format: { type: 'text' } },
431
+ tool_choice: 'auto',
432
+ tools: [
433
+ {
434
+ type: 'function',
435
+ description:
436
+ 'a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results',
437
+ name: 'lobe-web-browsing____search____builtin',
438
+ parameters: {
439
+ properties: {
440
+ query: { description: 'The search query', type: 'string' },
441
+ searchCategories: {
442
+ description: 'The search categories you can set:',
443
+ items: {
444
+ enum: ['general', 'images', 'news', 'science', 'videos'],
445
+ type: 'string',
446
+ },
447
+ type: 'array',
448
+ },
449
+ searchEngines: {
450
+ description: 'The search engines you can use:',
451
+ items: {
452
+ enum: [
453
+ 'google',
454
+ 'bilibili',
455
+ 'bing',
456
+ 'duckduckgo',
457
+ 'npm',
458
+ 'pypi',
459
+ 'github',
460
+ 'arxiv',
461
+ 'google scholar',
462
+ 'z-library',
463
+ 'reddit',
464
+ 'imdb',
465
+ 'brave',
466
+ 'wikipedia',
467
+ 'pinterest',
468
+ 'unsplash',
469
+ 'vimeo',
470
+ 'youtube',
471
+ ],
472
+ type: 'string',
473
+ },
474
+ type: 'array',
475
+ },
476
+ searchTimeRange: {
477
+ description: 'The time range you can set:',
478
+ enum: ['anytime', 'day', 'week', 'month', 'year'],
479
+ type: 'string',
480
+ },
481
+ },
482
+ required: ['query'],
483
+ type: 'object',
484
+ },
485
+ strict: true,
486
+ },
487
+ {
488
+ type: 'function',
489
+ description:
490
+ 'A crawler can visit page content. Output is a JSON object of title, content, url and website',
491
+ name: 'lobe-web-browsing____crawlSinglePage____builtin',
492
+ parameters: {
493
+ properties: {
494
+ url: { description: 'The url need to be crawled', type: 'string' },
495
+ },
496
+ required: ['url'],
497
+ type: 'object',
498
+ },
499
+ strict: true,
500
+ },
501
+ {
502
+ type: 'function',
503
+ description:
504
+ 'A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website',
505
+ name: 'lobe-web-browsing____crawlMultiPages____builtin',
506
+ parameters: {
507
+ properties: {
508
+ urls: {
509
+ items: { description: 'The urls need to be crawled', type: 'string' },
510
+ type: 'array',
511
+ },
512
+ },
513
+ required: ['urls'],
514
+ type: 'object',
515
+ },
516
+ strict: true,
517
+ },
518
+ ],
519
+ top_p: 1,
520
+ truncation: 'disabled',
521
+ usage: null,
522
+ user: null,
523
+ metadata: {},
524
+ },
525
+ },
526
+ {
527
+ type: 'response.output_item.added',
528
+ output_index: 0,
529
+ item: {
530
+ id: 'rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d',
531
+ type: 'reasoning',
532
+ summary: [],
533
+ },
534
+ },
535
+ {
536
+ type: 'response.reasoning_summary_part.added',
537
+ item_id: 'rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d',
538
+ output_index: 0,
539
+ summary_index: 0,
540
+ part: { type: 'summary_text', text: '' },
541
+ },
542
+ {
543
+ type: 'response.reasoning_summary_text.delta',
544
+ item_id: 'rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d',
545
+ output_index: 0,
546
+ summary_index: 0,
547
+ delta: '**Answering a',
548
+ },
549
+ {
550
+ type: 'response.reasoning_summary_text.delta',
551
+ item_id: 'rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d',
552
+ output_index: 0,
553
+ summary_index: 0,
554
+ delta: ' numeric or 9.92',
555
+ },
556
+ {
557
+ type: 'response.reasoning_summary_text.delta',
558
+ item_id: 'rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d',
559
+ output_index: 0,
560
+ summary_index: 0,
561
+ delta: '.',
562
+ },
563
+ {
564
+ type: 'response.reasoning_summary_text.done',
565
+ item_id: 'rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d',
566
+ output_index: 0,
567
+ summary_index: 0,
568
+ text: '**Answering a numeric comparison**\n\nThe user is asking in Chinese which number is larger: 9.1 or 9.92. This is straightforward since 9.92 is clearly larger, as it\'s greater than 9.1. We can respond with "9.92大于9.1" without needing to search for more information. It\'s a simple comparison, but Iould also add a little explanation, noting that 9.92 is indeed 0.82 more than 9.1. However, keeping it simple with "9.92 > 9.1" is perfectly fine!',
569
+ },
570
+ {
571
+ type: 'response.reasoning_summary_part.done',
572
+ item_id: 'rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d',
573
+ output_index: 0,
574
+ summary_index: 0,
575
+ part: {
576
+ type: 'summary_text',
577
+ text: '**Answering a numeric comparison**\n\nThe user is asking in Chinese which number is larger: 9.1 or 9.92. This is straightforward since 9.92 is clearly larger, as it\'s greater than 9.1. We can respond with "9.92大于9.1" without needing to search for more information. Is a simple comparison, but I could also add a little explanation, noting that 9.92 is indeed 0.82 more than 9.1. However, keeping it simple with "9.92 > 9.1" is perfectly fine!',
578
+ },
579
+ },
580
+ {
581
+ type: 'response.reasoning_summary_part.added',
582
+ item_id: 'rs_6843fe13e73c8190a49d9372ef8cd46f08c019075e7c8955',
583
+ output_index: 0,
584
+ summary_index: 1,
585
+ part: { type: 'summary_text', text: '' },
586
+ },
587
+ {
588
+ type: 'response.reasoning_summary_text.delta',
589
+ item_id: 'rs_6843fe13e73c8190a49d9372ef8cd46f08c019075e7c8955',
590
+ output_index: 0,
591
+ summary_index: 1,
592
+ delta: '**Exploring a mathematical sequence**',
593
+ },
594
+ {
595
+ type: 'response.reasoning_summary_text.delta',
596
+ item_id: 'rs_6843fe13e73c8190a49d9372ef8cd46f08c019075e7c8955',
597
+ output_index: 0,
598
+ summary_index: 1,
599
+ delta: ' analyzing',
600
+ },
601
+ {
602
+ type: 'response.output_item.done',
603
+ output_index: 0,
604
+ item: {
605
+ id: 'rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d',
606
+ type: 'reasoning',
607
+ summary: [
608
+ {
609
+ type: 'summary_text',
610
+ text: '**Answering a numeric comparison**\n\nThe user is asking in Chinese which number is larger: 9.1 or 9.92. This is straightforward since 9.92 is clearly larger, as it\'s greater than 9.1. We can respond with "9.92大于9.1" without needing to search for more information. It\'s simple comparison, but I could also add a little explanation, noting that 9.92 is indeed 0.82 more than 9.1. However, keeping it simple with "9.92 > 9.1" is perfectly fine!',
611
+ },
612
+ ],
613
+ },
614
+ },
615
+ {
616
+ type: 'response.output_item.added',
617
+ output_index: 1,
618
+ item: {
619
+ id: 'msg_684313bee2c88190b0f4b09621ad7dc60b502bf083132d0d',
620
+ type: 'message',
621
+ status: 'in_progress',
622
+ content: [],
623
+ role: 'assistant',
624
+ },
625
+ },
626
+ {
627
+ type: 'response.content_part.added',
628
+ item_id: 'msg_684313bee2c88190b0f4b09621ad7dc60b502bf083132d0d',
629
+ output_index: 1,
630
+ content_index: 0,
631
+ part: { type: 'output_text', annotations: [], text: '' },
632
+ },
633
+ {
634
+ type: 'response.output_text.delta',
635
+ item_id: 'msg_684313bee2c88190b0f4b09621ad7dc60b502bf083132d0d',
636
+ output_index: 1,
637
+ content_index: 0,
638
+ delta: '9.92 比 9.1 大。',
639
+ },
640
+ {
641
+ type: 'response.output_text.done',
642
+ item_id: 'msg_684313bee2c88190b0f4b09621ad7dc60b502bf083132d0d',
643
+ output_index: 1,
644
+ content_index: 0,
645
+ text: '9.92 比 9.1 大。',
646
+ },
647
+ {
648
+ type: 'response.content_part.done',
649
+ item_id: 'msg_684313bee2c88190b0f4b09621ad7dc60b502bf083132d0d',
650
+ output_index: 1,
651
+ content_index: 0,
652
+ part: { type: 'output_text', annotations: [], text: '9.92 比 9.1 大。' },
653
+ },
654
+ {
655
+ type: 'response.output_item.done',
656
+ output_index: 1,
657
+ item: {
658
+ id: 'msg_684313bee2c88190b0f4b09621ad7dc60b502bf083132d0d',
659
+ type: 'message',
660
+ status: 'completed',
661
+ content: [{ type: 'output_text', annotations: [], text: '9.92 比 9. 大。' }],
662
+ role: 'assistant',
663
+ },
664
+ },
665
+ {
666
+ type: 'response.completed',
667
+ response: {
668
+ id: 'resp_684313b89200819087f27686e0c822260b502bf083132d0d',
669
+ object: 'response',
670
+ created_at: 1749226424,
671
+ status: 'completed',
672
+ error: null,
673
+ incomplete_details: null,
674
+ instructions: null,
675
+ max_output_tokens: null,
676
+ model: 'o4-mini',
677
+ output: [
678
+ {
679
+ id: 'rs_684313b9774481908ee856625f82fb8c0b502bf083132d0d',
680
+ type: 'reasoning',
681
+ summary: [
682
+ {
683
+ type: 'summary_text',
684
+ text: '**Answering a numeric comparison**\n\nThe user is asking in Chinese which number is larger: 9.1 or 9.92. This is straightforward since 9.92 is clearly larger, as it\'s greater than 9.1. We can respond with "9.92大于9.1" without needing to search for more information. It\'s a simplcomparison, but I could also add a little explanation, noting that 9.92 is indeed 0.82 more than 9.1. However, keeping it simple with "9.92 > 9.1" is perfectly fine!',
685
+ },
686
+ ],
687
+ },
688
+ {
689
+ id: 'msg_684313bee2c88190b0f4b09621ad7dc60b502bf083132d0d',
690
+ type: 'message',
691
+ status: 'completed',
692
+ content: [{ type: 'output_text', annotations: [], text: '9.92 比 9.1 大。' }],
693
+ role: 'assistant',
694
+ },
695
+ ],
696
+ parallel_tool_calls: true,
697
+ previous_response_id: null,
698
+ reasoning: { effort: 'medium', summary: 'detailed' },
699
+ service_tier: 'default',
700
+ store: false,
701
+ temperature: 1,
702
+ text: { format: { type: 'text' } },
703
+ tool_choice: 'auto',
704
+ tools: [
705
+ {
706
+ type: 'function',
707
+ description:
708
+ 'a search service. Useful for when you need to answer questions about current events. Input should be a search query. Output is a JSON array of the query results',
709
+ name: 'lobe-web-browsing____search____builtin',
710
+ parameters: {
711
+ properties: {
712
+ query: { description: 'The search query', type: 'string' },
713
+ searchCategories: {
714
+ description: 'The search categories you can set:',
715
+ items: {
716
+ enum: ['general', 'images', 'news', 'science', 'videos'],
717
+ type: 'string',
718
+ },
719
+ type: 'array',
720
+ },
721
+ searchEngines: {
722
+ description: 'The search engines you can use:',
723
+ items: {
724
+ enum: [
725
+ 'google',
726
+ 'bilibili',
727
+ 'bing',
728
+ 'duckduckgo',
729
+ 'npm',
730
+ 'pypi',
731
+ 'github',
732
+ 'arxiv',
733
+ 'google scholar',
734
+ 'z-library',
735
+ 'reddit',
736
+ 'imdb',
737
+ 'brave',
738
+ 'wikipedia',
739
+ 'pinterest',
740
+ 'unsplash',
741
+ 'vimeo',
742
+ 'youtube',
743
+ ],
744
+ type: 'string',
745
+ },
746
+ type: 'array',
747
+ },
748
+ searchTimeRange: {
749
+ description: 'The time range you can set:',
750
+ enum: ['anytime', 'day', 'week', 'month', 'year'],
751
+ type: 'string',
752
+ },
753
+ },
754
+ required: ['query'],
755
+ type: 'object',
756
+ },
757
+ strict: true,
758
+ },
759
+ {
760
+ type: 'function',
761
+ description:
762
+ 'A crawler can visit page content. Output is a JSON object of title, content, url and website',
763
+ name: 'lobe-web-browsing____crawlSinglePage____builtin',
764
+ parameters: {
765
+ properties: {
766
+ url: { description: 'The url need to be crawled', type: 'string' },
767
+ },
768
+ required: ['url'],
769
+ type: 'object',
770
+ },
771
+ strict: true,
772
+ },
773
+ {
774
+ type: 'function',
775
+ description:
776
+ 'A crawler can visit multi pages. If need to visit multi website, use this one. Output is an array of JSON object of title, content, url and website',
777
+ name: 'lobe-web-browsing____crawlMultiPages____builtin',
778
+ parameters: {
779
+ properties: {
780
+ urls: {
781
+ items: { description: 'The urls need to be crawled', type: 'string' },
782
+ type: 'array',
783
+ },
784
+ },
785
+ required: ['urls'],
786
+ type: 'object',
787
+ },
788
+ strict: true,
789
+ },
790
+ ],
791
+ top_p: 1,
792
+ truncation: 'disabled',
793
+ usage: {
794
+ input_tokens: 2391,
795
+ input_tokens_details: { cached_tokens: 2298 },
796
+ output_tokens: 144,
797
+ output_tokens_details: { reasoning_tokens: 128 },
798
+ total_tokens: 2535,
799
+ },
800
+ user: null,
801
+ metadata: {},
802
+ },
803
+ },
804
+ ]);
805
+
806
+ const onStartMock = vi.fn();
807
+ const onTextMock = vi.fn();
808
+ const onCompletionMock = vi.fn();
809
+
810
+ const protocolStream = OpenAIResponsesStream(mockOpenAIStream, {
811
+ callbacks: {
812
+ onStart: onStartMock,
813
+ onText: onTextMock,
814
+ onCompletion: onCompletionMock,
815
+ },
816
+ });
817
+
818
+ const chunks = await readStreamChunk(protocolStream);
819
+
820
+ expect(chunks).toMatchSnapshot();
821
+
822
+ expect(onStartMock).toHaveBeenCalledTimes(1);
823
+ expect(onCompletionMock).toHaveBeenCalledTimes(1);
824
+ });
825
+ });
826
+ });