@lobehub/chat 1.99.2 → 1.99.4

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 (110) hide show
  1. package/.cursor/rules/project-introduce.mdc +1 -56
  2. package/.cursor/rules/testing-guide/db-model-test.mdc +453 -0
  3. package/.cursor/rules/testing-guide/electron-ipc-test.mdc +80 -0
  4. package/.cursor/rules/testing-guide/testing-guide.mdc +401 -0
  5. package/CHANGELOG.md +50 -0
  6. package/changelog/v1.json +18 -0
  7. package/docs/usage/providers/ai21.mdx +1 -1
  8. package/docs/usage/providers/ai21.zh-CN.mdx +1 -1
  9. package/docs/usage/providers/ai360.mdx +1 -1
  10. package/docs/usage/providers/ai360.zh-CN.mdx +1 -1
  11. package/docs/usage/providers/anthropic.mdx +1 -1
  12. package/docs/usage/providers/anthropic.zh-CN.mdx +1 -1
  13. package/docs/usage/providers/azure.mdx +1 -1
  14. package/docs/usage/providers/azure.zh-CN.mdx +1 -1
  15. package/docs/usage/providers/baichuan.mdx +1 -1
  16. package/docs/usage/providers/baichuan.zh-CN.mdx +1 -1
  17. package/docs/usage/providers/bedrock.mdx +1 -1
  18. package/docs/usage/providers/bedrock.zh-CN.mdx +1 -1
  19. package/docs/usage/providers/cloudflare.mdx +1 -1
  20. package/docs/usage/providers/cloudflare.zh-CN.mdx +1 -1
  21. package/docs/usage/providers/deepseek.mdx +1 -1
  22. package/docs/usage/providers/deepseek.zh-CN.mdx +1 -1
  23. package/docs/usage/providers/fal.mdx +69 -0
  24. package/docs/usage/providers/fal.zh-CN.mdx +68 -0
  25. package/docs/usage/providers/fireworksai.mdx +1 -1
  26. package/docs/usage/providers/fireworksai.zh-CN.mdx +1 -1
  27. package/docs/usage/providers/giteeai.mdx +1 -1
  28. package/docs/usage/providers/giteeai.zh-CN.mdx +1 -1
  29. package/docs/usage/providers/github.mdx +1 -1
  30. package/docs/usage/providers/github.zh-CN.mdx +1 -1
  31. package/docs/usage/providers/google.mdx +1 -1
  32. package/docs/usage/providers/google.zh-CN.mdx +1 -1
  33. package/docs/usage/providers/groq.mdx +1 -1
  34. package/docs/usage/providers/groq.zh-CN.mdx +1 -1
  35. package/docs/usage/providers/hunyuan.mdx +1 -1
  36. package/docs/usage/providers/hunyuan.zh-CN.mdx +1 -1
  37. package/docs/usage/providers/internlm.mdx +1 -1
  38. package/docs/usage/providers/internlm.zh-CN.mdx +1 -1
  39. package/docs/usage/providers/jina.mdx +1 -1
  40. package/docs/usage/providers/jina.zh-CN.mdx +1 -1
  41. package/docs/usage/providers/minimax.mdx +1 -1
  42. package/docs/usage/providers/minimax.zh-CN.mdx +1 -1
  43. package/docs/usage/providers/mistral.mdx +1 -1
  44. package/docs/usage/providers/mistral.zh-CN.mdx +1 -1
  45. package/docs/usage/providers/moonshot.mdx +1 -1
  46. package/docs/usage/providers/moonshot.zh-CN.mdx +1 -1
  47. package/docs/usage/providers/novita.mdx +1 -1
  48. package/docs/usage/providers/novita.zh-CN.mdx +1 -1
  49. package/docs/usage/providers/ollama.mdx +1 -1
  50. package/docs/usage/providers/ollama.zh-CN.mdx +1 -1
  51. package/docs/usage/providers/openai.mdx +4 -4
  52. package/docs/usage/providers/openai.zh-CN.mdx +4 -4
  53. package/docs/usage/providers/openrouter.mdx +1 -1
  54. package/docs/usage/providers/openrouter.zh-CN.mdx +1 -1
  55. package/docs/usage/providers/perplexity.mdx +1 -1
  56. package/docs/usage/providers/perplexity.zh-CN.mdx +1 -1
  57. package/docs/usage/providers/ppio.mdx +1 -1
  58. package/docs/usage/providers/ppio.zh-CN.mdx +1 -1
  59. package/docs/usage/providers/qiniu.mdx +1 -1
  60. package/docs/usage/providers/qiniu.zh-CN.mdx +1 -1
  61. package/docs/usage/providers/qwen.mdx +1 -1
  62. package/docs/usage/providers/qwen.zh-CN.mdx +1 -1
  63. package/docs/usage/providers/sambanova.mdx +1 -1
  64. package/docs/usage/providers/sambanova.zh-CN.mdx +1 -1
  65. package/docs/usage/providers/sensenova.mdx +1 -1
  66. package/docs/usage/providers/sensenova.zh-CN.mdx +1 -1
  67. package/docs/usage/providers/siliconcloud.mdx +1 -1
  68. package/docs/usage/providers/siliconcloud.zh-CN.mdx +1 -1
  69. package/docs/usage/providers/spark.mdx +1 -1
  70. package/docs/usage/providers/spark.zh-CN.mdx +1 -1
  71. package/docs/usage/providers/stepfun.mdx +1 -1
  72. package/docs/usage/providers/stepfun.zh-CN.mdx +1 -1
  73. package/docs/usage/providers/taichu.mdx +1 -1
  74. package/docs/usage/providers/taichu.zh-CN.mdx +1 -1
  75. package/docs/usage/providers/togetherai.mdx +1 -1
  76. package/docs/usage/providers/togetherai.zh-CN.mdx +1 -1
  77. package/docs/usage/providers/upstage.mdx +1 -1
  78. package/docs/usage/providers/upstage.zh-CN.mdx +1 -1
  79. package/docs/usage/providers/vllm.mdx +1 -1
  80. package/docs/usage/providers/vllm.zh-CN.mdx +1 -1
  81. package/docs/usage/providers/wenxin.mdx +1 -1
  82. package/docs/usage/providers/wenxin.zh-CN.mdx +1 -1
  83. package/docs/usage/providers/xai.mdx +1 -1
  84. package/docs/usage/providers/xai.zh-CN.mdx +1 -1
  85. package/docs/usage/providers/zeroone.mdx +1 -1
  86. package/docs/usage/providers/zeroone.zh-CN.mdx +1 -1
  87. package/docs/usage/providers/zhipu.mdx +1 -1
  88. package/docs/usage/providers/zhipu.zh-CN.mdx +1 -1
  89. package/package.json +2 -2
  90. package/src/config/aiModels/openai.ts +24 -9
  91. package/src/libs/model-runtime/BaseAI.ts +1 -0
  92. package/src/libs/model-runtime/ModelRuntime.ts +0 -1
  93. package/src/libs/model-runtime/hunyuan/index.ts +4 -6
  94. package/src/libs/model-runtime/novita/__snapshots__/index.test.ts.snap +18 -0
  95. package/src/libs/model-runtime/openai/__snapshots__/index.test.ts.snap +28 -0
  96. package/src/libs/model-runtime/openai/index.test.ts +1 -338
  97. package/src/libs/model-runtime/openai/index.ts +0 -127
  98. package/src/libs/model-runtime/openrouter/__snapshots__/index.test.ts.snap +3 -0
  99. package/src/libs/model-runtime/ppio/__snapshots__/index.test.ts.snap +2 -0
  100. package/src/libs/model-runtime/utils/modelParse.ts +1 -0
  101. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.test.ts +364 -12
  102. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +145 -43
  103. package/src/libs/model-runtime/utils/openaiHelpers.test.ts +151 -0
  104. package/src/libs/model-runtime/utils/openaiHelpers.ts +26 -1
  105. package/src/libs/model-runtime/xai/index.ts +1 -4
  106. package/src/store/aiInfra/slices/aiModel/action.ts +1 -1
  107. package/src/store/aiInfra/slices/aiProvider/action.ts +5 -2
  108. package/src/types/aiModel.ts +1 -0
  109. package/src/types/llm.ts +3 -1
  110. package/.cursor/rules/testing-guide.mdc +0 -881
