@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/changelog/v1.json +21 -0
  3. package/docs/self-hosting/environment-variables/model-provider.mdx +2 -2
  4. package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +2 -2
  5. package/locales/ar/models.json +38 -11
  6. package/locales/bg-BG/models.json +38 -11
  7. package/locales/de-DE/models.json +38 -11
  8. package/locales/en-US/models.json +38 -11
  9. package/locales/es-ES/models.json +38 -11
  10. package/locales/fa-IR/models.json +38 -11
  11. package/locales/fr-FR/models.json +38 -11
  12. package/locales/it-IT/models.json +38 -11
  13. package/locales/ja-JP/models.json +38 -11
  14. package/locales/ko-KR/models.json +38 -11
  15. package/locales/nl-NL/models.json +38 -11
  16. package/locales/pl-PL/models.json +38 -11
  17. package/locales/pt-BR/models.json +38 -11
  18. package/locales/ru-RU/models.json +38 -11
  19. package/locales/tr-TR/models.json +38 -11
  20. package/locales/vi-VN/models.json +38 -11
  21. package/locales/zh-CN/image.json +3 -0
  22. package/locales/zh-CN/models.json +38 -11
  23. package/locales/zh-TW/models.json +38 -11
  24. package/package.json +3 -3
  25. package/packages/model-bank/package.json +1 -0
  26. package/packages/model-bank/src/aiModels/cometapi.ts +349 -0
  27. package/packages/model-bank/src/aiModels/fal.ts +46 -7
  28. package/packages/model-bank/src/aiModels/index.ts +3 -0
  29. package/packages/model-bank/src/aiModels/volcengine.ts +51 -21
  30. package/packages/model-bank/src/standard-parameters/index.ts +3 -0
  31. package/packages/model-runtime/src/cometapi/index.ts +49 -0
  32. package/packages/model-runtime/src/fal/index.test.ts +374 -0
  33. package/packages/model-runtime/src/fal/index.ts +23 -14
  34. package/packages/model-runtime/src/index.ts +1 -0
  35. package/packages/model-runtime/src/runtimeMap.ts +2 -0
  36. package/packages/model-runtime/src/types/type.ts +1 -0
  37. package/packages/model-runtime/src/volcengine/createImage.test.ts +522 -0
  38. package/packages/model-runtime/src/volcengine/createImage.ts +118 -0
  39. package/packages/model-runtime/src/volcengine/index.ts +2 -0
  40. package/packages/types/src/user/settings/keyVaults.ts +1 -0
  41. package/packages/utils/src/parseModels.test.ts +11 -8
  42. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/CfgSliderInput.tsx +11 -0
  43. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/StepsSliderInput.tsx +2 -2
  44. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +9 -0
  45. package/src/config/llm.ts +6 -0
  46. package/src/config/modelProviders/cometapi.ts +24 -0
  47. package/src/config/modelProviders/index.ts +3 -0
  48. package/src/features/ChatInput/ActionBar/index.tsx +19 -1
  49. package/src/features/ChatInput/Desktop/index.tsx +7 -0
  50. package/src/features/ChatInput/InputEditor/index.tsx +4 -6
  51. package/src/features/ChatInput/TypoBar/index.tsx +116 -103
  52. package/src/locales/default/image.ts +3 -0
  53. package/src/server/routers/async/image.ts +6 -1
  54. package/src/store/global/actions/workspacePane.ts +7 -0
  55. package/src/store/global/initialState.ts +2 -0
  56. 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;