@lobehub/chat 1.126.2 → 1.127.0
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 +59 -0
- package/changelog/v1.json +21 -0
- package/docs/self-hosting/environment-variables/model-provider.mdx +2 -2
- package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +2 -2
- package/locales/ar/models.json +38 -11
- package/locales/bg-BG/models.json +38 -11
- package/locales/de-DE/models.json +38 -11
- package/locales/en-US/models.json +38 -11
- package/locales/es-ES/models.json +38 -11
- package/locales/fa-IR/models.json +38 -11
- package/locales/fr-FR/models.json +38 -11
- package/locales/it-IT/models.json +38 -11
- package/locales/ja-JP/models.json +38 -11
- package/locales/ko-KR/models.json +38 -11
- package/locales/nl-NL/models.json +38 -11
- package/locales/pl-PL/models.json +38 -11
- package/locales/pt-BR/models.json +38 -11
- package/locales/ru-RU/models.json +38 -11
- package/locales/tr-TR/models.json +38 -11
- package/locales/vi-VN/models.json +38 -11
- package/locales/zh-CN/image.json +3 -0
- package/locales/zh-CN/models.json +38 -11
- package/locales/zh-TW/models.json +38 -11
- package/package.json +3 -3
- package/packages/model-bank/package.json +1 -0
- package/packages/model-bank/src/aiModels/cometapi.ts +349 -0
- package/packages/model-bank/src/aiModels/fal.ts +46 -7
- package/packages/model-bank/src/aiModels/index.ts +3 -0
- package/packages/model-bank/src/aiModels/volcengine.ts +51 -21
- package/packages/model-bank/src/standard-parameters/index.ts +3 -0
- package/packages/model-runtime/src/cometapi/index.ts +49 -0
- package/packages/model-runtime/src/fal/index.test.ts +374 -0
- package/packages/model-runtime/src/fal/index.ts +23 -14
- package/packages/model-runtime/src/index.ts +1 -0
- package/packages/model-runtime/src/runtimeMap.ts +2 -0
- package/packages/model-runtime/src/types/type.ts +1 -0
- package/packages/model-runtime/src/volcengine/createImage.test.ts +522 -0
- package/packages/model-runtime/src/volcengine/createImage.ts +118 -0
- package/packages/model-runtime/src/volcengine/index.ts +2 -0
- package/packages/types/src/user/settings/keyVaults.ts +1 -0
- package/packages/utils/src/parseModels.test.ts +11 -8
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/CfgSliderInput.tsx +11 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/StepsSliderInput.tsx +2 -2
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +9 -0
- package/src/config/llm.ts +6 -0
- package/src/config/modelProviders/cometapi.ts +24 -0
- package/src/config/modelProviders/index.ts +3 -0
- package/src/features/ChatInput/ActionBar/index.tsx +19 -1
- package/src/features/ChatInput/Desktop/index.tsx +7 -0
- package/src/features/ChatInput/InputEditor/index.tsx +4 -6
- package/src/features/ChatInput/TypoBar/index.tsx +116 -103
- package/src/locales/default/image.ts +3 -0
- package/src/server/routers/async/image.ts +6 -1
- package/src/store/global/actions/workspacePane.ts +7 -0
- package/src/store/global/initialState.ts +2 -0
- package/src/store/global/selectors/systemStatus.ts +2 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { CreateImagePayload } from '../types/image';
|
|
4
|
+
import { CreateImageOptions } from '../utils/openaiCompatibleFactory';
|
|
5
|
+
import { createVolcengineImage } from './createImage';
|
|
6
|
+
|
|
7
|
+
// Mock dependencies
|
|
8
|
+
vi.mock('debug', () => ({
|
|
9
|
+
default: vi.fn(() => vi.fn()),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
const mockGenerate = vi.fn();
|
|
13
|
+
vi.mock('openai', () => ({
|
|
14
|
+
default: vi.fn().mockImplementation(() => ({
|
|
15
|
+
images: {
|
|
16
|
+
generate: mockGenerate,
|
|
17
|
+
},
|
|
18
|
+
})),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
describe('createVolcengineImage', () => {
|
|
22
|
+
let payload: CreateImagePayload;
|
|
23
|
+
let options: CreateImageOptions;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
|
|
28
|
+
// Default test payload and options
|
|
29
|
+
payload = {
|
|
30
|
+
model: 'doubao-seedream-3-0-t2i',
|
|
31
|
+
params: {
|
|
32
|
+
prompt: 'a beautiful landscape',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
options = {
|
|
37
|
+
apiKey: 'test-api-key',
|
|
38
|
+
provider: 'volcengine',
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('successful image generation', () => {
|
|
43
|
+
it('should generate image with URL response format', async () => {
|
|
44
|
+
const mockResponse = {
|
|
45
|
+
data: [
|
|
46
|
+
{
|
|
47
|
+
url: 'https://example.com/generated-image.jpg',
|
|
48
|
+
size: '1024x768',
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
53
|
+
|
|
54
|
+
const result = await createVolcengineImage(payload, options);
|
|
55
|
+
|
|
56
|
+
expect(result).toEqual({
|
|
57
|
+
imageUrl: 'https://example.com/generated-image.jpg',
|
|
58
|
+
width: 1024,
|
|
59
|
+
height: 768,
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should generate image with base64 response format', async () => {
|
|
64
|
+
const mockBase64 =
|
|
65
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
|
|
66
|
+
const mockResponse = {
|
|
67
|
+
data: [
|
|
68
|
+
{
|
|
69
|
+
b64_json: mockBase64,
|
|
70
|
+
size: '512x512',
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
75
|
+
|
|
76
|
+
const result = await createVolcengineImage(payload, options);
|
|
77
|
+
|
|
78
|
+
expect(result).toEqual({
|
|
79
|
+
imageUrl: `data:image/jpeg;base64,${mockBase64}`,
|
|
80
|
+
width: 512,
|
|
81
|
+
height: 512,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should handle response without size information', async () => {
|
|
86
|
+
const mockResponse = {
|
|
87
|
+
data: [
|
|
88
|
+
{
|
|
89
|
+
url: 'https://example.com/generated-image.jpg',
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
94
|
+
|
|
95
|
+
const result = await createVolcengineImage(payload, options);
|
|
96
|
+
|
|
97
|
+
expect(result).toEqual({
|
|
98
|
+
imageUrl: 'https://example.com/generated-image.jpg',
|
|
99
|
+
width: undefined,
|
|
100
|
+
height: undefined,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('parameter mapping', () => {
|
|
106
|
+
it('should map cfg parameter to guidance_scale', async () => {
|
|
107
|
+
const mockResponse = {
|
|
108
|
+
data: [{ url: 'https://example.com/test.jpg' }],
|
|
109
|
+
};
|
|
110
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
111
|
+
|
|
112
|
+
payload.params = {
|
|
113
|
+
prompt: 'test prompt',
|
|
114
|
+
cfg: 7.5,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
await createVolcengineImage(payload, options);
|
|
118
|
+
|
|
119
|
+
expect(mockGenerate).toHaveBeenCalledWith({
|
|
120
|
+
model: 'doubao-seedream-3-0-t2i',
|
|
121
|
+
watermark: false,
|
|
122
|
+
prompt: 'test prompt',
|
|
123
|
+
guidance_scale: 7.5,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should map imageUrls parameter to image', async () => {
|
|
128
|
+
const mockResponse = {
|
|
129
|
+
data: [{ url: 'https://example.com/test.jpg' }],
|
|
130
|
+
};
|
|
131
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
132
|
+
|
|
133
|
+
payload.params = {
|
|
134
|
+
prompt: 'test prompt',
|
|
135
|
+
imageUrls: ['https://example.com/input1.jpg', 'https://example.com/input2.jpg'],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
await createVolcengineImage(payload, options);
|
|
139
|
+
|
|
140
|
+
expect(mockGenerate).toHaveBeenCalledWith({
|
|
141
|
+
model: 'doubao-seedream-3-0-t2i',
|
|
142
|
+
watermark: false,
|
|
143
|
+
prompt: 'test prompt',
|
|
144
|
+
image: ['https://example.com/input1.jpg', 'https://example.com/input2.jpg'],
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should map imageUrl parameter to image', async () => {
|
|
149
|
+
const mockResponse = {
|
|
150
|
+
data: [{ url: 'https://example.com/test.jpg' }],
|
|
151
|
+
};
|
|
152
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
153
|
+
|
|
154
|
+
payload.params = {
|
|
155
|
+
prompt: 'test prompt',
|
|
156
|
+
imageUrl: 'https://example.com/input.jpg',
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
await createVolcengineImage(payload, options);
|
|
160
|
+
|
|
161
|
+
expect(mockGenerate).toHaveBeenCalledWith({
|
|
162
|
+
model: 'doubao-seedream-3-0-t2i',
|
|
163
|
+
watermark: false,
|
|
164
|
+
prompt: 'test prompt',
|
|
165
|
+
image: 'https://example.com/input.jpg',
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should preserve unmapped parameters', async () => {
|
|
170
|
+
const mockResponse = {
|
|
171
|
+
data: [{ url: 'https://example.com/test.jpg' }],
|
|
172
|
+
};
|
|
173
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
174
|
+
|
|
175
|
+
payload.params = {
|
|
176
|
+
prompt: 'test prompt',
|
|
177
|
+
seed: 12345,
|
|
178
|
+
size: '1024x1024',
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
await createVolcengineImage(payload, options);
|
|
182
|
+
|
|
183
|
+
expect(mockGenerate).toHaveBeenCalledWith({
|
|
184
|
+
model: 'doubao-seedream-3-0-t2i',
|
|
185
|
+
watermark: false,
|
|
186
|
+
prompt: 'test prompt',
|
|
187
|
+
seed: 12345,
|
|
188
|
+
size: '1024x1024',
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('image input handling', () => {
|
|
194
|
+
it('should preserve image input when array has items', async () => {
|
|
195
|
+
const mockResponse = {
|
|
196
|
+
data: [{ url: 'https://example.com/test.jpg' }],
|
|
197
|
+
};
|
|
198
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
199
|
+
|
|
200
|
+
payload.params = {
|
|
201
|
+
prompt: 'test prompt',
|
|
202
|
+
imageUrls: ['https://example.com/input.jpg'],
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
await createVolcengineImage(payload, options);
|
|
206
|
+
|
|
207
|
+
expect(mockGenerate).toHaveBeenCalledWith(
|
|
208
|
+
expect.objectContaining({
|
|
209
|
+
image: ['https://example.com/input.jpg'],
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should remove image input when array is empty', async () => {
|
|
215
|
+
const mockResponse = {
|
|
216
|
+
data: [{ url: 'https://example.com/test.jpg' }],
|
|
217
|
+
};
|
|
218
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
219
|
+
|
|
220
|
+
payload.params = {
|
|
221
|
+
prompt: 'test prompt',
|
|
222
|
+
imageUrls: [],
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
await createVolcengineImage(payload, options);
|
|
226
|
+
|
|
227
|
+
expect(mockGenerate).toHaveBeenCalledWith({
|
|
228
|
+
model: 'doubao-seedream-3-0-t2i',
|
|
229
|
+
watermark: false,
|
|
230
|
+
prompt: 'test prompt',
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('should remove image input when value is null', async () => {
|
|
235
|
+
const mockResponse = {
|
|
236
|
+
data: [{ url: 'https://example.com/test.jpg' }],
|
|
237
|
+
};
|
|
238
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
239
|
+
|
|
240
|
+
payload.params = {
|
|
241
|
+
prompt: 'test prompt',
|
|
242
|
+
imageUrl: null,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
await createVolcengineImage(payload, options);
|
|
246
|
+
|
|
247
|
+
expect(mockGenerate).toHaveBeenCalledWith({
|
|
248
|
+
model: 'doubao-seedream-3-0-t2i',
|
|
249
|
+
watermark: false,
|
|
250
|
+
prompt: 'test prompt',
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should remove image input when value is undefined', async () => {
|
|
255
|
+
const mockResponse = {
|
|
256
|
+
data: [{ url: 'https://example.com/test.jpg' }],
|
|
257
|
+
};
|
|
258
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
259
|
+
|
|
260
|
+
payload.params = {
|
|
261
|
+
prompt: 'test prompt',
|
|
262
|
+
imageUrl: undefined,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
await createVolcengineImage(payload, options);
|
|
266
|
+
|
|
267
|
+
expect(mockGenerate).toHaveBeenCalledWith({
|
|
268
|
+
model: 'doubao-seedream-3-0-t2i',
|
|
269
|
+
watermark: false,
|
|
270
|
+
prompt: 'test prompt',
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe('client configuration', () => {
|
|
276
|
+
it('should use provided baseURL when specified', async () => {
|
|
277
|
+
const mockResponse = {
|
|
278
|
+
data: [{ url: 'https://example.com/test.jpg' }],
|
|
279
|
+
};
|
|
280
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
281
|
+
|
|
282
|
+
options.baseURL = 'https://custom-endpoint.com/api/v1';
|
|
283
|
+
|
|
284
|
+
await createVolcengineImage(payload, options);
|
|
285
|
+
|
|
286
|
+
// Verify OpenAI constructor was called with custom baseURL
|
|
287
|
+
const OpenAI = await import('openai');
|
|
288
|
+
expect(OpenAI.default).toHaveBeenCalledWith({
|
|
289
|
+
apiKey: 'test-api-key',
|
|
290
|
+
baseURL: 'https://custom-endpoint.com/api/v1',
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should use default baseURL when not provided', async () => {
|
|
295
|
+
const mockResponse = {
|
|
296
|
+
data: [{ url: 'https://example.com/test.jpg' }],
|
|
297
|
+
};
|
|
298
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
299
|
+
|
|
300
|
+
await createVolcengineImage(payload, options);
|
|
301
|
+
|
|
302
|
+
// Verify OpenAI constructor was called with default baseURL
|
|
303
|
+
const OpenAI = await import('openai');
|
|
304
|
+
expect(OpenAI.default).toHaveBeenCalledWith({
|
|
305
|
+
apiKey: 'test-api-key',
|
|
306
|
+
baseURL: 'https://ark.cn-beijing.volces.com/api/v3',
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should set watermark to false by default', async () => {
|
|
311
|
+
const mockResponse = {
|
|
312
|
+
data: [{ url: 'https://example.com/test.jpg' }],
|
|
313
|
+
};
|
|
314
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
315
|
+
|
|
316
|
+
await createVolcengineImage(payload, options);
|
|
317
|
+
|
|
318
|
+
expect(mockGenerate).toHaveBeenCalledWith(
|
|
319
|
+
expect.objectContaining({
|
|
320
|
+
watermark: false,
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
describe('size extraction', () => {
|
|
327
|
+
it('should extract dimensions from size string format', async () => {
|
|
328
|
+
const mockResponse = {
|
|
329
|
+
data: [
|
|
330
|
+
{
|
|
331
|
+
url: 'https://example.com/test.jpg',
|
|
332
|
+
size: '1920x1080',
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
};
|
|
336
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
337
|
+
|
|
338
|
+
const result = await createVolcengineImage(payload, options);
|
|
339
|
+
|
|
340
|
+
expect(result.width).toBe(1920);
|
|
341
|
+
expect(result.height).toBe(1080);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('should handle malformed size string', async () => {
|
|
345
|
+
const mockResponse = {
|
|
346
|
+
data: [
|
|
347
|
+
{
|
|
348
|
+
url: 'https://example.com/test.jpg',
|
|
349
|
+
size: 'invalid-size-format',
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
};
|
|
353
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
354
|
+
|
|
355
|
+
const result = await createVolcengineImage(payload, options);
|
|
356
|
+
|
|
357
|
+
expect(result.width).toBeUndefined();
|
|
358
|
+
expect(result.height).toBeUndefined();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should handle missing size property', async () => {
|
|
362
|
+
const mockResponse = {
|
|
363
|
+
data: [
|
|
364
|
+
{
|
|
365
|
+
url: 'https://example.com/test.jpg',
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
};
|
|
369
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
370
|
+
|
|
371
|
+
const result = await createVolcengineImage(payload, options);
|
|
372
|
+
|
|
373
|
+
expect(result.width).toBeUndefined();
|
|
374
|
+
expect(result.height).toBeUndefined();
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe('error handling', () => {
|
|
379
|
+
it('should throw error when response is null', async () => {
|
|
380
|
+
mockGenerate.mockResolvedValue(null);
|
|
381
|
+
|
|
382
|
+
await expect(createVolcengineImage(payload, options)).rejects.toThrow(
|
|
383
|
+
'Invalid response: missing or empty data array',
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('should throw error when response.data is missing', async () => {
|
|
388
|
+
mockGenerate.mockResolvedValue({});
|
|
389
|
+
|
|
390
|
+
await expect(createVolcengineImage(payload, options)).rejects.toThrow(
|
|
391
|
+
'Invalid response: missing or empty data array',
|
|
392
|
+
);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should throw error when response.data is not an array', async () => {
|
|
396
|
+
mockGenerate.mockResolvedValue({
|
|
397
|
+
data: 'not-an-array',
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
await expect(createVolcengineImage(payload, options)).rejects.toThrow(
|
|
401
|
+
'Invalid response: missing or empty data array',
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('should throw error when response.data is empty array', async () => {
|
|
406
|
+
mockGenerate.mockResolvedValue({
|
|
407
|
+
data: [],
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
await expect(createVolcengineImage(payload, options)).rejects.toThrow(
|
|
411
|
+
'Invalid response: missing or empty data array',
|
|
412
|
+
);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('should throw error when first data item is null', async () => {
|
|
416
|
+
mockGenerate.mockResolvedValue({
|
|
417
|
+
data: [null],
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
await expect(createVolcengineImage(payload, options)).rejects.toThrow(
|
|
421
|
+
'Invalid response: first data item is null or undefined',
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('should throw error when first data item is undefined', async () => {
|
|
426
|
+
mockGenerate.mockResolvedValue({
|
|
427
|
+
data: [undefined],
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
await expect(createVolcengineImage(payload, options)).rejects.toThrow(
|
|
431
|
+
'Invalid response: first data item is null or undefined',
|
|
432
|
+
);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should throw error when image data has neither url nor b64_json', async () => {
|
|
436
|
+
mockGenerate.mockResolvedValue({
|
|
437
|
+
data: [
|
|
438
|
+
{
|
|
439
|
+
// Missing both url and b64_json
|
|
440
|
+
metadata: 'some-metadata',
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
await expect(createVolcengineImage(payload, options)).rejects.toThrow(
|
|
446
|
+
'Invalid response: missing both b64_json and url fields',
|
|
447
|
+
);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('should throw error when image data has empty url and no b64_json', async () => {
|
|
451
|
+
mockGenerate.mockResolvedValue({
|
|
452
|
+
data: [
|
|
453
|
+
{
|
|
454
|
+
url: '',
|
|
455
|
+
// No b64_json field
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
await expect(createVolcengineImage(payload, options)).rejects.toThrow(
|
|
461
|
+
'Invalid response: missing both b64_json and url fields',
|
|
462
|
+
);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
describe('complex scenarios', () => {
|
|
467
|
+
it('should handle all parameter mappings together', async () => {
|
|
468
|
+
const mockResponse = {
|
|
469
|
+
data: [
|
|
470
|
+
{
|
|
471
|
+
b64_json: 'mock-base64-data',
|
|
472
|
+
size: '2048x1536',
|
|
473
|
+
},
|
|
474
|
+
],
|
|
475
|
+
};
|
|
476
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
477
|
+
|
|
478
|
+
payload.params = {
|
|
479
|
+
prompt: 'complex test prompt',
|
|
480
|
+
cfg: 5.5,
|
|
481
|
+
imageUrls: ['https://example.com/input1.jpg', 'https://example.com/input2.jpg'],
|
|
482
|
+
seed: 42,
|
|
483
|
+
size: '1024x1024',
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const result = await createVolcengineImage(payload, options);
|
|
487
|
+
|
|
488
|
+
expect(mockGenerate).toHaveBeenCalledWith({
|
|
489
|
+
model: 'doubao-seedream-3-0-t2i',
|
|
490
|
+
watermark: false,
|
|
491
|
+
prompt: 'complex test prompt',
|
|
492
|
+
guidance_scale: 5.5,
|
|
493
|
+
image: ['https://example.com/input1.jpg', 'https://example.com/input2.jpg'],
|
|
494
|
+
seed: 42,
|
|
495
|
+
size: '1024x1024',
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
expect(result).toEqual({
|
|
499
|
+
imageUrl: 'data:image/jpeg;base64,mock-base64-data',
|
|
500
|
+
width: 2048,
|
|
501
|
+
height: 1536,
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('should prioritize b64_json over url when both are present', async () => {
|
|
506
|
+
const mockResponse = {
|
|
507
|
+
data: [
|
|
508
|
+
{
|
|
509
|
+
url: 'https://example.com/should-be-ignored.jpg',
|
|
510
|
+
b64_json: 'base64-data-should-be-used',
|
|
511
|
+
size: '800x600',
|
|
512
|
+
},
|
|
513
|
+
],
|
|
514
|
+
};
|
|
515
|
+
mockGenerate.mockResolvedValue(mockResponse);
|
|
516
|
+
|
|
517
|
+
const result = await createVolcengineImage(payload, options);
|
|
518
|
+
|
|
519
|
+
expect(result.imageUrl).toBe('data:image/jpeg;base64,base64-data-should-be-used');
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import createDebug from 'debug';
|
|
2
|
+
import { RuntimeImageGenParamsValue } from 'model-bank';
|
|
3
|
+
import OpenAI from 'openai';
|
|
4
|
+
|
|
5
|
+
import { CreateImagePayload, CreateImageResponse } from '../types/image';
|
|
6
|
+
import { CreateImageOptions } from '../utils/openaiCompatibleFactory';
|
|
7
|
+
|
|
8
|
+
const log = createDebug('lobe-image:volcengine');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Volcengine image generation implementation
|
|
12
|
+
* Based on Volcengine API docs: https://www.volcengine.com/docs/82379/1541523
|
|
13
|
+
*/
|
|
14
|
+
export async function createVolcengineImage(
|
|
15
|
+
payload: CreateImagePayload,
|
|
16
|
+
options: CreateImageOptions,
|
|
17
|
+
): Promise<CreateImageResponse> {
|
|
18
|
+
const { model, params } = payload;
|
|
19
|
+
|
|
20
|
+
log('Creating image with Volcengine API - model: %s, params: %O', model, params);
|
|
21
|
+
|
|
22
|
+
// Create OpenAI client with Volcengine configuration
|
|
23
|
+
const client = new OpenAI({
|
|
24
|
+
apiKey: options.apiKey,
|
|
25
|
+
baseURL: options.baseURL || 'https://ark.cn-beijing.volces.com/api/v3',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Parameter mapping: imageUrls/imageUrl -> image, cfg -> guidance_scale
|
|
29
|
+
const paramsMap = new Map<RuntimeImageGenParamsValue, string>([
|
|
30
|
+
['imageUrls', 'image'],
|
|
31
|
+
['imageUrl', 'image'],
|
|
32
|
+
['cfg', 'guidance_scale'],
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const userInput: Record<string, any> = Object.fromEntries(
|
|
36
|
+
Object.entries(params).map(([key, value]) => [
|
|
37
|
+
paramsMap.get(key as RuntimeImageGenParamsValue) ?? key,
|
|
38
|
+
value,
|
|
39
|
+
]),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Volcengine supports direct URL or base64, no need to convert to File objects
|
|
43
|
+
// Check if there is image input
|
|
44
|
+
const hasImageInput =
|
|
45
|
+
userInput.image !== null &&
|
|
46
|
+
userInput.image !== undefined &&
|
|
47
|
+
(Array.isArray(userInput.image) ? userInput.image.length > 0 : true);
|
|
48
|
+
|
|
49
|
+
if (hasImageInput) {
|
|
50
|
+
log('Image input detected: %O', userInput.image);
|
|
51
|
+
} else {
|
|
52
|
+
delete userInput.image;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Build request options
|
|
56
|
+
const requestOptions = {
|
|
57
|
+
model,
|
|
58
|
+
watermark: false, // Default to no watermark
|
|
59
|
+
...userInput,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
log('Volcengine API options: %O', requestOptions);
|
|
63
|
+
|
|
64
|
+
// Call Volcengine image generation API
|
|
65
|
+
const response = await client.images.generate(requestOptions as any);
|
|
66
|
+
|
|
67
|
+
log('Volcengine API response: %O', response);
|
|
68
|
+
|
|
69
|
+
// Validate response data
|
|
70
|
+
if (!response || !response.data || !Array.isArray(response.data) || response.data.length === 0) {
|
|
71
|
+
log('Invalid response: missing data array');
|
|
72
|
+
throw new Error('Invalid response: missing or empty data array');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const imageData = response.data[0];
|
|
76
|
+
if (!imageData) {
|
|
77
|
+
log('Invalid response: first data item is null/undefined');
|
|
78
|
+
throw new Error('Invalid response: first data item is null or undefined');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let imageUrl: string;
|
|
82
|
+
let width: number | undefined;
|
|
83
|
+
let height: number | undefined;
|
|
84
|
+
|
|
85
|
+
// Handle base64 format response
|
|
86
|
+
if (imageData.b64_json) {
|
|
87
|
+
const mimeType = 'image/jpeg'; // Volcengine defaults to JPEG format
|
|
88
|
+
imageUrl = `data:${mimeType};base64,${imageData.b64_json}`;
|
|
89
|
+
log('Successfully converted base64 to data URL, length: %d', imageUrl.length);
|
|
90
|
+
}
|
|
91
|
+
// Handle URL format response
|
|
92
|
+
else if (imageData.url) {
|
|
93
|
+
imageUrl = imageData.url;
|
|
94
|
+
log('Using direct image URL: %s', imageUrl);
|
|
95
|
+
}
|
|
96
|
+
// If neither format exists, throw error
|
|
97
|
+
else {
|
|
98
|
+
log('Invalid response: missing both b64_json and url fields');
|
|
99
|
+
throw new Error('Invalid response: missing both b64_json and url fields');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Extract size information (Volcengine specific)
|
|
103
|
+
const volcengineImageData = imageData as any;
|
|
104
|
+
if (volcengineImageData.size) {
|
|
105
|
+
const sizeMatch = volcengineImageData.size.match(/^(\d+)x(\d+)$/);
|
|
106
|
+
if (sizeMatch) {
|
|
107
|
+
width = parseInt(sizeMatch[1], 10);
|
|
108
|
+
height = parseInt(sizeMatch[2], 10);
|
|
109
|
+
log('Extracted image dimensions: %dx%d', width, height);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
height,
|
|
115
|
+
imageUrl,
|
|
116
|
+
width,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ModelProvider } from '../types';
|
|
2
2
|
import { MODEL_LIST_CONFIGS, processModelList } from '../utils/modelParse';
|
|
3
3
|
import { createOpenAICompatibleRuntime } from '../utils/openaiCompatibleFactory';
|
|
4
|
+
import { createVolcengineImage } from './createImage';
|
|
4
5
|
|
|
5
6
|
const THINKING_MODELS = [
|
|
6
7
|
'thinking-vision-pro',
|
|
@@ -31,6 +32,7 @@ export const LobeVolcengineAI = createOpenAICompatibleRuntime({
|
|
|
31
32
|
} as any;
|
|
32
33
|
},
|
|
33
34
|
},
|
|
35
|
+
createImage: createVolcengineImage,
|
|
34
36
|
debug: {
|
|
35
37
|
chatCompletion: () => process.env.DEBUG_VOLCENGINE_CHAT_COMPLETION === '1',
|
|
36
38
|
},
|
|
@@ -49,6 +49,7 @@ export interface UserKeyVaults extends SearchEngineKeyVaults {
|
|
|
49
49
|
bedrock?: AWSBedrockKeyVault;
|
|
50
50
|
cloudflare?: CloudflareKeyVault;
|
|
51
51
|
cohere?: OpenAICompatibleKeyVault;
|
|
52
|
+
cometapi?: OpenAICompatibleKeyVault;
|
|
52
53
|
deepseek?: OpenAICompatibleKeyVault;
|
|
53
54
|
fal?: FalKeyVault;
|
|
54
55
|
fireworksai?: OpenAICompatibleKeyVault;
|