@lobehub/chat 1.96.12 → 1.96.14

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.
@@ -1,4 +1,4 @@
1
- import { EnhancedGenerateContentResponse } from '@google/generative-ai';
1
+ import { GenerateContentResponse } from '@google/genai';
2
2
  import { describe, expect, it, vi } from 'vitest';
3
3
 
4
4
  import * as uuidModule from '@/utils/uuid';
@@ -11,10 +11,9 @@ describe('GoogleGenerativeAIStream', () => {
11
11
 
12
12
  const mockGenerateContentResponse = (text: string, functionCalls?: any[]) =>
13
13
  ({
14
- text: () => text,
15
- functionCall: () => functionCalls?.[0],
16
- functionCalls: () => functionCalls,
17
- }) as EnhancedGenerateContentResponse;
14
+ text: text,
15
+ functionCalls: functionCalls,
16
+ }) as unknown as GenerateContentResponse;
18
17
 
19
18
  const mockGoogleStream = new ReadableStream({
20
19
  start(controller) {
@@ -114,12 +113,6 @@ describe('GoogleGenerativeAIStream', () => {
114
113
  },
115
114
  modelVersion: 'gemini-2.0-flash-exp',
116
115
  };
117
- const mockGenerateContentResponse = (text: string, functionCalls?: any[]) =>
118
- ({
119
- text: () => text,
120
- functionCall: () => functionCalls?.[0],
121
- functionCalls: () => functionCalls,
122
- }) as EnhancedGenerateContentResponse;
123
116
 
124
117
  const mockGoogleStream = new ReadableStream({
125
118
  start(controller) {
@@ -209,7 +202,7 @@ describe('GoogleGenerativeAIStream', () => {
209
202
  ],
210
203
  },
211
204
  ],
212
- text: () => '234',
205
+ text: '234',
213
206
  usageMetadata: {
214
207
  promptTokenCount: 20,
215
208
  totalTokenCount: 20,
@@ -218,7 +211,7 @@ describe('GoogleGenerativeAIStream', () => {
218
211
  modelVersion: 'gemini-2.0-flash-exp-image-generation',
219
212
  },
220
213
  {
221
- text: () => '567890\n',
214
+ text: '567890\n',
222
215
  candidates: [
223
216
  {
224
217
  content: { parts: [{ text: '567890\n' }], role: 'model' },
@@ -299,7 +292,7 @@ describe('GoogleGenerativeAIStream', () => {
299
292
  ],
300
293
  },
301
294
  ],
302
- text: () => '234',
295
+ text: '234',
303
296
  usageMetadata: {
304
297
  promptTokenCount: 19,
305
298
  candidatesTokenCount: 3,
@@ -307,10 +300,10 @@ describe('GoogleGenerativeAIStream', () => {
307
300
  promptTokensDetails: [{ modality: 'TEXT', tokenCount: 19 }],
308
301
  thoughtsTokenCount: 100,
309
302
  },
310
- modelVersion: 'gemini-2.0-flash-exp-image-generation',
303
+ modelVersion: 'gemini-2.5-flash-preview-04-17',
311
304
  },
312
305
  {
313
- text: () => '567890\n',
306
+ text: '567890\n',
314
307
  candidates: [
315
308
  {
316
309
  content: { parts: [{ text: '567890\n' }], role: 'model' },
@@ -331,7 +324,7 @@ describe('GoogleGenerativeAIStream', () => {
331
324
  candidatesTokensDetails: [{ modality: 'TEXT', tokenCount: 11 }],
332
325
  thoughtsTokenCount: 100,
333
326
  },
334
- modelVersion: 'gemini-2.0-flash-exp-image-generation',
327
+ modelVersion: 'gemini-2.5-flash-preview-04-17',
335
328
  },
336
329
  ];
337
330
 
@@ -375,4 +368,410 @@ describe('GoogleGenerativeAIStream', () => {
375
368
  ].map((i) => i + '\n'),
376
369
  );
377
370
  });
371
+
372
+ it('should handle thought candidate part', async () => {
373
+ vi.spyOn(uuidModule, 'nanoid').mockReturnValueOnce('1');
374
+
375
+ const data = [
376
+ {
377
+ candidates: [
378
+ {
379
+ content: {
380
+ parts: [{ text: '**Understanding the Conditional Logic**\n\n', thought: true }],
381
+ role: 'model',
382
+ },
383
+ index: 0,
384
+ },
385
+ ],
386
+ text: '**Understanding the Conditional Logic**\n\n',
387
+ usageMetadata: {
388
+ promptTokenCount: 38,
389
+ candidatesTokenCount: 7,
390
+ totalTokenCount: 301,
391
+ promptTokensDetails: [{ modality: 'TEXT', tokenCount: 38 }],
392
+ thoughtsTokenCount: 256,
393
+ },
394
+ modelVersion: 'models/gemini-2.5-flash-preview-04-17',
395
+ },
396
+ {
397
+ candidates: [
398
+ {
399
+ content: {
400
+ parts: [{ text: '**Finalizing Interpretation**\n\n', thought: true }],
401
+ role: 'model',
402
+ },
403
+ index: 0,
404
+ },
405
+ ],
406
+ text: '**Finalizing Interpretation**\n\n',
407
+ usageMetadata: {
408
+ promptTokenCount: 38,
409
+ candidatesTokenCount: 13,
410
+ totalTokenCount: 355,
411
+ promptTokensDetails: [{ modality: 'TEXT', tokenCount: 38 }],
412
+ thoughtsTokenCount: 304,
413
+ },
414
+ modelVersion: 'models/gemini-2.5-flash-preview-04-17',
415
+ },
416
+ {
417
+ candidates: [
418
+ {
419
+ content: {
420
+ parts: [{ text: '简单来说,' }],
421
+ role: 'model',
422
+ },
423
+ index: 0,
424
+ },
425
+ ],
426
+ text: '简单来说,',
427
+ usageMetadata: {
428
+ promptTokenCount: 38,
429
+ candidatesTokenCount: 16,
430
+ totalTokenCount: 358,
431
+ promptTokensDetails: [{ modality: 'TEXT', tokenCount: 38 }],
432
+ thoughtsTokenCount: 304,
433
+ },
434
+ modelVersion: 'models/gemini-2.5-flash-preview-04-17',
435
+ },
436
+ {
437
+ candidates: [
438
+ {
439
+ content: { parts: [{ text: '文本内容。' }], role: 'model' },
440
+ finishReason: 'STOP',
441
+ index: 0,
442
+ },
443
+ ],
444
+ text: '文本内容。',
445
+ usageMetadata: {
446
+ promptTokenCount: 38,
447
+ candidatesTokenCount: 19,
448
+ totalTokenCount: 361,
449
+ promptTokensDetails: [{ modality: 'TEXT', tokenCount: 38 }],
450
+ thoughtsTokenCount: 304,
451
+ },
452
+ modelVersion: 'models/gemini-2.5-flash-preview-04-17',
453
+ },
454
+ ];
455
+
456
+ const mockGoogleStream = new ReadableStream({
457
+ start(controller) {
458
+ data.forEach((item) => {
459
+ controller.enqueue(item);
460
+ });
461
+
462
+ controller.close();
463
+ },
464
+ });
465
+
466
+ const protocolStream = GoogleGenerativeAIStream(mockGoogleStream);
467
+
468
+ const decoder = new TextDecoder();
469
+ const chunks = [];
470
+
471
+ // @ts-ignore
472
+ for await (const chunk of protocolStream) {
473
+ chunks.push(decoder.decode(chunk, { stream: true }));
474
+ }
475
+
476
+ expect(chunks).toEqual(
477
+ [
478
+ 'id: chat_1',
479
+ 'event: reasoning',
480
+ 'data: "**Understanding the Conditional Logic**\\n\\n"\n',
481
+
482
+ 'id: chat_1',
483
+ 'event: reasoning',
484
+ `data: "**Finalizing Interpretation**\\n\\n"\n`,
485
+
486
+ 'id: chat_1',
487
+ 'event: text',
488
+ `data: "简单来说,"\n`,
489
+
490
+ 'id: chat_1',
491
+ 'event: text',
492
+ `data: "文本内容。"\n`,
493
+ // stop
494
+ 'id: chat_1',
495
+ 'event: stop',
496
+ `data: "STOP"\n`,
497
+ // usage
498
+ 'id: chat_1',
499
+ 'event: usage',
500
+ `data: {"inputTextTokens":38,"outputReasoningTokens":304,"outputTextTokens":19,"totalInputTokens":38,"totalOutputTokens":323,"totalTokens":361}\n`,
501
+ ].map((i) => i + '\n'),
502
+ );
503
+ });
504
+
505
+ it('should return undefined data without text', async () => {
506
+ vi.spyOn(uuidModule, 'nanoid').mockReturnValueOnce('1');
507
+
508
+ const data = [
509
+ {
510
+ candidates: [
511
+ {
512
+ content: { parts: [{ text: '234' }], role: 'model' },
513
+ safetyRatings: [
514
+ { category: 'HARM_CATEGORY_HATE_SPEECH', probability: 'NEGLIGIBLE' },
515
+ { category: 'HARM_CATEGORY_DANGEROUS_CONTENT', probability: 'NEGLIGIBLE' },
516
+ { category: 'HARM_CATEGORY_HARASSMENT', probability: 'NEGLIGIBLE' },
517
+ { category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', probability: 'NEGLIGIBLE' },
518
+ ],
519
+ },
520
+ ],
521
+ text: '234',
522
+ usageMetadata: {
523
+ promptTokenCount: 19,
524
+ candidatesTokenCount: 3,
525
+ totalTokenCount: 122,
526
+ promptTokensDetails: [{ modality: 'TEXT', tokenCount: 19 }],
527
+ thoughtsTokenCount: 100,
528
+ },
529
+ modelVersion: 'gemini-2.5-flash-preview-04-17',
530
+ },
531
+ {
532
+ text: '',
533
+ candidates: [
534
+ {
535
+ content: { parts: [{ text: '' }], role: 'model' },
536
+ safetyRatings: [
537
+ { category: 'HARM_CATEGORY_HATE_SPEECH', probability: 'NEGLIGIBLE' },
538
+ { category: 'HARM_CATEGORY_DANGEROUS_CONTENT', probability: 'NEGLIGIBLE' },
539
+ { category: 'HARM_CATEGORY_HARASSMENT', probability: 'NEGLIGIBLE' },
540
+ { category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', probability: 'NEGLIGIBLE' },
541
+ ],
542
+ },
543
+ ],
544
+ usageMetadata: {
545
+ promptTokenCount: 19,
546
+ candidatesTokenCount: 3,
547
+ totalTokenCount: 122,
548
+ promptTokensDetails: [{ modality: 'TEXT', tokenCount: 19 }],
549
+ candidatesTokensDetails: [{ modality: 'TEXT', tokenCount: 3 }],
550
+ thoughtsTokenCount: 100,
551
+ },
552
+ modelVersion: 'gemini-2.5-flash-preview-04-17',
553
+ },
554
+ {
555
+ text: '567890\n',
556
+ candidates: [
557
+ {
558
+ content: { parts: [{ text: '567890\n' }], role: 'model' },
559
+ finishReason: 'STOP',
560
+ safetyRatings: [
561
+ { category: 'HARM_CATEGORY_HATE_SPEECH', probability: 'NEGLIGIBLE' },
562
+ { category: 'HARM_CATEGORY_DANGEROUS_CONTENT', probability: 'NEGLIGIBLE' },
563
+ { category: 'HARM_CATEGORY_HARASSMENT', probability: 'NEGLIGIBLE' },
564
+ { category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', probability: 'NEGLIGIBLE' },
565
+ ],
566
+ },
567
+ ],
568
+ usageMetadata: {
569
+ promptTokenCount: 19,
570
+ candidatesTokenCount: 11,
571
+ totalTokenCount: 131,
572
+ promptTokensDetails: [{ modality: 'TEXT', tokenCount: 19 }],
573
+ candidatesTokensDetails: [{ modality: 'TEXT', tokenCount: 11 }],
574
+ thoughtsTokenCount: 100,
575
+ },
576
+ modelVersion: 'gemini-2.5-flash-preview-04-17',
577
+ },
578
+ ];
579
+
580
+ const mockGoogleStream = new ReadableStream({
581
+ start(controller) {
582
+ data.forEach((item) => {
583
+ controller.enqueue(item);
584
+ });
585
+
586
+ controller.close();
587
+ },
588
+ });
589
+
590
+ const protocolStream = GoogleGenerativeAIStream(mockGoogleStream);
591
+
592
+ const decoder = new TextDecoder();
593
+ const chunks = [];
594
+
595
+ // @ts-ignore
596
+ for await (const chunk of protocolStream) {
597
+ chunks.push(decoder.decode(chunk, { stream: true }));
598
+ }
599
+
600
+ expect(chunks).toEqual(
601
+ [
602
+ 'id: chat_1',
603
+ 'event: text',
604
+ 'data: "234"\n',
605
+
606
+ 'id: chat_1',
607
+ 'event: text',
608
+ 'data: ""\n',
609
+
610
+ 'id: chat_1',
611
+ 'event: text',
612
+ `data: "567890\\n"\n`,
613
+ // stop
614
+ 'id: chat_1',
615
+ 'event: stop',
616
+ `data: "STOP"\n`,
617
+ // usage
618
+ 'id: chat_1',
619
+ 'event: usage',
620
+ `data: {"inputTextTokens":19,"outputReasoningTokens":100,"outputTextTokens":11,"totalInputTokens":19,"totalOutputTokens":111,"totalTokens":131}\n`,
621
+ ].map((i) => i + '\n'),
622
+ );
623
+ });
624
+
625
+ it('should handle groundingMetadata', async () => {
626
+ vi.spyOn(uuidModule, 'nanoid').mockReturnValueOnce('1');
627
+
628
+ const data = [
629
+ {
630
+ text: '123',
631
+ candidates: [
632
+ {
633
+ content: {
634
+ parts: [
635
+ {
636
+ text: '123',
637
+ },
638
+ ],
639
+ role: 'model',
640
+ },
641
+ index: 0,
642
+ groundingMetadata: {},
643
+ },
644
+ ],
645
+ usageMetadata: {
646
+ promptTokenCount: 9,
647
+ candidatesTokenCount: 18,
648
+ totalTokenCount: 27,
649
+ promptTokensDetails: [
650
+ {
651
+ modality: 'TEXT',
652
+ tokenCount: 9,
653
+ },
654
+ ],
655
+ },
656
+ modelVersion: 'models/gemini-2.5-flash-preview-04-17',
657
+ },
658
+ {
659
+ text: '45678',
660
+ candidates: [
661
+ {
662
+ content: {
663
+ parts: [
664
+ {
665
+ text: '45678',
666
+ },
667
+ ],
668
+ role: 'model',
669
+ },
670
+ finishReason: 'STOP',
671
+ index: 0,
672
+ groundingMetadata: {
673
+ searchEntryPoint: {
674
+ renderedContent: 'content\n',
675
+ },
676
+ groundingChunks: [
677
+ {
678
+ web: {
679
+ uri: 'https://vertexaisearch.cloud.google.com/grounding-api-redirect/AbF9wXG1234545',
680
+ title: 'npmjs.com',
681
+ },
682
+ },
683
+ {
684
+ web: {
685
+ uri: 'https://vertexaisearch.cloud.google.com/grounding-api-redirect/AbF9wXE9288334',
686
+ title: 'google.dev',
687
+ },
688
+ },
689
+ ],
690
+ groundingSupports: [
691
+ {
692
+ segment: {
693
+ startIndex: 63,
694
+ endIndex: 67,
695
+ text: '1。',
696
+ },
697
+ groundingChunkIndices: [0],
698
+ confidenceScores: [1],
699
+ },
700
+ {
701
+ segment: {
702
+ startIndex: 69,
703
+ endIndex: 187,
704
+ text: 'SDK。',
705
+ },
706
+ groundingChunkIndices: [1],
707
+ confidenceScores: [1],
708
+ },
709
+ ],
710
+ webSearchQueries: ['sdk latest version'],
711
+ },
712
+ },
713
+ ],
714
+ usageMetadata: {
715
+ promptTokenCount: 9,
716
+ candidatesTokenCount: 122,
717
+ totalTokenCount: 131,
718
+ promptTokensDetails: [
719
+ {
720
+ modality: 'TEXT',
721
+ tokenCount: 9,
722
+ },
723
+ ],
724
+ },
725
+ modelVersion: 'models/gemini-2.5-flash-preview-04-17',
726
+ },
727
+ ];
728
+
729
+ const mockGoogleStream = new ReadableStream({
730
+ start(controller) {
731
+ data.forEach((item) => {
732
+ controller.enqueue(item);
733
+ });
734
+
735
+ controller.close();
736
+ },
737
+ });
738
+
739
+ const protocolStream = GoogleGenerativeAIStream(mockGoogleStream);
740
+
741
+ const decoder = new TextDecoder();
742
+ const chunks = [];
743
+
744
+ // @ts-ignore
745
+ for await (const chunk of protocolStream) {
746
+ chunks.push(decoder.decode(chunk, { stream: true }));
747
+ }
748
+
749
+ expect(chunks).toEqual(
750
+ [
751
+ 'id: chat_1',
752
+ 'event: text',
753
+ 'data: "123"\n',
754
+
755
+ 'id: chat_1',
756
+ 'event: grounding',
757
+ 'data: {}\n',
758
+
759
+ 'id: chat_1',
760
+ 'event: text',
761
+ 'data: "45678"\n',
762
+
763
+ 'id: chat_1',
764
+ 'event: grounding',
765
+ `data: {\"citations\":[{\"favicon\":\"npmjs.com\",\"title\":\"npmjs.com\",\"url\":\"https://vertexaisearch.cloud.google.com/grounding-api-redirect/AbF9wXG1234545\"},{\"favicon\":\"google.dev\",\"title\":\"google.dev\",\"url\":\"https://vertexaisearch.cloud.google.com/grounding-api-redirect/AbF9wXE9288334\"}],\"searchQueries\":[\"sdk latest version\"]}\n`,
766
+ // stop
767
+ 'id: chat_1',
768
+ 'event: stop',
769
+ `data: "STOP"\n`,
770
+ // usage
771
+ 'id: chat_1',
772
+ 'event: usage',
773
+ `data: {"inputTextTokens":9,"outputTextTokens":122,"totalInputTokens":9,"totalOutputTokens":122,"totalTokens":131}\n`,
774
+ ].map((i) => i + '\n'),
775
+ );
776
+ });
378
777
  });
@@ -1,4 +1,4 @@
1
- import { EnhancedGenerateContentResponse } from '@google/generative-ai';
1
+ import { GenerateContentResponse } from '@google/genai';
2
2
 
3
3
  import { ModelTokensUsage } from '@/types/message';
4
4
  import { GroundingSearch } from '@/types/search';
@@ -16,7 +16,7 @@ import {
16
16
  } from './protocol';
17
17
 
18
18
  const transformGoogleGenerativeAIStream = (
19
- chunk: EnhancedGenerateContentResponse,
19
+ chunk: GenerateContentResponse,
20
20
  context: StreamContext,
21
21
  ): StreamProtocolChunk | StreamProtocolChunk[] => {
22
22
  // maybe need another structure to add support for multiple choices
@@ -24,22 +24,22 @@ const transformGoogleGenerativeAIStream = (
24
24
  const usage = chunk.usageMetadata;
25
25
  const usageChunks: StreamProtocolChunk[] = [];
26
26
  if (candidate?.finishReason && usage) {
27
- const outputReasoningTokens = (usage as any).thoughtsTokenCount || undefined;
28
- const totalOutputTokens = (usage.candidatesTokenCount ?? 0) + (outputReasoningTokens ?? 0);
27
+ // totalTokenCount = promptTokenCount + candidatesTokenCount + thoughtsTokenCount
28
+ const reasoningTokens = usage.thoughtsTokenCount;
29
+ const outputTextTokens = usage.candidatesTokenCount ?? 0;
30
+ const totalOutputTokens = outputTextTokens + (reasoningTokens ?? 0);
29
31
 
30
32
  usageChunks.push(
31
33
  { data: candidate.finishReason, id: context?.id, type: 'stop' },
32
34
  {
33
35
  data: {
34
36
  // TODO: Google SDK 0.24.0 don't have promptTokensDetails types
35
- inputImageTokens: (usage as any).promptTokensDetails?.find(
36
- (i: any) => i.modality === 'IMAGE',
37
- )?.tokenCount,
38
- inputTextTokens: (usage as any).promptTokensDetails?.find(
39
- (i: any) => i.modality === 'TEXT',
40
- )?.tokenCount,
41
- outputReasoningTokens,
42
- outputTextTokens: totalOutputTokens - (outputReasoningTokens ?? 0),
37
+ inputImageTokens: usage.promptTokensDetails?.find((i: any) => i.modality === 'IMAGE')
38
+ ?.tokenCount,
39
+ inputTextTokens: usage.promptTokensDetails?.find((i: any) => i.modality === 'TEXT')
40
+ ?.tokenCount,
41
+ outputReasoningTokens: reasoningTokens,
42
+ outputTextTokens,
43
43
  totalInputTokens: usage.promptTokenCount,
44
44
  totalOutputTokens,
45
45
  totalTokens: usage.totalTokenCount,
@@ -50,7 +50,7 @@ const transformGoogleGenerativeAIStream = (
50
50
  );
51
51
  }
52
52
 
53
- const functionCalls = chunk.functionCalls?.();
53
+ const functionCalls = chunk.functionCalls;
54
54
 
55
55
  if (functionCalls) {
56
56
  return [
@@ -73,11 +73,11 @@ const transformGoogleGenerativeAIStream = (
73
73
  ];
74
74
  }
75
75
 
76
- const text = chunk.text?.();
76
+ const text = chunk.text;
77
77
 
78
78
  if (candidate) {
79
79
  // 首先检查是否为 reasoning 内容 (thought: true)
80
- if (Array.isArray(candidate.content.parts) && candidate.content.parts.length > 0) {
80
+ if (Array.isArray(candidate.content?.parts) && candidate.content.parts.length > 0) {
81
81
  for (const part of candidate.content.parts) {
82
82
  if (part && part.text && (part as any).thought === true) {
83
83
  return { data: part.text, id: context.id, type: 'reasoning' };
@@ -122,7 +122,7 @@ const transformGoogleGenerativeAIStream = (
122
122
  if (!!text?.trim()) return { data: text, id: context?.id, type: 'text' };
123
123
 
124
124
  // streaming the image
125
- if (Array.isArray(candidate.content.parts) && candidate.content.parts.length > 0) {
125
+ if (Array.isArray(candidate.content?.parts) && candidate.content.parts.length > 0) {
126
126
  const part = candidate.content.parts[0];
127
127
 
128
128
  if (part && part.inlineData && part.inlineData.data && part.inlineData.mimeType) {
@@ -148,7 +148,7 @@ export interface GoogleAIStreamOptions {
148
148
  }
149
149
 
150
150
  export const GoogleGenerativeAIStream = (
151
- rawStream: ReadableStream<EnhancedGenerateContentResponse>,
151
+ rawStream: ReadableStream<GenerateContentResponse>,
152
152
  { callbacks, inputStartAt }: GoogleAIStreamOptions = {},
153
153
  ) => {
154
154
  const streamStack: StreamContext = { id: 'chat_' + nanoid() };