@lobehub/chat 1.101.2 → 1.102.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 CHANGED
@@ -2,6 +2,64 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.102.1](https://github.com/lobehub/lobe-chat/compare/v1.102.0...v1.102.1)
6
+
7
+ <sup>Released on **2025-07-21**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **groq**: Enable streaming for tool calls and add Kimi K2 model.
12
+
13
+ #### 💄 Styles
14
+
15
+ - **misc**: Modal list header sticky style.
16
+
17
+ <br/>
18
+
19
+ <details>
20
+ <summary><kbd>Improvements and Fixes</kbd></summary>
21
+
22
+ #### What's fixed
23
+
24
+ - **groq**: Enable streaming for tool calls and add Kimi K2 model, closes [#8510](https://github.com/lobehub/lobe-chat/issues/8510) ([60739bc](https://github.com/lobehub/lobe-chat/commit/60739bc))
25
+
26
+ #### Styles
27
+
28
+ - **misc**: Modal list header sticky style, closes [#8514](https://github.com/lobehub/lobe-chat/issues/8514) ([75273d5](https://github.com/lobehub/lobe-chat/commit/75273d5))
29
+
30
+ </details>
31
+
32
+ <div align="right">
33
+
34
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
35
+
36
+ </div>
37
+
38
+ ## [Version 1.102.0](https://github.com/lobehub/lobe-chat/compare/v1.101.2...v1.102.0)
39
+
40
+ <sup>Released on **2025-07-21**</sup>
41
+
42
+ #### ✨ Features
43
+
44
+ - **misc**: Add image generation capabilities using Google AI Imagen API.
45
+
46
+ <br/>
47
+
48
+ <details>
49
+ <summary><kbd>Improvements and Fixes</kbd></summary>
50
+
51
+ #### What's improved
52
+
53
+ - **misc**: Add image generation capabilities using Google AI Imagen API, closes [#8503](https://github.com/lobehub/lobe-chat/issues/8503) ([cef8208](https://github.com/lobehub/lobe-chat/commit/cef8208))
54
+
55
+ </details>
56
+
57
+ <div align="right">
58
+
59
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
60
+
61
+ </div>
62
+
5
63
  ### [Version 1.101.2](https://github.com/lobehub/lobe-chat/compare/v1.101.1...v1.101.2)
6
64
 
7
65
  <sup>Released on **2025-07-21**</sup>
package/README.md CHANGED
@@ -383,8 +383,8 @@ In addition, these plugins are not limited to news aggregation, but can also ext
383
383
 
384
384
  | Recent Submits | Description |
385
385
  | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
386
+ | [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-07-21**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
386
387
  | [Speak](https://lobechat.com/discover/plugin/speak)<br/><sup>By **speak** on **2025-07-18**</sup> | Learn how to say anything in another language with Speak, your AI-powered language tutor.<br/>`education` `language` |
387
- | [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-05-27**</sup> | Analyze stocks and get comprehensive real-time investment data and analytics.<br/>`stock` |
388
388
  | [Web](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | Smart web search that reads and analyzes pages to deliver comprehensive answers from Google results.<br/>`web` `search` |
389
389
  | [Bing_websearch](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | Search for information from the internet base BingApi<br/>`bingsearch` |
390
390
 
package/README.zh-CN.md CHANGED
@@ -376,8 +376,8 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
376
376
 
377
377
  | 最近新增 | 描述 |
378
378
  | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
379
+ | [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-07-21**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
379
380
  | [Speak](https://lobechat.com/discover/plugin/speak)<br/><sup>By **speak** on **2025-07-18**</sup> | 使用 Speak,您的 AI 语言导师,学习如何用另一种语言说任何事情。<br/>`教育` `语言` |
380
- | [PortfolioMeta](https://lobechat.com/discover/plugin/StockData)<br/><sup>By **portfoliometa** on **2025-05-27**</sup> | 分析股票并获取全面的实时投资数据和分析。<br/>`股票` |
381
381
  | [网页](https://lobechat.com/discover/plugin/web)<br/><sup>By **Proghit** on **2025-01-24**</sup> | 智能网页搜索,读取和分析页面,以提供来自 Google 结果的全面答案。<br/>`网页` `搜索` |
382
382
  | [必应网页搜索](https://lobechat.com/discover/plugin/Bingsearch-identifier)<br/><sup>By **FineHow** on **2024-12-22**</sup> | 通过 BingApi 搜索互联网上的信息<br/>`bingsearch` |
383
383
 
package/changelog/v1.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Modal list header sticky style."
6
+ ]
7
+ },
8
+ "date": "2025-07-21",
9
+ "version": "1.102.1"
10
+ },
11
+ {
12
+ "children": {
13
+ "features": [
14
+ "Add image generation capabilities using Google AI Imagen API."
15
+ ]
16
+ },
17
+ "date": "2025-07-21",
18
+ "version": "1.102.0"
19
+ },
2
20
  {
3
21
  "children": {
4
22
  "improvements": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.101.2",
3
+ "version": "1.102.1",
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",
@@ -58,8 +58,10 @@ const ModelTitle = memo<ModelFetcherProps>(
58
58
  paddingBlock={8}
59
59
  style={{
60
60
  background: theme.colorBgContainer,
61
+ marginTop: mobile ? 0 : -12,
62
+ paddingTop: mobile ? 0 : 20,
61
63
  position: 'sticky',
62
- top: mobile ? -2 : -16,
64
+ top: mobile ? -2 : -32,
63
65
  zIndex: 15,
64
66
  }}
65
67
  >
@@ -1,4 +1,5 @@
1
- import { AIChatModelCard } from '@/types/aiModel';
1
+ import { ModelParamsSchema } from '@/libs/standard-parameters';
2
+ import { AIChatModelCard, AIImageModelCard } from '@/types/aiModel';
2
3
 
3
4
  const googleChatModels: AIChatModelCard[] = [
4
5
  {
@@ -496,6 +497,38 @@ const googleChatModels: AIChatModelCard[] = [
496
497
  },
497
498
  ];
498
499
 
499
- export const allModels = [...googleChatModels];
500
+ // Common parameters for Imagen models
501
+ const imagenBaseParameters: ModelParamsSchema = {
502
+ aspectRatio: {
503
+ default: '1:1',
504
+ enum: ['1:1', '16:9', '9:16', '3:4', '4:3'],
505
+ },
506
+ prompt: { default: '' },
507
+ };
508
+
509
+ const googleImageModels: AIImageModelCard[] = [
510
+ {
511
+ description: 'Imagen 4th generation text-to-image model series',
512
+ displayName: 'Imagen4 Preview 06-06',
513
+ enabled: true,
514
+ id: 'imagen-4.0-generate-preview-06-06',
515
+ organization: 'Deepmind',
516
+ parameters: imagenBaseParameters,
517
+ releasedAt: '2024-06-06',
518
+ type: 'image',
519
+ },
520
+ {
521
+ description: 'Imagen 4th generation text-to-image model series Ultra version',
522
+ displayName: 'Imagen4 Ultra Preview 06-06',
523
+ enabled: true,
524
+ id: 'imagen-4.0-ultra-generate-preview-06-06',
525
+ organization: 'Deepmind',
526
+ parameters: imagenBaseParameters,
527
+ releasedAt: '2024-06-06',
528
+ type: 'image',
529
+ },
530
+ ];
531
+
532
+ export const allModels = [...googleChatModels, ...googleImageModels];
500
533
 
501
534
  export default allModels;
@@ -23,6 +23,23 @@ const groqChatModels: AIChatModelCard[] = [
23
23
  maxOutput: 8192,
24
24
  type: 'chat',
25
25
  },
26
+ {
27
+ abilities: {
28
+ functionCall: true,
29
+ },
30
+ contextWindowTokens: 131_072,
31
+ description:
32
+ 'kimi-k2 是一款具备超强代码和 Agent 能力的 MoE 架构基础模型,总参数 1T,激活参数 32B。在通用知识推理、编程、数学、Agent 等主要类别的基准性能测试中,K2 模型的性能超过其他主流开源模型。',
33
+ displayName: 'Kimi K2 Instruct',
34
+ enabled: true,
35
+ id: 'moonshotai/kimi-k2-instruct',
36
+ pricing: {
37
+ input: 1,
38
+ output: 3,
39
+ },
40
+ releasedAt: '2025-07-11',
41
+ type: 'chat',
42
+ },
26
43
  {
27
44
  contextWindowTokens: 131_072,
28
45
  displayName: 'Llama 4 Scout (17Bx16E)',
@@ -4,6 +4,7 @@ import OpenAI from 'openai';
4
4
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
5
 
6
6
  import { OpenAIChatMessage } from '@/libs/model-runtime';
7
+ import { CreateImagePayload } from '@/libs/model-runtime/types/image';
7
8
  import { ChatStreamPayload } from '@/types/openai/chat';
8
9
  import * as imageToBase64Module from '@/utils/imageToBase64';
9
10
 
@@ -22,7 +23,7 @@ let instance: LobeGoogleAI;
22
23
  beforeEach(() => {
23
24
  instance = new LobeGoogleAI({ apiKey: 'test' });
24
25
 
25
- // 使用 vi.spyOn 来模拟 chat.completions.create 方法
26
+ // Use vi.spyOn to mock the chat.completions.create method
26
27
  const mockStreamData = (async function* (): AsyncGenerator<GenerateContentResponse> {})();
27
28
  vi.spyOn(instance['client'].models, 'generateContentStream').mockResolvedValue(mockStreamData);
28
29
  });
@@ -53,7 +54,7 @@ describe('LobeGoogleAI', () => {
53
54
  expect(result).toBeInstanceOf(Response);
54
55
  });
55
56
  it('should handle text messages correctly', async () => {
56
- // 模拟 Google AI SDK generateContentStream 方法返回一个成功的响应流
57
+ // Mock Google AI SDK's generateContentStream method to return a successful response stream
57
58
  const mockStream = new ReadableStream({
58
59
  start(controller) {
59
60
  controller.enqueue('Hello, world!');
@@ -71,7 +72,7 @@ describe('LobeGoogleAI', () => {
71
72
  });
72
73
 
73
74
  expect(result).toBeInstanceOf(Response);
74
- // 额外的断言可以加入,比如验证返回的流内容等
75
+ // Additional assertions can be added, such as verifying the returned stream content
75
76
  });
76
77
 
77
78
  it('should withGrounding', () => {
@@ -214,10 +215,10 @@ describe('LobeGoogleAI', () => {
214
215
  });
215
216
 
216
217
  it('should call debugStream in DEBUG mode', async () => {
217
- // 设置环境变量以启用DEBUG模式
218
+ // Set environment variable to enable DEBUG mode
218
219
  process.env.DEBUG_GOOGLE_CHAT_COMPLETION = '1';
219
220
 
220
- // 模拟 Google AI SDK generateContentStream 方法返回一个成功的响应流
221
+ // Mock Google AI SDK's generateContentStream method to return a successful response stream
221
222
  const mockStream = new ReadableStream({
222
223
  start(controller) {
223
224
  controller.enqueue('Debug mode test');
@@ -239,13 +240,13 @@ describe('LobeGoogleAI', () => {
239
240
 
240
241
  expect(debugStreamSpy).toHaveBeenCalled();
241
242
 
242
- // 清理环境变量
243
+ // Clean up environment variable
243
244
  delete process.env.DEBUG_GOOGLE_CHAT_COMPLETION;
244
245
  });
245
246
 
246
247
  describe('Error', () => {
247
248
  it('should throw InvalidGoogleAPIKey error on API_KEY_INVALID error', async () => {
248
- // 模拟 Google AI SDK 抛出异常
249
+ // Mock Google AI SDK throwing an exception
249
250
  const message = `[GoogleGenerativeAI Error]: Error fetching from https://generativelanguage.googleapis.com/v1/models/gemini-pro:streamGenerateContent?alt=sse: [400 Bad Request] API key not valid. Please pass a valid API key. [{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"API_KEY_INVALID","domain":"googleapis.com","metadata":{"service":"generativelanguage.googleapis.com"}}]`;
250
251
 
251
252
  const apiError = new Error(message);
@@ -264,7 +265,7 @@ describe('LobeGoogleAI', () => {
264
265
  });
265
266
 
266
267
  it('should throw LocationNotSupportError error on location not support error', async () => {
267
- // 模拟 Google AI SDK 抛出异常
268
+ // Mock Google AI SDK throwing an exception
268
269
  const message = `[GoogleGenerativeAI Error]: Error fetching from https://generativelanguage.googleapis.com/v1/models/gemini-pro:streamGenerateContent?alt=sse: [400 Bad Request] User location is not supported for the API use.`;
269
270
 
270
271
  const apiError = new Error(message);
@@ -283,7 +284,7 @@ describe('LobeGoogleAI', () => {
283
284
  });
284
285
 
285
286
  it('should throw BizError error', async () => {
286
- // 模拟 Google AI SDK 抛出异常
287
+ // Mock Google AI SDK throwing an exception
287
288
  const message = `[GoogleGenerativeAI Error]: Error fetching from https://generativelanguage.googleapis.com/v1/models/gemini-pro:streamGenerateContent?alt=sse: [400 Bad Request] API key not valid. Please pass a valid API key. [{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"Error","domain":"googleapis.com","metadata":{"service":"generativelanguage.googleapis.com"}}]`;
288
289
 
289
290
  const apiError = new Error(message);
@@ -315,7 +316,7 @@ describe('LobeGoogleAI', () => {
315
316
  });
316
317
 
317
318
  it('should throw DefaultError error', async () => {
318
- // 模拟 Google AI SDK 抛出异常
319
+ // Mock Google AI SDK throwing an exception
319
320
  const message = `[GoogleGenerativeAI Error]: Error fetching from https://generativelanguage.googleapis.com/v1/models/gemini-pro:streamGenerateContent?alt=sse: [400 Bad Request] API key not valid. Please pass a valid API key. [{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"Error","domain":"googleapis.com","metadata":{"service":"generativelanguage.googleapis.com}}]`;
320
321
 
321
322
  const apiError = new Error(message);
@@ -345,7 +346,7 @@ describe('LobeGoogleAI', () => {
345
346
  // Arrange
346
347
  const apiError = new Error('Error message');
347
348
 
348
- // 使用 vi.spyOn 来模拟 chat.completions.create 方法
349
+ // Use vi.spyOn to mock the chat.completions.create method
349
350
  vi.spyOn(instance['client'].models, 'generateContentStream').mockRejectedValue(apiError);
350
351
 
351
352
  // Act
@@ -537,7 +538,7 @@ describe('LobeGoogleAI', () => {
537
538
  },
538
539
  ];
539
540
 
540
- // 调用 buildGoogleMessages 方法
541
+ // Call the buildGoogleMessages method
541
542
  const contents = await instance['buildGoogleMessages'](messages);
542
543
 
543
544
  expect(contents).toHaveLength(1);
@@ -826,4 +827,331 @@ describe('LobeGoogleAI', () => {
826
827
  });
827
828
  });
828
829
  });
830
+
831
+ describe('createImage', () => {
832
+ it('should create image successfully with basic parameters', async () => {
833
+ // Arrange - Use real base64 image data (5x5 red pixel PNG)
834
+ const realBase64ImageData =
835
+ 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
836
+ const mockImageResponse = {
837
+ generatedImages: [
838
+ {
839
+ image: {
840
+ imageBytes: realBase64ImageData,
841
+ },
842
+ },
843
+ ],
844
+ };
845
+ vi.spyOn(instance['client'].models, 'generateImages').mockResolvedValue(
846
+ mockImageResponse as any,
847
+ );
848
+
849
+ const payload: CreateImagePayload = {
850
+ model: 'imagen-4.0-generate-preview-06-06',
851
+ params: {
852
+ prompt: 'A beautiful landscape with mountains and trees',
853
+ aspectRatio: '1:1',
854
+ },
855
+ };
856
+
857
+ // Act
858
+ const result = await instance.createImage(payload);
859
+
860
+ // Assert
861
+ expect(instance['client'].models.generateImages).toHaveBeenCalledWith({
862
+ model: 'imagen-4.0-generate-preview-06-06',
863
+ prompt: 'A beautiful landscape with mountains and trees',
864
+ config: {
865
+ aspectRatio: '1:1',
866
+ numberOfImages: 1,
867
+ },
868
+ });
869
+ expect(result).toEqual({
870
+ imageUrl: `data:image/png;base64,${realBase64ImageData}`,
871
+ });
872
+ });
873
+
874
+ it('should support different aspect ratios like 16:9 for widescreen images', async () => {
875
+ // Arrange - Use real base64 data
876
+ const realBase64Data =
877
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
878
+ const mockImageResponse = {
879
+ generatedImages: [
880
+ {
881
+ image: {
882
+ imageBytes: realBase64Data,
883
+ },
884
+ },
885
+ ],
886
+ };
887
+ vi.spyOn(instance['client'].models, 'generateImages').mockResolvedValue(
888
+ mockImageResponse as any,
889
+ );
890
+
891
+ const payload: CreateImagePayload = {
892
+ model: 'imagen-4.0-ultra-generate-preview-06-06',
893
+ params: {
894
+ prompt: 'Cinematic landscape shot with dramatic lighting',
895
+ aspectRatio: '16:9',
896
+ },
897
+ };
898
+
899
+ // Act
900
+ await instance.createImage(payload);
901
+
902
+ // Assert
903
+ expect(instance['client'].models.generateImages).toHaveBeenCalledWith({
904
+ model: 'imagen-4.0-ultra-generate-preview-06-06',
905
+ prompt: 'Cinematic landscape shot with dramatic lighting',
906
+ config: {
907
+ aspectRatio: '16:9',
908
+ numberOfImages: 1,
909
+ },
910
+ });
911
+ });
912
+
913
+ it('should work with only prompt when aspect ratio is not specified', async () => {
914
+ // Arrange
915
+ const realBase64Data =
916
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
917
+ const mockImageResponse = {
918
+ generatedImages: [
919
+ {
920
+ image: {
921
+ imageBytes: realBase64Data,
922
+ },
923
+ },
924
+ ],
925
+ };
926
+ vi.spyOn(instance['client'].models, 'generateImages').mockResolvedValue(
927
+ mockImageResponse as any,
928
+ );
929
+
930
+ const payload: CreateImagePayload = {
931
+ model: 'imagen-4.0-generate-preview-06-06',
932
+ params: {
933
+ prompt: 'A cute cat sitting in a garden',
934
+ },
935
+ };
936
+
937
+ // Act
938
+ await instance.createImage(payload);
939
+
940
+ // Assert
941
+ expect(instance['client'].models.generateImages).toHaveBeenCalledWith({
942
+ model: 'imagen-4.0-generate-preview-06-06',
943
+ prompt: 'A cute cat sitting in a garden',
944
+ config: {
945
+ aspectRatio: undefined,
946
+ numberOfImages: 1,
947
+ },
948
+ });
949
+ });
950
+
951
+ describe('Error handling', () => {
952
+ it('should throw InvalidProviderAPIKey error when API key is invalid', async () => {
953
+ // Arrange - Use real Google AI error format
954
+ const message = `[GoogleGenerativeAI Error]: Error fetching from https://generativelanguage.googleapis.com/v1/models/imagen-4.0:generateImages: [400 Bad Request] API key not valid. Please pass a valid API key. [{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"API_KEY_INVALID","domain":"googleapis.com","metadata":{"service":"generativelanguage.googleapis.com"}}]`;
955
+ const apiError = new Error(message);
956
+ vi.spyOn(instance['client'].models, 'generateImages').mockRejectedValue(apiError);
957
+
958
+ const payload: CreateImagePayload = {
959
+ model: 'imagen-4.0-generate-preview-06-06',
960
+ params: {
961
+ prompt: 'A realistic landscape photo',
962
+ },
963
+ };
964
+
965
+ // Act & Assert - Test error type rather than specific text
966
+ await expect(instance.createImage(payload)).rejects.toEqual(
967
+ expect.objectContaining({
968
+ errorType: invalidErrorType,
969
+ provider,
970
+ }),
971
+ );
972
+ });
973
+
974
+ it('should throw ProviderBizError for network and API errors', async () => {
975
+ // Arrange
976
+ const apiError = new Error('Network connection failed');
977
+ vi.spyOn(instance['client'].models, 'generateImages').mockRejectedValue(apiError);
978
+
979
+ const payload: CreateImagePayload = {
980
+ model: 'imagen-4.0-generate-preview-06-06',
981
+ params: {
982
+ prompt: 'A digital art portrait',
983
+ },
984
+ };
985
+
986
+ // Act & Assert - Test error type and basic structure
987
+ await expect(instance.createImage(payload)).rejects.toEqual(
988
+ expect.objectContaining({
989
+ errorType: bizErrorType,
990
+ provider,
991
+ error: expect.objectContaining({
992
+ message: expect.any(String),
993
+ }),
994
+ }),
995
+ );
996
+ });
997
+
998
+ it('should throw error when API response is malformed - missing generatedImages', async () => {
999
+ // Arrange
1000
+ const mockImageResponse = {};
1001
+ vi.spyOn(instance['client'].models, 'generateImages').mockResolvedValue(
1002
+ mockImageResponse as any,
1003
+ );
1004
+
1005
+ const payload: CreateImagePayload = {
1006
+ model: 'imagen-4.0-generate-preview-06-06',
1007
+ params: {
1008
+ prompt: 'Abstract geometric patterns',
1009
+ },
1010
+ };
1011
+
1012
+ // Act & Assert - Test error behavior rather than specific text
1013
+ await expect(instance.createImage(payload)).rejects.toEqual(
1014
+ expect.objectContaining({
1015
+ errorType: bizErrorType,
1016
+ provider,
1017
+ }),
1018
+ );
1019
+ });
1020
+
1021
+ it('should throw error when API response contains empty image array', async () => {
1022
+ // Arrange
1023
+ const mockImageResponse = {
1024
+ generatedImages: [],
1025
+ };
1026
+ vi.spyOn(instance['client'].models, 'generateImages').mockResolvedValue(
1027
+ mockImageResponse as any,
1028
+ );
1029
+
1030
+ const payload: CreateImagePayload = {
1031
+ model: 'imagen-4.0-generate-preview-06-06',
1032
+ params: {
1033
+ prompt: 'Minimalist design poster',
1034
+ },
1035
+ };
1036
+
1037
+ // Act & Assert
1038
+ await expect(instance.createImage(payload)).rejects.toEqual(
1039
+ expect.objectContaining({
1040
+ errorType: bizErrorType,
1041
+ provider,
1042
+ }),
1043
+ );
1044
+ });
1045
+
1046
+ it('should throw error when generated image lacks required data', async () => {
1047
+ // Arrange
1048
+ const mockImageResponse = {
1049
+ generatedImages: [
1050
+ {
1051
+ image: {}, // Missing imageBytes
1052
+ },
1053
+ ],
1054
+ };
1055
+ vi.spyOn(instance['client'].models, 'generateImages').mockResolvedValue(
1056
+ mockImageResponse as any,
1057
+ );
1058
+
1059
+ const payload: CreateImagePayload = {
1060
+ model: 'imagen-4.0-generate-preview-06-06',
1061
+ params: {
1062
+ prompt: 'Watercolor painting style',
1063
+ },
1064
+ };
1065
+
1066
+ // Act & Assert
1067
+ await expect(instance.createImage(payload)).rejects.toEqual(
1068
+ expect.objectContaining({
1069
+ errorType: bizErrorType,
1070
+ provider,
1071
+ }),
1072
+ );
1073
+ });
1074
+ });
1075
+
1076
+ describe('Edge cases', () => {
1077
+ it('should return first image when API returns multiple generated images', async () => {
1078
+ // Arrange - Use two different real base64 image data
1079
+ const firstImageData =
1080
+ 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==';
1081
+ const secondImageData =
1082
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
1083
+ const mockImageResponse = {
1084
+ generatedImages: [
1085
+ {
1086
+ image: {
1087
+ imageBytes: firstImageData,
1088
+ },
1089
+ },
1090
+ {
1091
+ image: {
1092
+ imageBytes: secondImageData,
1093
+ },
1094
+ },
1095
+ ],
1096
+ };
1097
+ vi.spyOn(instance['client'].models, 'generateImages').mockResolvedValue(
1098
+ mockImageResponse as any,
1099
+ );
1100
+
1101
+ const payload: CreateImagePayload = {
1102
+ model: 'imagen-4.0-generate-preview-06-06',
1103
+ params: {
1104
+ prompt: 'Generate multiple variations of a sunset',
1105
+ },
1106
+ };
1107
+
1108
+ // Act
1109
+ const result = await instance.createImage(payload);
1110
+
1111
+ // Assert - Should return the first image
1112
+ expect(result).toEqual({
1113
+ imageUrl: `data:image/png;base64,${firstImageData}`,
1114
+ });
1115
+ });
1116
+
1117
+ it('should work with custom future Imagen model versions', async () => {
1118
+ // Arrange
1119
+ const realBase64Data =
1120
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
1121
+ const mockImageResponse = {
1122
+ generatedImages: [
1123
+ {
1124
+ image: {
1125
+ imageBytes: realBase64Data,
1126
+ },
1127
+ },
1128
+ ],
1129
+ };
1130
+ vi.spyOn(instance['client'].models, 'generateImages').mockResolvedValue(
1131
+ mockImageResponse as any,
1132
+ );
1133
+
1134
+ const payload: CreateImagePayload = {
1135
+ model: 'imagen-5.0-future-model',
1136
+ params: {
1137
+ prompt: 'Photorealistic portrait with soft lighting',
1138
+ aspectRatio: '4:3',
1139
+ },
1140
+ };
1141
+
1142
+ // Act
1143
+ await instance.createImage(payload);
1144
+
1145
+ // Assert
1146
+ expect(instance['client'].models.generateImages).toHaveBeenCalledWith({
1147
+ model: 'imagen-5.0-future-model',
1148
+ prompt: 'Photorealistic portrait with soft lighting',
1149
+ config: {
1150
+ aspectRatio: '4:3',
1151
+ numberOfImages: 1,
1152
+ },
1153
+ });
1154
+ });
1155
+ });
1156
+ });
829
1157
  });
@@ -21,6 +21,7 @@ import {
21
21
  OpenAIChatMessage,
22
22
  UserMessageContentPart,
23
23
  } from '../types';
24
+ import { CreateImagePayload, CreateImageResponse } from '../types/image';
24
25
  import { AgentRuntimeError } from '../utils/createError';
25
26
  import { debugStream } from '../utils/debugStream';
26
27
  import { StreamingResponse } from '../utils/response';
@@ -243,6 +244,52 @@ export class LobeGoogleAI implements LobeRuntimeAI {
243
244
  }
244
245
  }
245
246
 
247
+ /**
248
+ * Generate images using Google AI Imagen API
249
+ * @see https://ai.google.dev/gemini-api/docs/image-generation#imagen
250
+ */
251
+ async createImage(payload: CreateImagePayload): Promise<CreateImageResponse> {
252
+ try {
253
+ const { model, params } = payload;
254
+
255
+ const response = await this.client.models.generateImages({
256
+ config: {
257
+ aspectRatio: params.aspectRatio,
258
+ numberOfImages: 1,
259
+ },
260
+ model,
261
+ prompt: params.prompt,
262
+ });
263
+
264
+ if (!response.generatedImages || response.generatedImages.length === 0) {
265
+ throw new Error('No images generated');
266
+ }
267
+
268
+ const generatedImage = response.generatedImages[0];
269
+ if (!generatedImage.image || !generatedImage.image.imageBytes) {
270
+ throw new Error('Invalid image data');
271
+ }
272
+
273
+ const { imageBytes } = generatedImage.image;
274
+ // 1. official doc use png as example
275
+ // 2. no responseType param support like openai now.
276
+ // I think we can just hard code png now
277
+ const imageUrl = `data:image/png;base64,${imageBytes}`;
278
+
279
+ return { imageUrl };
280
+ } catch (error) {
281
+ const err = error as Error;
282
+ console.error('Google AI image generation error:', err);
283
+
284
+ const { errorType, error: parsedError } = this.parseErrorMessage(err.message);
285
+ throw AgentRuntimeError.createImage({
286
+ error: parsedError,
287
+ errorType,
288
+ provider: this.provider,
289
+ });
290
+ }
291
+ }
292
+
246
293
  private createEnhancedStream(originalStream: any, signal: AbortSignal): ReadableStream {
247
294
  return new ReadableStream({
248
295
  async start(controller) {
@@ -34,25 +34,7 @@ afterEach(() => {
34
34
 
35
35
  describe('LobeGroqAI Temperature Tests', () => {
36
36
  describe('handlePayload option', () => {
37
- it('should set stream to false when payload contains tools', async () => {
38
- const mockCreateMethod = vi
39
- .spyOn(instance['client'].chat.completions, 'create')
40
- .mockResolvedValue({
41
- id: 'chatcmpl-8xDx5AETP8mESQN7UB30GxTN2H1SO',
42
- object: 'chat.completion',
43
- created: 1709125675,
44
- model: 'mistralai/mistral-7b-instruct:free',
45
- system_fingerprint: 'fp_86156a94a0',
46
- choices: [
47
- {
48
- index: 0,
49
- message: { role: 'assistant', content: 'hello', refusal: null },
50
- logprobs: null,
51
- finish_reason: 'stop',
52
- },
53
- ],
54
- });
55
-
37
+ it('should not set stream to false when payload contains tools', async () => {
56
38
  await instance.chat({
57
39
  messages: [{ content: 'Hello', role: 'user' }],
58
40
  model: 'mistralai/mistral-7b-instruct:free',
@@ -65,8 +47,8 @@ describe('LobeGroqAI Temperature Tests', () => {
65
47
  ],
66
48
  });
67
49
 
68
- expect(mockCreateMethod).toHaveBeenCalledWith(
69
- expect.objectContaining({ stream: false }),
50
+ expect(instance['client'].chat.completions.create).toHaveBeenCalledWith(
51
+ expect.objectContaining({ stream: true }),
70
52
  expect.anything(),
71
53
  );
72
54
  });
@@ -21,8 +21,7 @@ export const LobeGroq = createOpenAICompatibleRuntime({
21
21
  const { temperature, ...restPayload } = payload;
22
22
  return {
23
23
  ...restPayload,
24
- // disable stream for tools due to groq dont support
25
- stream: !payload.tools,
24
+ stream: payload.stream ?? true,
26
25
 
27
26
  temperature: temperature <= 0 ? undefined : temperature,
28
27
  } as any;
@@ -16,6 +16,12 @@ export interface ChatCompletionErrorPayload {
16
16
  provider: string;
17
17
  }
18
18
 
19
+ export interface CreateImageErrorPayload {
20
+ error: object;
21
+ errorType: ILobeAgentRuntimeErrorType;
22
+ provider: string;
23
+ }
24
+
19
25
  export interface CreateChatCompletionOptions {
20
26
  chatModel: OpenAI;
21
27
  payload: ChatStreamPayload;
@@ -1,5 +1,9 @@
1
1
  import { ILobeAgentRuntimeErrorType } from '../error';
2
- import { AgentInitErrorPayload, ChatCompletionErrorPayload } from '../types';
2
+ import {
3
+ AgentInitErrorPayload,
4
+ ChatCompletionErrorPayload,
5
+ CreateImageErrorPayload,
6
+ } from '../types';
3
7
 
4
8
  export const AgentRuntimeError = {
5
9
  chat: (error: ChatCompletionErrorPayload): ChatCompletionErrorPayload => error,
@@ -7,5 +11,6 @@ export const AgentRuntimeError = {
7
11
  errorType: ILobeAgentRuntimeErrorType | string | number,
8
12
  error?: any,
9
13
  ): AgentInitErrorPayload => ({ error, errorType }),
14
+ createImage: (error: CreateImageErrorPayload): CreateImageErrorPayload => error,
10
15
  textToImage: (error: any): any => error,
11
16
  };
@@ -39,10 +39,10 @@ export const createAuthSlice: StateCreator<
39
39
  },
40
40
  openLogin: async () => {
41
41
  if (enableClerk) {
42
- const reditectUrl = location.toString();
42
+ const redirectUrl = location.toString();
43
43
  get().clerkSignIn?.({
44
- fallbackRedirectUrl: reditectUrl,
45
- signUpForceRedirectUrl: reditectUrl,
44
+ fallbackRedirectUrl: redirectUrl,
45
+ signUpForceRedirectUrl: redirectUrl,
46
46
  signUpUrl: '/signup',
47
47
  });
48
48