@lobehub/chat 1.15.28 → 1.15.29

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.

Potentially problematic release.


This version of @lobehub/chat might be problematic. Click here for more details.

package/CHANGELOG.md CHANGED
@@ -2,6 +2,31 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.15.29](https://github.com/lobehub/lobe-chat/compare/v1.15.28...v1.15.29)
6
+
7
+ <sup>Released on **2024-09-09**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Gemini cannot input images when server database is enabled.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's fixed
19
+
20
+ - **misc**: Gemini cannot input images when server database is enabled, closes [#3370](https://github.com/lobehub/lobe-chat/issues/3370) ([eb552d2](https://github.com/lobehub/lobe-chat/commit/eb552d2))
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 1.15.28](https://github.com/lobehub/lobe-chat/compare/v1.15.27...v1.15.28)
6
31
 
7
32
  <sup>Released on **2024-09-09**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.15.28",
3
+ "version": "1.15.29",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot 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",
@@ -5,6 +5,7 @@ import OpenAI from 'openai';
5
5
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
6
 
7
7
  import { OpenAIChatMessage } from '@/libs/agent-runtime';
8
+ import * as imageToBase64Module from '@/utils/imageToBase64';
8
9
 
9
10
  import * as debugStreamModule from '../utils/debugStream';
10
11
  import { LobeGoogleAI } from './index';
@@ -303,36 +304,57 @@ describe('LobeGoogleAI', () => {
303
304
 
304
305
  describe('private method', () => {
305
306
  describe('convertContentToGooglePart', () => {
306
- it('should throw TypeError when image URL does not contain base64 data', () => {
307
- // 提供一个不包含base64数据的图像URL
308
- const invalidImageUrl = 'http://example.com/image.png';
307
+ it('should handle URL type images', async () => {
308
+ const imageUrl = 'http://example.com/image.png';
309
+ const mockBase64 = 'mockBase64Data';
309
310
 
310
- expect(() =>
311
+ // Mock the imageUrlToBase64 function
312
+ vi.spyOn(imageToBase64Module, 'imageUrlToBase64').mockResolvedValueOnce(mockBase64);
313
+
314
+ const result = await instance['convertContentToGooglePart']({
315
+ type: 'image_url',
316
+ image_url: { url: imageUrl },
317
+ });
318
+
319
+ expect(result).toEqual({
320
+ inlineData: {
321
+ data: mockBase64,
322
+ mimeType: 'image/png',
323
+ },
324
+ });
325
+
326
+ expect(imageToBase64Module.imageUrlToBase64).toHaveBeenCalledWith(imageUrl);
327
+ });
328
+
329
+ it('should throw TypeError for unsupported image URL types', async () => {
330
+ const unsupportedImageUrl = 'unsupported://example.com/image.png';
331
+
332
+ await expect(
311
333
  instance['convertContentToGooglePart']({
312
334
  type: 'image_url',
313
- image_url: { url: invalidImageUrl },
335
+ image_url: { url: unsupportedImageUrl },
314
336
  }),
315
- ).toThrow(TypeError);
337
+ ).rejects.toThrow(TypeError);
316
338
  });
317
339
  });
318
340
 
319
341
  describe('buildGoogleMessages', () => {
320
- it('get default result with gemini-pro', () => {
342
+ it('get default result with gemini-pro', async () => {
321
343
  const messages: OpenAIChatMessage[] = [{ content: 'Hello', role: 'user' }];
322
344
 
323
- const contents = instance['buildGoogleMessages'](messages, 'gemini-pro');
345
+ const contents = await instance['buildGoogleMessages'](messages, 'gemini-pro');
324
346
 
325
347
  expect(contents).toHaveLength(1);
326
348
  expect(contents).toEqual([{ parts: [{ text: 'Hello' }], role: 'user' }]);
327
349
  });
328
350
 
329
- it('messages should end with user if using gemini-pro', () => {
351
+ it('messages should end with user if using gemini-pro', async () => {
330
352
  const messages: OpenAIChatMessage[] = [
331
353
  { content: 'Hello', role: 'user' },
332
354
  { content: 'Hi', role: 'assistant' },
333
355
  ];
334
356
 
335
- const contents = instance['buildGoogleMessages'](messages, 'gemini-pro');
357
+ const contents = await instance['buildGoogleMessages'](messages, 'gemini-pro');
336
358
 
337
359
  expect(contents).toHaveLength(3);
338
360
  expect(contents).toEqual([
@@ -342,13 +364,13 @@ describe('LobeGoogleAI', () => {
342
364
  ]);
343
365
  });
344
366
 
345
- it('should include system role if there is a system role prompt', () => {
367
+ it('should include system role if there is a system role prompt', async () => {
346
368
  const messages: OpenAIChatMessage[] = [
347
369
  { content: 'you are ChatGPT', role: 'system' },
348
370
  { content: 'Who are you', role: 'user' },
349
371
  ];
350
372
 
351
- const contents = instance['buildGoogleMessages'](messages, 'gemini-pro');
373
+ const contents = await instance['buildGoogleMessages'](messages, 'gemini-pro');
352
374
 
353
375
  expect(contents).toHaveLength(3);
354
376
  expect(contents).toEqual([
@@ -358,13 +380,13 @@ describe('LobeGoogleAI', () => {
358
380
  ]);
359
381
  });
360
382
 
361
- it('should not modify the length if model is gemini-1.5-pro', () => {
383
+ it('should not modify the length if model is gemini-1.5-pro', async () => {
362
384
  const messages: OpenAIChatMessage[] = [
363
385
  { content: 'Hello', role: 'user' },
364
386
  { content: 'Hi', role: 'assistant' },
365
387
  ];
366
388
 
367
- const contents = instance['buildGoogleMessages'](messages, 'gemini-1.5-pro-latest');
389
+ const contents = await instance['buildGoogleMessages'](messages, 'gemini-1.5-pro-latest');
368
390
 
369
391
  expect(contents).toHaveLength(2);
370
392
  expect(contents).toEqual([
@@ -373,7 +395,7 @@ describe('LobeGoogleAI', () => {
373
395
  ]);
374
396
  });
375
397
 
376
- it('should use specified model when images are included in messages', () => {
398
+ it('should use specified model when images are included in messages', async () => {
377
399
  const messages: OpenAIChatMessage[] = [
378
400
  {
379
401
  content: [
@@ -386,7 +408,7 @@ describe('LobeGoogleAI', () => {
386
408
  const model = 'gemini-1.5-flash-latest';
387
409
 
388
410
  // 调用 buildGoogleMessages 方法
389
- const contents = instance['buildGoogleMessages'](messages, model);
411
+ const contents = await instance['buildGoogleMessages'](messages, model);
390
412
 
391
413
  expect(contents).toHaveLength(1);
392
414
  expect(contents).toEqual([
@@ -501,13 +523,13 @@ describe('LobeGoogleAI', () => {
501
523
  });
502
524
 
503
525
  describe('convertOAIMessagesToGoogleMessage', () => {
504
- it('should correctly convert assistant message', () => {
526
+ it('should correctly convert assistant message', async () => {
505
527
  const message: OpenAIChatMessage = {
506
528
  role: 'assistant',
507
529
  content: 'Hello',
508
530
  };
509
531
 
510
- const converted = instance['convertOAIMessagesToGoogleMessage'](message);
532
+ const converted = await instance['convertOAIMessagesToGoogleMessage'](message);
511
533
 
512
534
  expect(converted).toEqual({
513
535
  role: 'model',
@@ -515,13 +537,13 @@ describe('LobeGoogleAI', () => {
515
537
  });
516
538
  });
517
539
 
518
- it('should correctly convert user message', () => {
540
+ it('should correctly convert user message', async () => {
519
541
  const message: OpenAIChatMessage = {
520
542
  role: 'user',
521
543
  content: 'Hi',
522
544
  };
523
545
 
524
- const converted = instance['convertOAIMessagesToGoogleMessage'](message);
546
+ const converted = await instance['convertOAIMessagesToGoogleMessage'](message);
525
547
 
526
548
  expect(converted).toEqual({
527
549
  role: 'user',
@@ -529,7 +551,7 @@ describe('LobeGoogleAI', () => {
529
551
  });
530
552
  });
531
553
 
532
- it('should correctly convert message with inline base64 image parts', () => {
554
+ it('should correctly convert message with inline base64 image parts', async () => {
533
555
  const message: OpenAIChatMessage = {
534
556
  role: 'user',
535
557
  content: [
@@ -538,7 +560,7 @@ describe('LobeGoogleAI', () => {
538
560
  ],
539
561
  };
540
562
 
541
- const converted = instance['convertOAIMessagesToGoogleMessage'](message);
563
+ const converted = await instance['convertOAIMessagesToGoogleMessage'](message);
542
564
 
543
565
  expect(converted).toEqual({
544
566
  role: 'user',
@@ -548,7 +570,7 @@ describe('LobeGoogleAI', () => {
548
570
  ],
549
571
  });
550
572
  });
551
- it.skip('should correctly convert message with image url parts', () => {
573
+ it.skip('should correctly convert message with image url parts', async () => {
552
574
  const message: OpenAIChatMessage = {
553
575
  role: 'user',
554
576
  content: [
@@ -557,7 +579,7 @@ describe('LobeGoogleAI', () => {
557
579
  ],
558
580
  };
559
581
 
560
- const converted = instance['convertOAIMessagesToGoogleMessage'](message);
582
+ const converted = await instance['convertOAIMessagesToGoogleMessage'](message);
561
583
 
562
584
  expect(converted).toEqual({
563
585
  role: 'user',
@@ -10,6 +10,8 @@ import {
10
10
  import { JSONSchema7 } from 'json-schema';
11
11
  import { transform } from 'lodash-es';
12
12
 
13
+ import { imageUrlToBase64 } from '@/utils/imageToBase64';
14
+
13
15
  import { LobeRuntimeAI } from '../BaseAI';
14
16
  import { AgentRuntimeErrorType, ILobeAgentRuntimeErrorType } from '../error';
15
17
  import {
@@ -52,7 +54,7 @@ export class LobeGoogleAI implements LobeRuntimeAI {
52
54
  try {
53
55
  const model = payload.model;
54
56
 
55
- const contents = this.buildGoogleMessages(payload.messages, model);
57
+ const contents = await this.buildGoogleMessages(payload.messages, model);
56
58
 
57
59
  const geminiStreamResult = await this.client
58
60
  .getGenerativeModel(
@@ -109,7 +111,7 @@ export class LobeGoogleAI implements LobeRuntimeAI {
109
111
  }
110
112
  }
111
113
 
112
- private convertContentToGooglePart = (content: UserMessageContentPart): Part => {
114
+ private convertContentToGooglePart = async (content: UserMessageContentPart): Promise<Part> => {
113
115
  switch (content.type) {
114
116
  case 'text': {
115
117
  return { text: content.text };
@@ -130,51 +132,60 @@ export class LobeGoogleAI implements LobeRuntimeAI {
130
132
  };
131
133
  }
132
134
 
133
- // if (type === 'url') {
134
- // return {
135
- // fileData: {
136
- // fileUri: content.image_url.url,
137
- // mimeType: mimeType || 'image/png',
138
- // },
139
- // };
140
- // }
135
+ if (type === 'url') {
136
+ const base64Image = await imageUrlToBase64(content.image_url.url);
137
+
138
+ return {
139
+ inlineData: {
140
+ data: base64Image,
141
+ mimeType: mimeType || 'image/png',
142
+ },
143
+ };
144
+ }
141
145
 
142
146
  throw new TypeError(`currently we don't support image url: ${content.image_url.url}`);
143
147
  }
144
148
  }
145
149
  };
146
150
 
147
- private convertOAIMessagesToGoogleMessage = (message: OpenAIChatMessage): Content => {
151
+ private convertOAIMessagesToGoogleMessage = async (
152
+ message: OpenAIChatMessage,
153
+ ): Promise<Content> => {
148
154
  const content = message.content as string | UserMessageContentPart[];
149
155
 
150
156
  return {
151
157
  parts:
152
158
  typeof content === 'string'
153
159
  ? [{ text: content }]
154
- : content.map((c) => this.convertContentToGooglePart(c)),
160
+ : await Promise.all(content.map(async (c) => await this.convertContentToGooglePart(c))),
155
161
  role: message.role === 'assistant' ? 'model' : 'user',
156
162
  };
157
163
  };
158
164
 
159
165
  // convert messages from the Vercel AI SDK Format to the format
160
166
  // that is expected by the Google GenAI SDK
161
- private buildGoogleMessages = (messages: OpenAIChatMessage[], model: string): Content[] => {
167
+ private buildGoogleMessages = async (
168
+ messages: OpenAIChatMessage[],
169
+ model: string,
170
+ ): Promise<Content[]> => {
162
171
  // if the model is gemini-1.5-pro-latest, we don't need any special handling
163
172
  if (model === 'gemini-1.5-pro-latest') {
164
- return messages
173
+ const pools = messages
165
174
  .filter((message) => message.role !== 'function')
166
- .map((msg) => this.convertOAIMessagesToGoogleMessage(msg));
175
+ .map(async (msg) => await this.convertOAIMessagesToGoogleMessage(msg));
176
+
177
+ return Promise.all(pools);
167
178
  }
168
179
 
169
180
  const contents: Content[] = [];
170
181
  let lastRole = 'model';
171
182
 
172
- messages.forEach((message) => {
183
+ for (const message of messages) {
173
184
  // current to filter function message
174
185
  if (message.role === 'function') {
175
- return;
186
+ continue;
176
187
  }
177
- const googleMessage = this.convertOAIMessagesToGoogleMessage(message);
188
+ const googleMessage = await this.convertOAIMessagesToGoogleMessage(message);
178
189
 
179
190
  // if the last message is a model message and the current message is a model message
180
191
  // then we need to add a user message to separate them
@@ -187,7 +198,7 @@ export class LobeGoogleAI implements LobeRuntimeAI {
187
198
 
188
199
  // update the last role
189
200
  lastRole = googleMessage.role;
190
- });
201
+ }
191
202
 
192
203
  // if the last message is a user message, then we need to add a model message to separate them
193
204
  if (lastRole === 'model') {
@@ -35,3 +35,19 @@ export const imageToBase64 = ({
35
35
 
36
36
  return canvas.toDataURL(type);
37
37
  };
38
+
39
+ export const imageUrlToBase64 = async (imageUrl: string): Promise<string> => {
40
+ try {
41
+ const res = await fetch(imageUrl);
42
+ const arrayBuffer = await res.arrayBuffer();
43
+
44
+ return typeof btoa === 'function'
45
+ ? btoa(
46
+ new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), ''),
47
+ )
48
+ : Buffer.from(arrayBuffer).toString('base64');
49
+ } catch (error) {
50
+ console.error('Error converting image to base64:', error);
51
+ throw error;
52
+ }
53
+ };