@@ -1,9 +1,4 @@
1
- import debug from 'debug';
2
- import { toFile } from 'openai';
3
- import { FileLike } from 'openai/uploads';
4
-
5
1
  import { responsesAPIModels } from '@/const/models';
6
- import { RuntimeImageGenParamsValue } from '@/libs/standard-parameters/meta-schema';
7
2
 
8
3
  import { ChatStreamPayload, ModelProvider } from '../types';
9
4
  import { processMultiProviderModelList } from '../utils/modelParse';
@@ -15,45 +10,8 @@ export interface OpenAIModelCard {
15
10
  }
16
11
 
17
12
  const prunePrefixes = ['o1', 'o3', 'o4', 'codex', 'computer-use'];
18
-
19
13
  const oaiSearchContextSize = process.env.OPENAI_SEARCH_CONTEXT_SIZE; // low, medium, high
20
14
 
21
- const log = debug('lobe-image:openai');
22
-
23
- /**
24
- * 将图片 URL 转换为 File 对象
25
- * @param imageUrl - 图片 URL(可以是 HTTP URL 或 base64 data URL)
26
- * @returns FileLike 对象
27
- */
28
- export const convertImageUrlToFile = async (imageUrl: string): Promise<FileLike> => {
29
- log('Converting image URL to File: %s', imageUrl.startsWith('data:') ? 'base64 data' : imageUrl);
30
-
31
- let buffer: Buffer;
32
- let mimeType: string;
33
-
34
- if (imageUrl.startsWith('data:')) {
35
- // 处理 base64 data URL
36
- log('Processing base64 image data');
37
- const [mimeTypePart, base64Data] = imageUrl.split(',');
38
- mimeType = mimeTypePart.split(':')[1].split(';')[0];
39
- buffer = Buffer.from(base64Data, 'base64');
40
- } else {
41
- // 处理 HTTP URL
42
- log('Fetching image from URL: %s', imageUrl);
43
- const response = await fetch(imageUrl);
44
- if (!response.ok) {
45
- throw new Error(`Failed to fetch image from ${imageUrl}: ${response.statusText}`);
46
- }
47
- buffer = Buffer.from(await response.arrayBuffer());
48
- mimeType = response.headers.get('content-type') || 'image/png';
49
- }
50
-
51
- log('Successfully converted image to buffer, size: %s, mimeType: %s', buffer.length, mimeType);
52
-
53
- // 使用 OpenAI 的 toFile 方法创建 File 对象
54
- return toFile(buffer, `image.${mimeType.split('/')[1]}`, { type: mimeType });
55
- };
56
-
57
15
  export const LobeOpenAI = createOpenAICompatibleRuntime({
58
16
  baseURL: 'https://api.openai.com/v1',
59
17
  chatCompletion: {
@@ -88,91 +46,6 @@ export const LobeOpenAI = createOpenAICompatibleRuntime({
88
46
  return { ...rest, model, stream: payload.stream ?? true };
89
47
  },
90
48
  },
91
- createImage: async (payload) => {
92
- const { model, params, client } = payload;
93
- log('Creating image with model: %s and params: %O', model, params);
94
-
95
- const defaultInput = {
96
- n: 1,
97
- };
98
-
99
- // 映射参数名称,将 imageUrls 映射为 image
100
- const paramsMap = new Map<RuntimeImageGenParamsValue, string>([['imageUrls', 'image']]);
101
- const userInput: Record<string, any> = Object.fromEntries(
102
- Object.entries(params).map(([key, value]) => [
103
- paramsMap.get(key as RuntimeImageGenParamsValue) ?? key,
104
- value,
105
- ]),
106
- );
107
-
108
- const isImageEdit = Array.isArray(userInput.image) && userInput.image.length > 0;
109
- // 如果有 imageUrls 参数,将其转换为 File 对象
110
- if (isImageEdit) {
111
- log('Converting imageUrls to File objects: %O', userInput.image);
112
- try {
113
- // 转换所有图片 URL 为 File 对象
114
- const imageFiles = await Promise.all(
115
- userInput.image.map((url: string) => convertImageUrlToFile(url)),
116
- );
117
-
118
- log('Successfully converted %d images to File objects', imageFiles.length);
119
-
120
- // 根据官方文档,如果有多个图片,传递数组;如果只有一个,传递单个 File
121
- userInput.image = imageFiles.length === 1 ? imageFiles[0] : imageFiles;
122
- } catch (error) {
123
- log('Error converting imageUrls to File objects: %O', error);
124
- throw new Error(`Failed to convert image URLs to File objects: ${error}`);
125
- }
126
- } else {
127
- delete userInput.image;
128
- }
129
-
130
- if (userInput.size === 'auto') {
131
- delete userInput.size;
132
- }
133
-
134
- const options = {
135
- model,
136
- ...defaultInput,
137
- ...(userInput as any),
138
- };
139
-
140
- log('options: %O', options);
141
-
142
- // 判断是否为图片编辑操作
143
- const img = isImageEdit
144
- ? await client.images.edit(options)
145
- : await client.images.generate(options);
146
-
147
- // 检查响应数据的完整性
148
- if (!img || !img.data || !Array.isArray(img.data) || img.data.length === 0) {
149
- log('Invalid image response: missing data array');
150
- throw new Error('Invalid image response: missing or empty data array');
151
- }
152
-
153
- const imageData = img.data[0];
154
- if (!imageData) {
155
- log('Invalid image response: first data item is null/undefined');
156
- throw new Error('Invalid image response: first data item is null or undefined');
157
- }
158
-
159
- if (!imageData.b64_json) {
160
- log('Invalid image response: missing b64_json field');
161
- throw new Error('Invalid image response: missing b64_json field');
162
- }
163
-
164
- // 确定图片的 MIME 类型,默认为 PNG
165
- const mimeType = 'image/png'; // OpenAI 图片生成默认返回 PNG 格式
166
-
167
- // 将 base64 字符串转换为完整的 data URL
168
- const dataUrl = `data:${mimeType};base64,${imageData.b64_json}`;
169
-
170
- log('Successfully converted base64 to data URL, length: %d', dataUrl.length);
171
-
172
- return {
173
- imageUrl: dataUrl,
174
- };
175
- },
176
49
  debug: {
177
50
  chatCompletion: () => process.env.DEBUG_OPENAI_CHAT_COMPLETION === '1',
178
51
  responses: () => process.env.DEBUG_OPENAI_RESPONSES === '1',
@@ -21,6 +21,7 @@ _These are free, rate-limited endpoints for [Reflection 70B](/models/mattshumer/
21
21
  },
22
22
  "reasoning": true,
23
23
  "releasedAt": "2024-09-06",
24
+ "type": "chat",
24
25
  "vision": false,
25
26
  },
26
27
  ]
@@ -47,6 +48,7 @@ _These are free, rate-limited endpoints for [Reflection 70B](/models/mattshumer/
47
48
  },
48
49
  "reasoning": false,
49
50
  "releasedAt": "2024-09-06",
51
+ "type": "chat",
50
52
  "vision": false,
51
53
  },
52
54
  ]
