@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
@@ -10,6 +10,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
10
10
|
"id": "whisper-1",
|
11
11
|
"maxOutput": undefined,
|
12
12
|
"reasoning": false,
|
13
|
+
"type": "stt",
|
13
14
|
"vision": false,
|
14
15
|
},
|
15
16
|
{
|
@@ -20,6 +21,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
20
21
|
"id": "davinci-002",
|
21
22
|
"maxOutput": undefined,
|
22
23
|
"reasoning": false,
|
24
|
+
"type": "chat",
|
23
25
|
"vision": false,
|
24
26
|
},
|
25
27
|
{
|
@@ -30,6 +32,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
30
32
|
"id": "gpt-3.5-turbo",
|
31
33
|
"maxOutput": undefined,
|
32
34
|
"reasoning": false,
|
35
|
+
"type": "chat",
|
33
36
|
"vision": false,
|
34
37
|
},
|
35
38
|
{
|
@@ -40,6 +43,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
40
43
|
"id": "dall-e-2",
|
41
44
|
"maxOutput": undefined,
|
42
45
|
"reasoning": false,
|
46
|
+
"type": "image",
|
43
47
|
"vision": false,
|
44
48
|
},
|
45
49
|
{
|
@@ -50,6 +54,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
50
54
|
"id": "gpt-3.5-turbo-16k",
|
51
55
|
"maxOutput": undefined,
|
52
56
|
"reasoning": false,
|
57
|
+
"type": "chat",
|
53
58
|
"vision": false,
|
54
59
|
},
|
55
60
|
{
|
@@ -60,6 +65,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
60
65
|
"id": "tts-1-hd-1106",
|
61
66
|
"maxOutput": undefined,
|
62
67
|
"reasoning": false,
|
68
|
+
"type": "chat",
|
63
69
|
"vision": false,
|
64
70
|
},
|
65
71
|
{
|
@@ -70,6 +76,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
70
76
|
"id": "tts-1-hd",
|
71
77
|
"maxOutput": undefined,
|
72
78
|
"reasoning": false,
|
79
|
+
"type": "tts",
|
73
80
|
"vision": false,
|
74
81
|
},
|
75
82
|
{
|
@@ -80,6 +87,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
80
87
|
"id": "gpt-3.5-turbo-16k-0613",
|
81
88
|
"maxOutput": undefined,
|
82
89
|
"reasoning": false,
|
90
|
+
"type": "chat",
|
83
91
|
"vision": false,
|
84
92
|
},
|
85
93
|
{
|
@@ -90,6 +98,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
90
98
|
"id": "text-embedding-3-large",
|
91
99
|
"maxOutput": undefined,
|
92
100
|
"reasoning": false,
|
101
|
+
"type": "embedding",
|
93
102
|
"vision": false,
|
94
103
|
},
|
95
104
|
{
|
@@ -100,6 +109,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
100
109
|
"id": "gpt-4-1106-vision-preview",
|
101
110
|
"maxOutput": undefined,
|
102
111
|
"reasoning": false,
|
112
|
+
"type": "chat",
|
103
113
|
"vision": false,
|
104
114
|
},
|
105
115
|
{
|
@@ -110,6 +120,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
110
120
|
"id": "gpt-3.5-turbo-instruct-0914",
|
111
121
|
"maxOutput": undefined,
|
112
122
|
"reasoning": false,
|
123
|
+
"type": "chat",
|
113
124
|
"vision": false,
|
114
125
|
},
|
115
126
|
{
|
@@ -120,6 +131,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
120
131
|
"id": "gpt-4-0125-preview",
|
121
132
|
"maxOutput": undefined,
|
122
133
|
"reasoning": false,
|
134
|
+
"type": "chat",
|
123
135
|
"vision": false,
|
124
136
|
},
|
125
137
|
{
|
@@ -130,6 +142,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
130
142
|
"id": "gpt-4-turbo-preview",
|
131
143
|
"maxOutput": undefined,
|
132
144
|
"reasoning": false,
|
145
|
+
"type": "chat",
|
133
146
|
"vision": false,
|
134
147
|
},
|
135
148
|
{
|
@@ -140,6 +153,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
140
153
|
"id": "gpt-3.5-turbo-instruct",
|
141
154
|
"maxOutput": undefined,
|
142
155
|
"reasoning": false,
|
156
|
+
"type": "chat",
|
143
157
|
"vision": false,
|
144
158
|
},
|
145
159
|
{
|
@@ -150,6 +164,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
150
164
|
"id": "gpt-3.5-turbo-0301",
|
151
165
|
"maxOutput": undefined,
|
152
166
|
"reasoning": false,
|
167
|
+
"type": "chat",
|
153
168
|
"vision": false,
|
154
169
|
},
|
155
170
|
{
|
@@ -160,6 +175,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
160
175
|
"id": "gpt-3.5-turbo-0613",
|
161
176
|
"maxOutput": undefined,
|
162
177
|
"reasoning": false,
|
178
|
+
"type": "chat",
|
163
179
|
"vision": false,
|
164
180
|
},
|
165
181
|
{
|
@@ -170,6 +186,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
170
186
|
"id": "tts-1",
|
171
187
|
"maxOutput": undefined,
|
172
188
|
"reasoning": false,
|
189
|
+
"type": "tts",
|
173
190
|
"vision": false,
|
174
191
|
},
|
175
192
|
{
|
@@ -180,6 +197,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
180
197
|
"id": "dall-e-3",
|
181
198
|
"maxOutput": undefined,
|
182
199
|
"reasoning": false,
|
200
|
+
"type": "image",
|
183
201
|
"vision": false,
|
184
202
|
},
|
185
203
|
{
|
@@ -190,6 +208,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
190
208
|
"id": "gpt-3.5-turbo-1106",
|
191
209
|
"maxOutput": undefined,
|
192
210
|
"reasoning": false,
|
211
|
+
"type": "chat",
|
193
212
|
"vision": false,
|
194
213
|
},
|
195
214
|
{
|
@@ -200,6 +219,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
200
219
|
"id": "gpt-4-1106-preview",
|
201
220
|
"maxOutput": undefined,
|
202
221
|
"reasoning": false,
|
222
|
+
"type": "chat",
|
203
223
|
"vision": false,
|
204
224
|
},
|
205
225
|
{
|
@@ -210,6 +230,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
210
230
|
"id": "babbage-002",
|
211
231
|
"maxOutput": undefined,
|
212
232
|
"reasoning": false,
|
233
|
+
"type": "chat",
|
213
234
|
"vision": false,
|
214
235
|
},
|
215
236
|
{
|
@@ -220,6 +241,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
220
241
|
"id": "tts-1-1106",
|
221
242
|
"maxOutput": undefined,
|
222
243
|
"reasoning": false,
|
244
|
+
"type": "chat",
|
223
245
|
"vision": false,
|
224
246
|
},
|
225
247
|
{
|
@@ -230,6 +252,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
230
252
|
"id": "gpt-4-vision-preview",
|
231
253
|
"maxOutput": undefined,
|
232
254
|
"reasoning": false,
|
255
|
+
"type": "chat",
|
233
256
|
"vision": true,
|
234
257
|
},
|
235
258
|
{
|
@@ -240,6 +263,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
240
263
|
"id": "text-embedding-3-small",
|
241
264
|
"maxOutput": undefined,
|
242
265
|
"reasoning": false,
|
266
|
+
"type": "embedding",
|
243
267
|
"vision": false,
|
244
268
|
},
|
245
269
|
{
|
@@ -250,6 +274,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
250
274
|
"id": "gpt-4",
|
251
275
|
"maxOutput": 4096,
|
252
276
|
"reasoning": false,
|
277
|
+
"type": "chat",
|
253
278
|
"vision": true,
|
254
279
|
},
|
255
280
|
{
|
@@ -260,6 +285,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
260
285
|
"id": "text-embedding-ada-002",
|
261
286
|
"maxOutput": undefined,
|
262
287
|
"reasoning": false,
|
288
|
+
"type": "chat",
|
263
289
|
"vision": false,
|
264
290
|
},
|
265
291
|
{
|
@@ -270,6 +296,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
270
296
|
"id": "gpt-3.5-turbo-0125",
|
271
297
|
"maxOutput": undefined,
|
272
298
|
"reasoning": false,
|
299
|
+
"type": "chat",
|
273
300
|
"vision": false,
|
274
301
|
},
|
275
302
|
{
|
@@ -280,6 +307,7 @@ exports[`LobeOpenAI > models > should get models 1`] = `
|
|
280
307
|
"id": "gpt-4-0613",
|
281
308
|
"maxOutput": undefined,
|
282
309
|
"reasoning": false,
|
310
|
+
"type": "chat",
|
283
311
|
"vision": false,
|
284
312
|
},
|
285
313
|
]
|
@@ -4,7 +4,6 @@ import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
|
5
5
|
// 引入模块以便于对函数进行spy
|
6
6
|
import { ChatStreamCallbacks } from '@/libs/model-runtime';
|
7
|
-
import * as openai from '@/libs/model-runtime/openai';
|
8
7
|
|
9
8
|
import * as debugStreamModule from '../utils/debugStream';
|
10
9
|
import officalOpenAIModels from './fixtures/openai-models.json';
|
@@ -13,22 +12,13 @@ import { LobeOpenAI } from './index';
|
|
13
12
|
// Mock the console.error to avoid polluting test output
|
14
13
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
15
14
|
|
16
|
-
// Mock fetch for most tests, but will be restored for
|
15
|
+
// Mock fetch for most tests, but will be restored for real network tests
|
17
16
|
const mockFetch = vi.fn();
|
18
17
|
global.fetch = mockFetch;
|
19
18
|
|
20
|
-
const convertImageUrlToFileSpy = vi.spyOn(openai, 'convertImageUrlToFile');
|
21
|
-
|
22
19
|
describe('LobeOpenAI', () => {
|
23
20
|
let instance: InstanceType<typeof LobeOpenAI>;
|
24
21
|
|
25
|
-
// Create mock params for createImage tests - only gpt-image-1 supported params
|
26
|
-
const mockParams = {
|
27
|
-
prompt: 'test prompt',
|
28
|
-
imageUrls: [] as string[],
|
29
|
-
size: '1024x1024' as const,
|
30
|
-
};
|
31
|
-
|
32
22
|
beforeEach(() => {
|
33
23
|
instance = new LobeOpenAI({ apiKey: 'test' });
|
34
24
|
|
@@ -40,27 +30,6 @@ describe('LobeOpenAI', () => {
|
|
40
30
|
|
41
31
|
// Mock responses.create for responses API tests
|
42
32
|
vi.spyOn(instance['client'].responses, 'create').mockResolvedValue(new ReadableStream() as any);
|
43
|
-
|
44
|
-
// Mock convertImageUrlToFile to return a mock File object
|
45
|
-
convertImageUrlToFileSpy.mockResolvedValue({
|
46
|
-
name: 'image.png',
|
47
|
-
type: 'image/png',
|
48
|
-
size: 1024,
|
49
|
-
} as any);
|
50
|
-
|
51
|
-
// Mock fetch response for most tests
|
52
|
-
mockFetch.mockResolvedValue({
|
53
|
-
ok: true,
|
54
|
-
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
55
|
-
headers: {
|
56
|
-
get: (header: string) => {
|
57
|
-
if (header === 'content-type') {
|
58
|
-
return 'image/png';
|
59
|
-
}
|
60
|
-
return null;
|
61
|
-
},
|
62
|
-
},
|
63
|
-
});
|
64
33
|
});
|
65
34
|
|
66
35
|
afterEach(() => {
|
@@ -282,312 +251,6 @@ describe('LobeOpenAI', () => {
|
|
282
251
|
});
|
283
252
|
});
|
284
253
|
|
285
|
-
describe('createImage', () => {
|
286
|
-
it('should generate an image with gpt-image-1', async () => {
|
287
|
-
// Arrange
|
288
|
-
const mockResponse = { data: [{ b64_json: 'test-base64-string' }] };
|
289
|
-
const generateSpy = vi
|
290
|
-
.spyOn(instance['client'].images, 'generate')
|
291
|
-
.mockResolvedValue(mockResponse as any);
|
292
|
-
|
293
|
-
// Act
|
294
|
-
const result = await instance.createImage({
|
295
|
-
model: 'gpt-image-1',
|
296
|
-
params: {
|
297
|
-
...mockParams,
|
298
|
-
prompt: 'A cute cat',
|
299
|
-
size: '1024x1024',
|
300
|
-
},
|
301
|
-
});
|
302
|
-
|
303
|
-
// Assert
|
304
|
-
expect(generateSpy).toHaveBeenCalledWith(
|
305
|
-
expect.objectContaining({
|
306
|
-
model: 'gpt-image-1',
|
307
|
-
prompt: 'A cute cat',
|
308
|
-
n: 1,
|
309
|
-
size: '1024x1024',
|
310
|
-
}),
|
311
|
-
);
|
312
|
-
expect(result.imageUrl).toBe('data:image/png;base64,test-base64-string');
|
313
|
-
});
|
314
|
-
|
315
|
-
it('should edit an image from a URL', async () => {
|
316
|
-
// Arrange
|
317
|
-
const mockResponse = { data: [{ b64_json: 'edited-base64-string' }] };
|
318
|
-
const editSpy = vi
|
319
|
-
.spyOn(instance['client'].images, 'edit')
|
320
|
-
.mockResolvedValue(mockResponse as any);
|
321
|
-
|
322
|
-
// Temporarily restore the spy to use real implementation
|
323
|
-
convertImageUrlToFileSpy.mockRestore();
|
324
|
-
|
325
|
-
const imageUrl = 'https://lobehub.com/_next/static/media/logo.98482105.png';
|
326
|
-
|
327
|
-
// Act
|
328
|
-
const result = await instance.createImage({
|
329
|
-
model: 'gpt-image-1',
|
330
|
-
params: {
|
331
|
-
...mockParams,
|
332
|
-
prompt: 'A cat in a hat',
|
333
|
-
imageUrls: [imageUrl],
|
334
|
-
},
|
335
|
-
});
|
336
|
-
|
337
|
-
// Assert
|
338
|
-
expect(editSpy).toHaveBeenCalled();
|
339
|
-
const callArg = editSpy.mock.calls[0][0];
|
340
|
-
expect(callArg.model).toBe('gpt-image-1');
|
341
|
-
expect(callArg.prompt).toBe('A cat in a hat');
|
342
|
-
expect(result.imageUrl).toBe('data:image/png;base64,edited-base64-string');
|
343
|
-
|
344
|
-
// Restore the spy for other tests
|
345
|
-
convertImageUrlToFileSpy.mockResolvedValue({
|
346
|
-
name: 'image.png',
|
347
|
-
type: 'image/png',
|
348
|
-
size: 1024,
|
349
|
-
} as any);
|
350
|
-
});
|
351
|
-
|
352
|
-
it('should handle `size` set to `auto`', async () => {
|
353
|
-
// Arrange
|
354
|
-
const mockResponse = { data: [{ b64_json: 'test-base64-string' }] };
|
355
|
-
const generateSpy = vi
|
356
|
-
.spyOn(instance['client'].images, 'generate')
|
357
|
-
.mockResolvedValue(mockResponse as any);
|
358
|
-
|
359
|
-
// Act
|
360
|
-
await instance.createImage({
|
361
|
-
model: 'gpt-image-1',
|
362
|
-
params: {
|
363
|
-
...mockParams,
|
364
|
-
prompt: 'A cute cat',
|
365
|
-
size: 'auto',
|
366
|
-
},
|
367
|
-
});
|
368
|
-
|
369
|
-
// Assert
|
370
|
-
expect(generateSpy).toHaveBeenCalledWith(
|
371
|
-
expect.objectContaining({
|
372
|
-
model: 'gpt-image-1',
|
373
|
-
prompt: 'A cute cat',
|
374
|
-
n: 1,
|
375
|
-
}),
|
376
|
-
);
|
377
|
-
// Should not include size when it's 'auto'
|
378
|
-
expect(generateSpy.mock.calls[0][0]).not.toHaveProperty('size');
|
379
|
-
});
|
380
|
-
|
381
|
-
it('should throw an error if convertImageUrlToFile fails', async () => {
|
382
|
-
// Arrange
|
383
|
-
const imageUrl = 'https://example.com/test-image.png';
|
384
|
-
|
385
|
-
// Mock fetch to fail for the image URL, which will cause convertImageUrlToFile to fail
|
386
|
-
vi.mocked(global.fetch).mockRejectedValueOnce(new Error('Network error'));
|
387
|
-
|
388
|
-
// Mock the OpenAI API methods to ensure they don't get called
|
389
|
-
const generateSpy = vi.spyOn(instance['client'].images, 'generate');
|
390
|
-
const editSpy = vi.spyOn(instance['client'].images, 'edit');
|
391
|
-
|
392
|
-
// Act & Assert - Note: imageUrls must be non-empty array to trigger isImageEdit = true
|
393
|
-
await expect(
|
394
|
-
instance.createImage({
|
395
|
-
model: 'gpt-image-1',
|
396
|
-
params: {
|
397
|
-
prompt: 'A cat in a hat',
|
398
|
-
imageUrls: [imageUrl], // This is the key - non-empty array
|
399
|
-
},
|
400
|
-
}),
|
401
|
-
).rejects.toThrow('Failed to convert image URLs to File objects: Error: Network error');
|
402
|
-
|
403
|
-
// Verify that OpenAI API methods were not called since conversion failed
|
404
|
-
expect(generateSpy).not.toHaveBeenCalled();
|
405
|
-
expect(editSpy).not.toHaveBeenCalled();
|
406
|
-
});
|
407
|
-
|
408
|
-
it('should throw an error when image response is missing data array', async () => {
|
409
|
-
// Arrange
|
410
|
-
const mockInvalidResponse = {}; // missing data property
|
411
|
-
vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockInvalidResponse as any);
|
412
|
-
|
413
|
-
// Act & Assert
|
414
|
-
await expect(
|
415
|
-
instance.createImage({
|
416
|
-
model: 'gpt-image-1',
|
417
|
-
params: { ...mockParams, prompt: 'A cute cat' },
|
418
|
-
}),
|
419
|
-
).rejects.toThrow('Invalid image response: missing or empty data array');
|
420
|
-
});
|
421
|
-
|
422
|
-
it('should throw an error when image response data array is empty', async () => {
|
423
|
-
// Arrange
|
424
|
-
const mockInvalidResponse = { data: [] }; // empty data array
|
425
|
-
vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockInvalidResponse as any);
|
426
|
-
|
427
|
-
// Act & Assert
|
428
|
-
await expect(
|
429
|
-
instance.createImage({
|
430
|
-
model: 'gpt-image-1',
|
431
|
-
params: { ...mockParams, prompt: 'A cute cat' },
|
432
|
-
}),
|
433
|
-
).rejects.toThrow('Invalid image response: missing or empty data array');
|
434
|
-
});
|
435
|
-
|
436
|
-
it('should throw an error when first data item is null', async () => {
|
437
|
-
// Arrange
|
438
|
-
const mockInvalidResponse = { data: [null] }; // first item is null
|
439
|
-
vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockInvalidResponse as any);
|
440
|
-
|
441
|
-
// Act & Assert
|
442
|
-
await expect(
|
443
|
-
instance.createImage({
|
444
|
-
model: 'gpt-image-1',
|
445
|
-
params: { ...mockParams, prompt: 'A cute cat' },
|
446
|
-
}),
|
447
|
-
).rejects.toThrow('Invalid image response: first data item is null or undefined');
|
448
|
-
});
|
449
|
-
|
450
|
-
it('should throw an error when first data item is undefined', async () => {
|
451
|
-
// Arrange
|
452
|
-
const mockInvalidResponse = { data: [undefined] }; // first item is undefined
|
453
|
-
vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockInvalidResponse as any);
|
454
|
-
|
455
|
-
// Act & Assert
|
456
|
-
await expect(
|
457
|
-
instance.createImage({
|
458
|
-
model: 'gpt-image-1',
|
459
|
-
params: { ...mockParams, prompt: 'A cute cat' },
|
460
|
-
}),
|
461
|
-
).rejects.toThrow('Invalid image response: first data item is null or undefined');
|
462
|
-
});
|
463
|
-
|
464
|
-
it('should re-throw OpenAI API errors during image generation', async () => {
|
465
|
-
// Arrange
|
466
|
-
const apiError = new OpenAI.APIError(
|
467
|
-
400,
|
468
|
-
{ error: { message: 'Bad Request' } },
|
469
|
-
'Error message',
|
470
|
-
{},
|
471
|
-
);
|
472
|
-
vi.spyOn(instance['client'].images, 'generate').mockRejectedValue(apiError);
|
473
|
-
|
474
|
-
// Act & Assert
|
475
|
-
await expect(
|
476
|
-
instance.createImage({
|
477
|
-
model: 'gpt-image-1',
|
478
|
-
params: { ...mockParams, prompt: 'A cute cat' },
|
479
|
-
}),
|
480
|
-
).rejects.toThrow(apiError);
|
481
|
-
});
|
482
|
-
|
483
|
-
it('should throw an error for invalid image response', async () => {
|
484
|
-
// Arrange
|
485
|
-
const mockInvalidResponse = { data: [{ url: 'some_url' }] }; // missing b64_json
|
486
|
-
vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockInvalidResponse as any);
|
487
|
-
|
488
|
-
// Act & Assert
|
489
|
-
await expect(
|
490
|
-
instance.createImage({
|
491
|
-
model: 'gpt-image-1',
|
492
|
-
params: { ...mockParams, prompt: 'A cute cat' },
|
493
|
-
}),
|
494
|
-
).rejects.toThrow('Invalid image response: missing b64_json field');
|
495
|
-
});
|
496
|
-
});
|
497
|
-
|
498
|
-
describe('convertImageUrlToFile', () => {
|
499
|
-
beforeEach(() => {
|
500
|
-
// Reset the spy to use the real implementation for these tests
|
501
|
-
convertImageUrlToFileSpy.mockRestore();
|
502
|
-
});
|
503
|
-
|
504
|
-
afterEach(() => {
|
505
|
-
// Restore the spy for other tests
|
506
|
-
convertImageUrlToFileSpy.mockResolvedValue({
|
507
|
-
name: 'image.png',
|
508
|
-
type: 'image/png',
|
509
|
-
size: 1024,
|
510
|
-
} as any);
|
511
|
-
});
|
512
|
-
|
513
|
-
it('should convert the real lobehub logo URL to a FileLike object', async () => {
|
514
|
-
const imageUrl = 'https://lobehub.com/_next/static/media/logo.98482105.png';
|
515
|
-
const file = await openai.convertImageUrlToFile(imageUrl);
|
516
|
-
|
517
|
-
expect(file).toBeDefined();
|
518
|
-
expect((file as any).name).toBe('image.png');
|
519
|
-
expect((file as any).type).toMatch(/^image\//);
|
520
|
-
expect((file as any).size).toBeGreaterThan(0);
|
521
|
-
});
|
522
|
-
|
523
|
-
it('should convert a base64 data URL to a FileLike object', async () => {
|
524
|
-
const dataUrl =
|
525
|
-
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAABAAEBAREA/8QAFAABAAAAAAAAAAAAAAAAAAAAA//EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAD8AN//Z';
|
526
|
-
const file = await openai.convertImageUrlToFile(dataUrl);
|
527
|
-
|
528
|
-
expect(file).toBeDefined();
|
529
|
-
expect((file as any).name).toBe('image.jpeg');
|
530
|
-
expect((file as any).type).toBe('image/jpeg');
|
531
|
-
});
|
532
|
-
|
533
|
-
it('should handle different image mime types from data URL', async () => {
|
534
|
-
const webpDataUrl = 'data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==';
|
535
|
-
const file = await openai.convertImageUrlToFile(webpDataUrl);
|
536
|
-
|
537
|
-
expect(file).toBeDefined();
|
538
|
-
expect((file as any).name).toBe('image.webp');
|
539
|
-
expect((file as any).type).toBe('image/webp');
|
540
|
-
});
|
541
|
-
});
|
542
|
-
|
543
|
-
// Separate describe block for mocked fetch scenarios
|
544
|
-
describe('convertImageUrlToFile - mocked scenarios', () => {
|
545
|
-
beforeEach(() => {
|
546
|
-
// Reset the spy to use the real implementation
|
547
|
-
convertImageUrlToFileSpy.mockRestore();
|
548
|
-
});
|
549
|
-
|
550
|
-
afterEach(() => {
|
551
|
-
// Restore the spy for other tests
|
552
|
-
convertImageUrlToFileSpy.mockResolvedValue({
|
553
|
-
name: 'image.png',
|
554
|
-
type: 'image/png',
|
555
|
-
size: 1024,
|
556
|
-
} as any);
|
557
|
-
});
|
558
|
-
|
559
|
-
it('should throw an error if fetching an image from a URL fails', async () => {
|
560
|
-
// Use vi.mocked for type-safe mocking
|
561
|
-
vi.mocked(global.fetch).mockResolvedValueOnce({
|
562
|
-
ok: false,
|
563
|
-
statusText: 'Not Found',
|
564
|
-
} as any);
|
565
|
-
|
566
|
-
const imageUrl = 'https://example.com/invalid-image.png';
|
567
|
-
|
568
|
-
await expect(openai.convertImageUrlToFile(imageUrl)).rejects.toThrow(
|
569
|
-
'Failed to fetch image from https://example.com/invalid-image.png: Not Found',
|
570
|
-
);
|
571
|
-
});
|
572
|
-
|
573
|
-
it('should use a default mime type of image/png if content-type header is not available', async () => {
|
574
|
-
// Use vi.mocked for type-safe mocking
|
575
|
-
vi.mocked(global.fetch).mockResolvedValueOnce({
|
576
|
-
ok: true,
|
577
|
-
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
578
|
-
headers: {
|
579
|
-
get: () => null,
|
580
|
-
},
|
581
|
-
} as any);
|
582
|
-
|
583
|
-
const imageUrl = 'https://example.com/image-no-content-type';
|
584
|
-
const file = await openai.convertImageUrlToFile(imageUrl);
|
585
|
-
|
586
|
-
expect(file).toBeDefined();
|
587
|
-
expect((file as any).type).toBe('image/png');
|
588
|
-
});
|
589
|
-
});
|
590
|
-
|
591
254
|
describe('responses.handlePayload', () => {
|
592
255
|
it('should add web_search_preview tool when enabledSearch is true', async () => {
|
593
256
|
const payload = {
|