@lobehub/lobehub 2.0.0-next.129 → 2.0.0-next.130

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 CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.130](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.129...v2.0.0-next.130)
6
+
7
+ <sup>Released on **2025-11-28**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Add handling for `content_part` and `reasoning_part` events in fetchSSE.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Add handling for `content_part` and `reasoning_part` events in fetchSSE, closes [#10470](https://github.com/lobehub/lobe-chat/issues/10470) ([8aff3ab](https://github.com/lobehub/lobe-chat/commit/8aff3ab))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
5
30
  ## [Version 2.0.0-next.129](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.128...v2.0.0-next.129)
6
31
 
7
32
  <sup>Released on **2025-11-28**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Add handling for content_part and reasoning_part events in fetchSSE."
6
+ ]
7
+ },
8
+ "date": "2025-11-28",
9
+ "version": "2.0.0-next.130"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "fixes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.129",
3
+ "version": "2.0.0-next.130",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -235,6 +235,135 @@ describe('fetchSSE', () => {
235
235
  });
236
236
  });
237
237
 
238
+ describe('content_part and reasoning_part', () => {
239
+ it('should handle content_part event with text and accumulate output', async () => {
240
+ const mockOnMessageHandle = vi.fn();
241
+ const mockOnFinish = vi.fn();
242
+
243
+ (fetchEventSource as any).mockImplementationOnce(
244
+ async (url: string, options: FetchEventSourceInit) => {
245
+ options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
246
+ options.onmessage!({
247
+ event: 'content_part',
248
+ data: JSON.stringify({ content: 'Hello', partType: 'text' }),
249
+ } as any);
250
+ options.onmessage!({
251
+ event: 'content_part',
252
+ data: JSON.stringify({ content: ' World', partType: 'text' }),
253
+ } as any);
254
+ },
255
+ );
256
+
257
+ await fetchSSE('/', {
258
+ onMessageHandle: mockOnMessageHandle,
259
+ onFinish: mockOnFinish,
260
+ responseAnimation: 'none',
261
+ });
262
+
263
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, {
264
+ content: 'Hello',
265
+ mimeType: undefined,
266
+ partType: 'text',
267
+ thoughtSignature: undefined,
268
+ type: 'content_part',
269
+ });
270
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, {
271
+ content: ' World',
272
+ mimeType: undefined,
273
+ partType: 'text',
274
+ thoughtSignature: undefined,
275
+ type: 'content_part',
276
+ });
277
+
278
+ // Verify output is accumulated correctly
279
+ expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
280
+ observationId: null,
281
+ toolCalls: undefined,
282
+ traceId: null,
283
+ type: 'done',
284
+ });
285
+ });
286
+
287
+ it('should handle reasoning_part event with text and accumulate thinking', async () => {
288
+ const mockOnMessageHandle = vi.fn();
289
+ const mockOnFinish = vi.fn();
290
+
291
+ (fetchEventSource as any).mockImplementationOnce(
292
+ async (url: string, options: FetchEventSourceInit) => {
293
+ options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
294
+ options.onmessage!({
295
+ event: 'reasoning_part',
296
+ data: JSON.stringify({ content: 'Thinking:', partType: 'text' }),
297
+ } as any);
298
+ options.onmessage!({
299
+ event: 'reasoning_part',
300
+ data: JSON.stringify({ content: ' step 1', partType: 'text' }),
301
+ } as any);
302
+ options.onmessage!({
303
+ event: 'content_part',
304
+ data: JSON.stringify({ content: 'Final answer', partType: 'text' }),
305
+ } as any);
306
+ },
307
+ );
308
+
309
+ await fetchSSE('/', {
310
+ onMessageHandle: mockOnMessageHandle,
311
+ onFinish: mockOnFinish,
312
+ responseAnimation: 'none',
313
+ });
314
+
315
+ // Verify reasoning is accumulated correctly
316
+ expect(mockOnFinish).toHaveBeenCalledWith('Final answer', {
317
+ observationId: null,
318
+ reasoning: { content: 'Thinking: step 1' },
319
+ toolCalls: undefined,
320
+ traceId: null,
321
+ type: 'done',
322
+ });
323
+ });
324
+
325
+ it('should not accumulate output for non-text content_part (e.g., image)', async () => {
326
+ const mockOnMessageHandle = vi.fn();
327
+ const mockOnFinish = vi.fn();
328
+
329
+ (fetchEventSource as any).mockImplementationOnce(
330
+ async (url: string, options: FetchEventSourceInit) => {
331
+ options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
332
+ options.onmessage!({
333
+ event: 'content_part',
334
+ data: JSON.stringify({
335
+ content: 'base64imagedata',
336
+ partType: 'image',
337
+ mimeType: 'image/png',
338
+ }),
339
+ } as any);
340
+ },
341
+ );
342
+
343
+ await fetchSSE('/', {
344
+ onMessageHandle: mockOnMessageHandle,
345
+ onFinish: mockOnFinish,
346
+ responseAnimation: 'none',
347
+ });
348
+
349
+ expect(mockOnMessageHandle).toHaveBeenCalledWith({
350
+ content: 'base64imagedata',
351
+ mimeType: 'image/png',
352
+ partType: 'image',
353
+ thoughtSignature: undefined,
354
+ type: 'content_part',
355
+ });
356
+
357
+ // Output should be empty since image content is not accumulated
358
+ expect(mockOnFinish).toHaveBeenCalledWith('', {
359
+ observationId: null,
360
+ toolCalls: undefined,
361
+ traceId: null,
362
+ type: 'done',
363
+ });
364
+ });
365
+ });
366
+
238
367
  it('should handle grounding event', async () => {
239
368
  const mockOnMessageHandle = vi.fn();
240
369
  const mockOnFinish = vi.fn();
@@ -438,8 +438,27 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
438
438
  break;
439
439
  }
440
440
 
441
- case 'reasoning_part':
441
+ case 'reasoning_part': {
442
+ // For reasoning_part, accumulate thinking content
443
+ if (data.partType === 'text' && data.content) {
444
+ thinking += data.content;
445
+ }
446
+ options.onMessageHandle?.({
447
+ content: data.content,
448
+ mimeType: data.mimeType,
449
+ partType: data.partType,
450
+ thoughtSignature: data.thoughtSignature,
451
+ type: ev.event,
452
+ });
453
+ break;
454
+ }
455
+
442
456
  case 'content_part': {
457
+ // For content_part, accumulate text content to output
458
+ // This is critical for Gemini 2.5 models which use content_part instead of text events
459
+ if (data.partType === 'text' && data.content) {
460
+ output += data.content;
461
+ }
443
462
  options.onMessageHandle?.({
444
463
  content: data.content,
445
464
  mimeType: data.mimeType,