@@ -73,6 +75,7 @@ _These are free, rate-limited endpoints for [Reflection 70B](/models/mattshumer/
73
75
  },
74
76
  "reasoning": false,
75
77
  "releasedAt": "2024-09-06",
78
+ "type": "chat",
76
79
  "vision": false,
77
80
  },
78
81
  ]
@@ -10,6 +10,7 @@ exports[`PPIO > models > should get models 1`] = `
10
10
  "functionCall": false,
11
11
  "id": "deepseek/deepseek-r1/community",
12
12
  "reasoning": true,
13
+ "type": "chat",
13
14
  "vision": false,
14
15
  },
15
16
  {
@@ -20,6 +21,7 @@ exports[`PPIO > models > should get models 1`] = `
20
21
  "functionCall": false,
21
22
  "id": "deepseek/deepseek-v3/community",
22
23
  "reasoning": false,
24
+ "type": "chat",
23
25
  "vision": false,
24
26
  },
25
27
  ]
@@ -136,6 +136,7 @@ const processModelCard = (
136
136
  reasoningKeywords.some((keyword) => model.id.toLowerCase().includes(keyword)) ||
137
137
  knownModel?.abilities?.reasoning ||
138
138
  false,
139
+ type: model.type || knownModel?.type || 'chat',
139
140
  vision:
140
141
  (visionKeywords.some((keyword) => model.id.toLowerCase().includes(keyword)) &&
141
142
  !isExcludedModel) ||
@@ -14,6 +14,7 @@ import officalOpenAIModels from '@/libs/model-runtime/openai/fixtures/openai-mod
14
14
  import { sleep } from '@/utils/sleep';
15
15
 
16
16
  import * as debugStreamModule from '../debugStream';
17
+ import * as openaiHelpers from '../openaiHelpers';
17
18
  import { createOpenAICompatibleRuntime } from './index';
18
19
 
19
20
  const provider = 'groq';
@@ -978,6 +979,329 @@ describe('LobeOpenAICompatibleFactory', () => {
978
979
  });
979
980
  });
