@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.
- package/.cursor/rules/project-introduce.mdc +1 -56
- package/.cursor/rules/testing-guide/db-model-test.mdc +453 -0
- package/.cursor/rules/testing-guide/electron-ipc-test.mdc +80 -0
- package/.cursor/rules/testing-guide/testing-guide.mdc +401 -0
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/docs/usage/providers/ai21.mdx +1 -1
- package/docs/usage/providers/ai21.zh-CN.mdx +1 -1
- package/docs/usage/providers/ai360.mdx +1 -1
- package/docs/usage/providers/ai360.zh-CN.mdx +1 -1
- package/docs/usage/providers/anthropic.mdx +1 -1
- package/docs/usage/providers/anthropic.zh-CN.mdx +1 -1
- package/docs/usage/providers/azure.mdx +1 -1
- package/docs/usage/providers/azure.zh-CN.mdx +1 -1
- package/docs/usage/providers/baichuan.mdx +1 -1
- package/docs/usage/providers/baichuan.zh-CN.mdx +1 -1
- package/docs/usage/providers/bedrock.mdx +1 -1
- package/docs/usage/providers/bedrock.zh-CN.mdx +1 -1
- package/docs/usage/providers/cloudflare.mdx +1 -1
- package/docs/usage/providers/cloudflare.zh-CN.mdx +1 -1
- package/docs/usage/providers/deepseek.mdx +1 -1
- package/docs/usage/providers/deepseek.zh-CN.mdx +1 -1
- package/docs/usage/providers/fal.mdx +69 -0
- package/docs/usage/providers/fal.zh-CN.mdx +68 -0
- package/docs/usage/providers/fireworksai.mdx +1 -1
- package/docs/usage/providers/fireworksai.zh-CN.mdx +1 -1
- package/docs/usage/providers/giteeai.mdx +1 -1
- package/docs/usage/providers/giteeai.zh-CN.mdx +1 -1
- package/docs/usage/providers/github.mdx +1 -1
- package/docs/usage/providers/github.zh-CN.mdx +1 -1
- package/docs/usage/providers/google.mdx +1 -1
- package/docs/usage/providers/google.zh-CN.mdx +1 -1
- package/docs/usage/providers/groq.mdx +1 -1
- package/docs/usage/providers/groq.zh-CN.mdx +1 -1
- package/docs/usage/providers/hunyuan.mdx +1 -1
- package/docs/usage/providers/hunyuan.zh-CN.mdx +1 -1
- package/docs/usage/providers/internlm.mdx +1 -1
- package/docs/usage/providers/internlm.zh-CN.mdx +1 -1
- package/docs/usage/providers/jina.mdx +1 -1
- package/docs/usage/providers/jina.zh-CN.mdx +1 -1
- package/docs/usage/providers/minimax.mdx +1 -1
- package/docs/usage/providers/minimax.zh-CN.mdx +1 -1
- package/docs/usage/providers/mistral.mdx +1 -1
- package/docs/usage/providers/mistral.zh-CN.mdx +1 -1
- package/docs/usage/providers/moonshot.mdx +1 -1
- package/docs/usage/providers/moonshot.zh-CN.mdx +1 -1
- package/docs/usage/providers/novita.mdx +1 -1
- package/docs/usage/providers/novita.zh-CN.mdx +1 -1
- package/docs/usage/providers/ollama.mdx +1 -1
- package/docs/usage/providers/ollama.zh-CN.mdx +1 -1
- package/docs/usage/providers/openai.mdx +4 -4
- package/docs/usage/providers/openai.zh-CN.mdx +4 -4
- package/docs/usage/providers/openrouter.mdx +1 -1
- package/docs/usage/providers/openrouter.zh-CN.mdx +1 -1
- package/docs/usage/providers/perplexity.mdx +1 -1
- package/docs/usage/providers/perplexity.zh-CN.mdx +1 -1
- package/docs/usage/providers/ppio.mdx +1 -1
- package/docs/usage/providers/ppio.zh-CN.mdx +1 -1
- package/docs/usage/providers/qiniu.mdx +1 -1
- package/docs/usage/providers/qiniu.zh-CN.mdx +1 -1
- package/docs/usage/providers/qwen.mdx +1 -1
- package/docs/usage/providers/qwen.zh-CN.mdx +1 -1
- package/docs/usage/providers/sambanova.mdx +1 -1
- package/docs/usage/providers/sambanova.zh-CN.mdx +1 -1
- package/docs/usage/providers/sensenova.mdx +1 -1
- package/docs/usage/providers/sensenova.zh-CN.mdx +1 -1
- package/docs/usage/providers/siliconcloud.mdx +1 -1
- package/docs/usage/providers/siliconcloud.zh-CN.mdx +1 -1
- package/docs/usage/providers/spark.mdx +1 -1
- package/docs/usage/providers/spark.zh-CN.mdx +1 -1
- package/docs/usage/providers/stepfun.mdx +1 -1
- package/docs/usage/providers/stepfun.zh-CN.mdx +1 -1
- package/docs/usage/providers/taichu.mdx +1 -1
- package/docs/usage/providers/taichu.zh-CN.mdx +1 -1
- package/docs/usage/providers/togetherai.mdx +1 -1
- package/docs/usage/providers/togetherai.zh-CN.mdx +1 -1
- package/docs/usage/providers/upstage.mdx +1 -1
- package/docs/usage/providers/upstage.zh-CN.mdx +1 -1
- package/docs/usage/providers/vllm.mdx +1 -1
- package/docs/usage/providers/vllm.zh-CN.mdx +1 -1
- package/docs/usage/providers/wenxin.mdx +1 -1
- package/docs/usage/providers/wenxin.zh-CN.mdx +1 -1
- package/docs/usage/providers/xai.mdx +1 -1
- package/docs/usage/providers/xai.zh-CN.mdx +1 -1
- package/docs/usage/providers/zeroone.mdx +1 -1
- package/docs/usage/providers/zeroone.zh-CN.mdx +1 -1
- package/docs/usage/providers/zhipu.mdx +1 -1
- package/docs/usage/providers/zhipu.zh-CN.mdx +1 -1
- package/package.json +2 -2
- package/src/config/aiModels/openai.ts +24 -9
- package/src/libs/model-runtime/BaseAI.ts +1 -0
- package/src/libs/model-runtime/ModelRuntime.ts +0 -1
- package/src/libs/model-runtime/hunyuan/index.ts +4 -6
- package/src/libs/model-runtime/novita/__snapshots__/index.test.ts.snap +18 -0
- package/src/libs/model-runtime/openai/__snapshots__/index.test.ts.snap +28 -0
- package/src/libs/model-runtime/openai/index.test.ts +1 -338
- package/src/libs/model-runtime/openai/index.ts +0 -127
- package/src/libs/model-runtime/openrouter/__snapshots__/index.test.ts.snap +3 -0
- package/src/libs/model-runtime/ppio/__snapshots__/index.test.ts.snap +2 -0
- package/src/libs/model-runtime/utils/modelParse.ts +1 -0
- package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.test.ts +364 -12
- package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +145 -43
- package/src/libs/model-runtime/utils/openaiHelpers.test.ts +151 -0
- package/src/libs/model-runtime/utils/openaiHelpers.ts +26 -1
- package/src/libs/model-runtime/xai/index.ts +1 -4
- package/src/store/aiInfra/slices/aiModel/action.ts +1 -1
- package/src/store/aiInfra/slices/aiProvider/action.ts +5 -2
- package/src/types/aiModel.ts +1 -0
- package/src/types/llm.ts +3 -1
- 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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
1029
|
-
|
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:
|
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
|
-
|
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
|
});
|