980
981
 
982
+ describe('createImage', () => {
983
+ beforeEach(() => {
984
+ // Mock convertImageUrlToFile since it's already tested in openaiHelpers.test.ts
985
+ vi.spyOn(openaiHelpers, 'convertImageUrlToFile').mockResolvedValue(
986
+ new File(['mock-file-content'], 'test-image.jpg', { type: 'image/jpeg' }),
987
+ );
988
+ });
989
+
990
+ describe('basic image generation', () => {
991
+ it('should generate image successfully without imageUrls', async () => {
992
+ const mockResponse = {
993
+ data: [
994
+ {
995
+ b64_json:
996
+ 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
997
+ },
998
+ ],
999
+ };
1000
+
1001
+ vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockResponse as any);
1002
+
1003
+ const payload = {
1004
+ model: 'dall-e-3',
1005
+ params: {
1006
+ prompt: 'A beautiful sunset',
1007
+ size: '1024x1024',
1008
+ quality: 'standard',
1009
+ },
1010
+ };
1011
+
1012
+ const result = await (instance as any).createImage(payload);
1013
+
1014
+ expect(instance['client'].images.generate).toHaveBeenCalledWith({
1015
+ model: 'dall-e-3',
1016
+ n: 1,
1017
+ prompt: 'A beautiful sunset',
1018
+ size: '1024x1024',
1019
+ quality: 'standard',
1020
+ response_format: 'b64_json',
1021
+ });
1022
+
1023
+ expect(result).toEqual({
1024
+ imageUrl:
1025
+ 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
1026
+ });
1027
+ });
1028
+
1029
+ it('should handle size auto parameter correctly', async () => {
1030
+ const mockResponse = {
1031
+ data: [{ b64_json: 'mock-base64-data' }],
1032
+ };
1033
+
1034
+ vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockResponse as any);
1035
+
1036
+ const payload = {
1037
+ model: 'dall-e-3',
1038
+ params: {
1039
+ prompt: 'A beautiful sunset',
1040
+ size: 'auto',
1041
+ },
1042
+ };
1043
+
1044
+ await (instance as any).createImage(payload);
1045
+
1046
+ // size: 'auto' should be removed from the options
1047
+ expect(instance['client'].images.generate).toHaveBeenCalledWith({
1048
+ model: 'dall-e-3',
1049
+ n: 1,
1050
+ prompt: 'A beautiful sunset',
1051
+ response_format: 'b64_json',
1052
+ });
1053
+ });
1054
+
1055
+ it('should not add response_format parameter for gpt-image-1 model', async () => {
1056
+ const mockResponse = {
1057
+ data: [{ b64_json: 'gpt-image-1-base64-data' }],
1058
+ };
1059
+
1060
+ vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockResponse as any);
1061
+
1062
+ const payload = {
1063
+ model: 'gpt-image-1',
1064
+ params: {
1065
+ prompt: 'A modern digital artwork',
1066
+ size: '1024x1024',
1067
+ },
1068
+ };
1069
+
1070
+ const result = await (instance as any).createImage(payload);
1071
+
1072
+ // gpt-image-1 model should not include response_format parameter
1073
+ expect(instance['client'].images.generate).toHaveBeenCalledWith({
1074
+ model: 'gpt-image-1',
1075
+ n: 1,
1076
+ prompt: 'A modern digital artwork',
1077
+ size: '1024x1024',
1078
+ });
1079
+
1080
+ expect(result).toEqual({
1081
+ imageUrl: 'data:image/png;base64,gpt-image-1-base64-data',
1082
+ });
1083
+ });
1084
+ });
1085
+
1086
+ describe('image editing', () => {
1087
+ it('should edit image with single imageUrl', async () => {
1088
+ const mockResponse = {
1089
+ data: [{ b64_json: 'edited-image-base64' }],
1090
+ };
1091
+
1092
+ vi.spyOn(instance['client'].images, 'edit').mockResolvedValue(mockResponse as any);
1093
+
1094
+ const payload = {
1095
+ model: 'dall-e-2',
1096
+ params: {
1097
+ prompt: 'Add a rainbow to this image',
1098
+ imageUrls: ['https://example.com/image1.jpg'],
1099
+ mask: 'https://example.com/mask.jpg',
1100
+ },
1101
+ };
1102
+
1103
+ const result = await (instance as any).createImage(payload);
1104
+
1105
+ expect(openaiHelpers.convertImageUrlToFile).toHaveBeenCalledWith(
1106
+ 'https://example.com/image1.jpg',
1107
+ );
1108
+ expect(instance['client'].images.edit).toHaveBeenCalledWith({
1109
+ model: 'dall-e-2',
1110
+ n: 1,
1111
+ prompt: 'Add a rainbow to this image',
1112
+ image: expect.any(File),
1113
+ mask: 'https://example.com/mask.jpg',
1114
+ response_format: 'b64_json',
1115
+ });
1116
+
1117
+ expect(result).toEqual({
1118
+ imageUrl: 'data:image/png;base64,edited-image-base64',
1119
+ });
1120
+ });
1121
+
1122
+ it('should edit image with multiple imageUrls', async () => {
1123
+ const mockResponse = {
1124
+ data: [{ b64_json: 'edited-multiple-images-base64' }],
1125
+ };
1126
+
1127
+ const mockFile1 = new File(['content1'], 'image1.jpg', { type: 'image/jpeg' });
1128
+ const mockFile2 = new File(['content2'], 'image2.jpg', { type: 'image/jpeg' });
1129
+
1130
+ vi.mocked(openaiHelpers.convertImageUrlToFile)
1131
+ .mockResolvedValueOnce(mockFile1)
1132
+ .mockResolvedValueOnce(mockFile2);
1133
+
1134
+ vi.spyOn(instance['client'].images, 'edit').mockResolvedValue(mockResponse as any);
1135
+
1136
+ const payload = {
1137
+ model: 'dall-e-2',
1138
+ params: {
1139
+ prompt: 'Merge these images',
1140
+ imageUrls: ['https://example.com/image1.jpg', 'https://example.com/image2.jpg'],
1141
+ },
1142
+ };
1143
+
1144
+ const result = await (instance as any).createImage(payload);
1145
+
1146
+ expect(openaiHelpers.convertImageUrlToFile).toHaveBeenCalledTimes(2);
1147
+ expect(openaiHelpers.convertImageUrlToFile).toHaveBeenCalledWith(
1148
+ 'https://example.com/image1.jpg',
1149
+ );
1150
+ expect(openaiHelpers.convertImageUrlToFile).toHaveBeenCalledWith(
1151
+ 'https://example.com/image2.jpg',
1152
+ );
1153
+
1154
+ expect(instance['client'].images.edit).toHaveBeenCalledWith({
1155
+ model: 'dall-e-2',
1156
+ n: 1,
1157
+ prompt: 'Merge these images',
1158
+ image: [mockFile1, mockFile2],
1159
+ response_format: 'b64_json',
1160
+ });
1161
+
1162
+ expect(result).toEqual({
1163
+ imageUrl: 'data:image/png;base64,edited-multiple-images-base64',
1164
+ });
1165
+ });
1166
+
1167
+ it('should handle convertImageUrlToFile error', async () => {
1168
+ vi.mocked(openaiHelpers.convertImageUrlToFile).mockRejectedValue(
1169
+ new Error('Failed to download image'),
1170
+ );
1171
+
1172
+ const payload = {
1173
+ model: 'dall-e-2',
1174
+ params: {
1175
+ prompt: 'Edit this image',
1176
+ imageUrls: ['https://invalid-url.com/image.jpg'],
1177
+ },
1178
+ };
1179
+
1180
+ await expect((instance as any).createImage(payload)).rejects.toThrow(
1181
+ 'Failed to convert image URLs to File objects: Error: Failed to download image',
1182
+ );
1183
+ });
1184
+ });
1185
+
1186
+ describe('error handling', () => {
1187
+ it('should throw error when API response is invalid - no data', async () => {
1188
+ vi.spyOn(instance['client'].images, 'generate').mockResolvedValue({} as any);
1189
+
1190
+ const payload = {
1191
+ model: 'dall-e-3',
1192
+ params: { prompt: 'Test prompt' },
1193
+ };
1194
+
1195
+ await expect((instance as any).createImage(payload)).rejects.toThrow(
1196
+ 'Invalid image response: missing or empty data array',
1197
+ );
1198
+ });
1199
+
1200
+ it('should throw error when API response is invalid - empty data array', async () => {
1201
+ vi.spyOn(instance['client'].images, 'generate').mockResolvedValue({
1202
+ data: [],
1203
+ } as any);
1204
+
1205
+ const payload = {
1206
+ model: 'dall-e-3',
1207
+ params: { prompt: 'Test prompt' },
1208
+ };
1209
+
1210
+ await expect((instance as any).createImage(payload)).rejects.toThrow(
1211
+ 'Invalid image response: missing or empty data array',
1212
+ );
1213
+ });
1214
+
1215
+ it('should throw error when first data item is null', async () => {
1216
+ vi.spyOn(instance['client'].images, 'generate').mockResolvedValue({
1217
+ data: [null],
1218
+ } as any);
1219
+
1220
+ const payload = {
1221
+ model: 'dall-e-3',
1222
+ params: { prompt: 'Test prompt' },
1223
+ };
1224
+
1225
+ await expect((instance as any).createImage(payload)).rejects.toThrow(
1226
+ 'Invalid image response: first data item is null or undefined',
1227
+ );
1228
+ });
1229
+
1230
+ it('should throw error when b64_json is missing', async () => {
1231
+ vi.spyOn(instance['client'].images, 'generate').mockResolvedValue({
1232
+ data: [{ url: 'https://example.com/image.jpg' }],
1233
+ } as any);
1234
+
1235
+ const payload = {
1236
+ model: 'dall-e-3',
1237
+ params: { prompt: 'Test prompt' },
1238
+ };
1239
+
1240
+ await expect((instance as any).createImage(payload)).rejects.toThrow(
1241
+ 'Invalid image response: missing b64_json field',
1242
+ );
1243
+ });
1244
+ });
1245
+
1246
+ describe('parameter mapping', () => {
1247
+ it('should map imageUrls parameter to image', async () => {
1248
+ const mockResponse = {
1249
+ data: [{ b64_json: 'test-base64' }],
1250
+ };
1251
+
1252
+ vi.spyOn(instance['client'].images, 'edit').mockResolvedValue(mockResponse as any);
1253
+
1254
+ const payload = {
1255
+ model: 'dall-e-2',
1256
+ params: {
1257
+ prompt: 'Test prompt',
1258
+ imageUrls: ['https://example.com/image.jpg'],
1259
+ customParam: 'should remain unchanged',
1260
+ },
1261
+ };
1262
+
1263
+ await (instance as any).createImage(payload);
1264
+
1265
+ expect(instance['client'].images.edit).toHaveBeenCalledWith({
1266
+ model: 'dall-e-2',
1267
+ n: 1,
1268
+ prompt: 'Test prompt',
1269
+ image: expect.any(File),
1270
+ customParam: 'should remain unchanged',
1271
+ response_format: 'b64_json',
1272
+ });
1273
+ });
1274
+
1275
+ it('should handle parameters without imageUrls', async () => {
1276
+ const mockResponse = {
1277
+ data: [{ b64_json: 'test-base64' }],
1278
+ };
1279
+
1280
+ vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockResponse as any);
1281
+
1282
+ const payload = {
1283
+ model: 'dall-e-3',
1284
+ params: {
1285
+ prompt: 'Test prompt',
1286
+ quality: 'hd',
1287
+ style: 'vivid',
1288
+ },
1289
+ };
1290
+
1291
+ await (instance as any).createImage(payload);
1292
+
1293
+ expect(instance['client'].images.generate).toHaveBeenCalledWith({
1294
+ model: 'dall-e-3',
1295
+ n: 1,
1296
+ prompt: 'Test prompt',
1297
+ quality: 'hd',
1298
+ style: 'vivid',
1299
+ response_format: 'b64_json',
1300
+ });
1301
+ });
1302
+ });
1303
+ });
1304
+
981
1305
  describe('models', () => {
982
1306
  it('should get models with third party model list', async () => {
983
1307
  vi.spyOn(instance['client'].models, 'list').mockResolvedValue({
@@ -993,54 +1317,82 @@ describe('LobeOpenAICompatibleFactory', () => {
993
1317
 
994
1318
  expect(list).toEqual([
995
1319
  {
1320
+ abilities: {
1321
+ functionCall: true,
1322
+ vision: true,
1323
+ },
1324
+ config: {
1325
+ deploymentName: 'gpt-4o',
1326
+ },
996
1327
  contextWindowTokens: 128000,
997
- releasedAt: '2023-10-25',
998
1328
  description:
999
1329
  'ChatGPT-4o 是一款动态模型,实时更新以保持当前最新版本。它结合了强大的语言理解与生成能力,适合于大规模应用场景,包括客户服务、教育和技术支持。',
1000
1330
  displayName: 'GPT-4o',
1001
1331
  enabled: true,
1002
- functionCall: true,
1003
1332
  id: 'gpt-4o',
1333
+ maxOutput: 4096,
1004
1334
  pricing: {
1335
+ cachedInput: 1.25,
1005
1336
  input: 2.5,
1006
1337
  output: 10,
1007
1338
  },
1008
- vision: true,
1339
+ providerId: 'azure',
1340
+ releasedAt: '2024-05-13',
1341
+ source: 'builtin',
1342
+ type: 'chat',
1009
1343
  },
1010
1344
  {
1345
+ abilities: {
1346
+ functionCall: true,
1347
+ vision: true,
1348
+ },
1011
1349
  contextWindowTokens: 200000,
1012
1350
  description:
1013
1351
  'Claude 3 Haiku 是 Anthropic 的最快且最紧凑的模型,旨在实现近乎即时的响应。它具有快速且准确的定向性能。',
1014
1352
  displayName: 'Claude 3 Haiku',
1015
- functionCall: true,
1353
+ enabled: false,
1016
1354
  id: 'claude-3-haiku-20240307',
1017
1355
  maxOutput: 4096,
1018
1356
  pricing: {
1019
1357
  input: 0.25,
1020
1358
  output: 1.25,
1021
1359
  },
1360
+ providerId: 'anthropic',
1022
1361
  releasedAt: '2024-03-07',
1023
- vision: true,
1362
+ settings: {
1363
+ extendParams: ['disableContextCaching'],
1364
+ },
1365
+ source: 'builtin',
1366
+ type: 'chat',
1024
1367
  },
1025
1368
  {
1369
+ abilities: {
1370
+ functionCall: true,
1371
+ vision: true,
1372
+ },
1373
+ config: {
1374
+ deploymentName: 'gpt-4o-mini',
1375
+ },
1026
1376
  contextWindowTokens: 128000,
1027
- description:
1028
- 'GPT-4o mini是OpenAI在GPT-4 Omni之后推出的最新模型,支持图文输入并输出文本。作为他们最先进的小型模型,它比其他近期的前沿模型便宜很多,并且比GPT-3.5 Turbo便宜超过60%。它保持了最先进的智能,同时具有显著的性价比。GPT-4o mini在MMLU测试中获得了 82% 的得分,目前在聊天偏好上排名高于 GPT-4。',
1029
- displayName: 'GPT-4o mini',
1030
- enabled: true,
1031
- functionCall: true,
1377
+ description: 'GPT-4o Mini,小型高效模型,具备与GPT-4o相似的卓越性能。',
1378
+ displayName: 'GPT 4o Mini',
1379
+ enabled: false,
1032
1380
  id: 'gpt-4o-mini',
1033
- maxOutput: 16385,
1381
+ maxOutput: 4096,
1034
1382
  pricing: {
1383
+ cachedInput: 0.075,
1035
1384
  input: 0.15,
1036
1385
  output: 0.6,
1037
1386
  },
1387
+ providerId: 'azure',
1038
1388
  releasedAt: '2023-10-26',
1039
- vision: true,
1389
+ source: 'builtin',
1390
+ type: 'chat',
1040
1391
  },
1041
1392
  {
1042
1393
  id: 'gemini',
1043
1394
  releasedAt: '2025-01-10',
1395
+ type: undefined,
1044
1396
  },
1045
1397
  ]);
1046
1398
  });