@lobehub/lobehub 2.0.0-next.141 → 2.0.0-next.142

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 (24) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/Dockerfile +2 -0
  3. package/apps/desktop/src/main/controllers/__tests__/McpInstallCtr.test.ts +286 -0
  4. package/apps/desktop/src/main/controllers/__tests__/NotificationCtr.test.ts +347 -0
  5. package/apps/desktop/src/main/controllers/__tests__/RemoteServerConfigCtr.test.ts +645 -0
  6. package/apps/desktop/src/main/controllers/__tests__/RemoteServerSyncCtr.test.ts +372 -0
  7. package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +276 -0
  8. package/apps/desktop/src/main/controllers/__tests__/UploadFileCtr.test.ts +171 -0
  9. package/apps/desktop/src/main/core/browser/__tests__/Browser.test.ts +573 -0
  10. package/apps/desktop/src/main/core/browser/__tests__/BrowserManager.test.ts +415 -0
  11. package/apps/desktop/src/main/core/infrastructure/__tests__/I18nManager.test.ts +353 -0
  12. package/apps/desktop/src/main/core/infrastructure/__tests__/IoCContainer.test.ts +156 -0
  13. package/apps/desktop/src/main/core/infrastructure/__tests__/ProtocolManager.test.ts +348 -0
  14. package/apps/desktop/src/main/core/infrastructure/__tests__/StaticFileServerManager.test.ts +481 -0
  15. package/apps/desktop/src/main/core/infrastructure/__tests__/StoreManager.test.ts +164 -0
  16. package/apps/desktop/src/main/core/infrastructure/__tests__/UpdaterManager.test.ts +513 -0
  17. package/changelog/v1.json +9 -0
  18. package/docs/self-hosting/environment-variables/model-provider.mdx +31 -0
  19. package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +30 -0
  20. package/package.json +1 -1
  21. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +6 -3
  22. package/src/config/modelProviders/vertexai.ts +1 -1
  23. package/src/envs/llm.ts +4 -0
  24. package/src/server/modules/ModelRuntime/index.ts +4 -4
@@ -0,0 +1,513 @@
1
+ import { autoUpdater } from 'electron-updater';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import type { App as AppCore } from '../../App';
5
+ import { UpdaterManager } from '../UpdaterManager';
6
+
7
+ // Use vi.hoisted to ensure mocks work with require()
8
+ const { mockGetAllWindows, mockReleaseSingleInstanceLock } = vi.hoisted(() => ({
9
+ mockGetAllWindows: vi.fn().mockReturnValue([]),
10
+ mockReleaseSingleInstanceLock: vi.fn(),
11
+ }));
12
+
13
+ // Mock electron-log
14
+ vi.mock('electron-log', () => ({
15
+ default: {
16
+ transports: {
17
+ file: {
18
+ level: 'info',
19
+ getFile: vi.fn().mockReturnValue({ path: '/mock/log/path' }),
20
+ },
21
+ },
22
+ },
23
+ }));
24
+
25
+ // Mock electron-updater
26
+ vi.mock('electron-updater', () => ({
27
+ autoUpdater: {
28
+ allowDowngrade: false,
29
+ allowPrerelease: false,
30
+ autoDownload: false,
31
+ autoInstallOnAppQuit: false,
32
+ channel: 'stable',
33
+ checkForUpdates: vi.fn(),
34
+ downloadUpdate: vi.fn(),
35
+ forceDevUpdateConfig: false,
36
+ logger: null as any,
37
+ on: vi.fn(),
38
+ quitAndInstall: vi.fn(),
39
+ },
40
+ }));
41
+
42
+ // Mock electron - uses hoisted functions for require() compatibility
43
+ vi.mock('electron', () => ({
44
+ BrowserWindow: {
45
+ getAllWindows: mockGetAllWindows,
46
+ },
47
+ app: {
48
+ releaseSingleInstanceLock: mockReleaseSingleInstanceLock,
49
+ },
50
+ }));
51
+
52
+ // Mock logger
53
+ vi.mock('@/utils/logger', () => ({
54
+ createLogger: () => ({
55
+ debug: vi.fn(),
56
+ error: vi.fn(),
57
+ info: vi.fn(),
58
+ warn: vi.fn(),
59
+ }),
60
+ }));
61
+
62
+ // Mock updater configs
63
+ vi.mock('@/modules/updater/configs', () => ({
64
+ UPDATE_CHANNEL: 'stable',
65
+ updaterConfig: {
66
+ app: {
67
+ autoCheckUpdate: false,
68
+ autoDownloadUpdate: true,
69
+ checkUpdateInterval: 60 * 60 * 1000,
70
+ },
71
+ enableAppUpdate: true,
72
+ enableRenderHotUpdate: true,
73
+ },
74
+ }));
75
+
76
+ // Mock isDev
77
+ vi.mock('@/const/env', () => ({
78
+ isDev: false,
79
+ }));
80
+
81
+ describe('UpdaterManager', () => {
82
+ let updaterManager: UpdaterManager;
83
+ let mockApp: AppCore;
84
+ let mockBroadcast: ReturnType<typeof vi.fn>;
85
+ let registeredEvents: Map<string, (...args: any[]) => void>;
86
+
87
+ beforeEach(() => {
88
+ vi.clearAllMocks();
89
+ vi.useFakeTimers();
90
+
91
+ // Reset autoUpdater state
92
+ (autoUpdater as any).autoDownload = false;
93
+ (autoUpdater as any).autoInstallOnAppQuit = false;
94
+ (autoUpdater as any).channel = 'stable';
95
+ (autoUpdater as any).allowPrerelease = false;
96
+ (autoUpdater as any).allowDowngrade = false;
97
+ (autoUpdater as any).forceDevUpdateConfig = false;
98
+
99
+ // Capture registered events
100
+ registeredEvents = new Map();
101
+ vi.mocked(autoUpdater.on).mockImplementation((event: string, handler: any) => {
102
+ registeredEvents.set(event, handler);
103
+ return autoUpdater;
104
+ });
105
+
106
+ // Mock broadcast function
107
+ mockBroadcast = vi.fn();
108
+
109
+ // Create mock App
110
+ mockApp = {
111
+ browserManager: {
112
+ getMainWindow: vi.fn().mockReturnValue({
113
+ broadcast: mockBroadcast,
114
+ }),
115
+ },
116
+ isQuiting: false,
117
+ } as unknown as AppCore;
118
+
119
+ updaterManager = new UpdaterManager(mockApp);
120
+ });
121
+
122
+ afterEach(() => {
123
+ vi.useRealTimers();
124
+ });
125
+
126
+ describe('constructor', () => {
127
+ it('should set up electron-log for autoUpdater', () => {
128
+ expect(autoUpdater.logger).not.toBeNull();
129
+ });
130
+ });
131
+
132
+ describe('initialize', () => {
133
+ it('should configure autoUpdater properties', async () => {
134
+ await updaterManager.initialize();
135
+
136
+ expect(autoUpdater.autoDownload).toBe(false);
137
+ expect(autoUpdater.autoInstallOnAppQuit).toBe(false);
138
+ expect(autoUpdater.channel).toBe('stable');
139
+ expect(autoUpdater.allowPrerelease).toBe(false);
140
+ expect(autoUpdater.allowDowngrade).toBe(false);
141
+ });
142
+
143
+ it('should register all event listeners', async () => {
144
+ await updaterManager.initialize();
145
+
146
+ expect(autoUpdater.on).toHaveBeenCalledWith('checking-for-update', expect.any(Function));
147
+ expect(autoUpdater.on).toHaveBeenCalledWith('update-available', expect.any(Function));
148
+ expect(autoUpdater.on).toHaveBeenCalledWith('update-not-available', expect.any(Function));
149
+ expect(autoUpdater.on).toHaveBeenCalledWith('error', expect.any(Function));
150
+ expect(autoUpdater.on).toHaveBeenCalledWith('download-progress', expect.any(Function));
151
+ expect(autoUpdater.on).toHaveBeenCalledWith('update-downloaded', expect.any(Function));
152
+ });
153
+ });
154
+
155
+ describe('checkForUpdates', () => {
156
+ beforeEach(async () => {
157
+ await updaterManager.initialize();
158
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
159
+ });
160
+
161
+ it('should call autoUpdater.checkForUpdates', async () => {
162
+ await updaterManager.checkForUpdates();
163
+
164
+ expect(autoUpdater.checkForUpdates).toHaveBeenCalled();
165
+ });
166
+
167
+ it('should broadcast manualUpdateCheckStart when manual check', async () => {
168
+ await updaterManager.checkForUpdates({ manual: true });
169
+
170
+ expect(mockBroadcast).toHaveBeenCalledWith('manualUpdateCheckStart');
171
+ });
172
+
173
+ it('should not broadcast when auto check', async () => {
174
+ await updaterManager.checkForUpdates({ manual: false });
175
+
176
+ expect(mockBroadcast).not.toHaveBeenCalledWith('manualUpdateCheckStart');
177
+ });
178
+
179
+ it('should ignore duplicate check requests while checking', async () => {
180
+ // Start first check but don't resolve
181
+ vi.mocked(autoUpdater.checkForUpdates).mockImplementation(
182
+ () => new Promise((resolve) => setTimeout(resolve, 1000)) as any,
183
+ );
184
+
185
+ const firstCheck = updaterManager.checkForUpdates();
186
+ const secondCheck = updaterManager.checkForUpdates();
187
+
188
+ await vi.advanceTimersByTimeAsync(1000);
189
+ await Promise.all([firstCheck, secondCheck]);
190
+
191
+ expect(autoUpdater.checkForUpdates).toHaveBeenCalledTimes(1);
192
+ });
193
+
194
+ it('should broadcast updateError when check fails during manual check', async () => {
195
+ const error = new Error('Network error');
196
+ vi.mocked(autoUpdater.checkForUpdates).mockRejectedValue(error);
197
+
198
+ await updaterManager.checkForUpdates({ manual: true });
199
+
200
+ expect(mockBroadcast).toHaveBeenCalledWith('updateError', 'Network error');
201
+ });
202
+ });
203
+
204
+ describe('downloadUpdate', () => {
205
+ beforeEach(async () => {
206
+ await updaterManager.initialize();
207
+ vi.mocked(autoUpdater.downloadUpdate).mockResolvedValue([] as any);
208
+
209
+ // Simulate update available
210
+ const updateAvailableHandler = registeredEvents.get('update-available');
211
+ updateAvailableHandler?.({ version: '2.0.0' });
212
+ });
213
+
214
+ it('should call autoUpdater.downloadUpdate', async () => {
215
+ await updaterManager.downloadUpdate();
216
+
217
+ expect(autoUpdater.downloadUpdate).toHaveBeenCalled();
218
+ });
219
+
220
+ it('should ignore download request when no update available', async () => {
221
+ // Create fresh manager without update available
222
+ const freshManager = new UpdaterManager(mockApp);
223
+ await freshManager.initialize();
224
+
225
+ await freshManager.downloadUpdate();
226
+
227
+ // Reset call count since downloadUpdate might have been called in beforeEach
228
+ vi.mocked(autoUpdater.downloadUpdate).mockClear();
229
+ await freshManager.downloadUpdate();
230
+
231
+ // downloadUpdate should not be called on autoUpdater for fresh manager
232
+ expect(autoUpdater.downloadUpdate).not.toHaveBeenCalled();
233
+ });
234
+
235
+ it('should ignore duplicate download requests while downloading', async () => {
236
+ vi.mocked(autoUpdater.downloadUpdate).mockImplementation(
237
+ () => new Promise((resolve) => setTimeout(resolve, 1000)) as any,
238
+ );
239
+
240
+ const firstDownload = updaterManager.downloadUpdate();
241
+ const secondDownload = updaterManager.downloadUpdate();
242
+
243
+ await vi.advanceTimersByTimeAsync(1000);
244
+ await Promise.all([firstDownload, secondDownload]);
245
+
246
+ expect(autoUpdater.downloadUpdate).toHaveBeenCalledTimes(1);
247
+ });
248
+
249
+ it('should broadcast updateDownloadStart when isManualCheck is true', async () => {
250
+ // Create a fresh manager to avoid state pollution from beforeEach
251
+ const freshManager = new UpdaterManager(mockApp);
252
+
253
+ // Setup fresh event capture
254
+ const freshEvents = new Map<string, (...args: any[]) => void>();
255
+ vi.mocked(autoUpdater.on).mockImplementation((event: string, handler: any) => {
256
+ freshEvents.set(event, handler);
257
+ return autoUpdater;
258
+ });
259
+ await freshManager.initialize();
260
+
261
+ // Trigger a manual check to set isManualCheck = true
262
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
263
+ await freshManager.checkForUpdates({ manual: true });
264
+
265
+ // Manually set updateAvailable without triggering auto-download
266
+ // Access private property to set state
267
+ (freshManager as any).updateAvailable = true;
268
+
269
+ // Clear previous broadcast calls
270
+ mockBroadcast.mockClear();
271
+
272
+ // Now download should broadcast updateDownloadStart because isManualCheck is true
273
+ vi.mocked(autoUpdater.downloadUpdate).mockResolvedValue([] as any);
274
+ await freshManager.downloadUpdate();
275
+
276
+ expect(mockBroadcast).toHaveBeenCalledWith('updateDownloadStart');
277
+ });
278
+
279
+ it('should broadcast updateError when download fails with isManualCheck true', async () => {
280
+ // Create a fresh manager to avoid state pollution from beforeEach
281
+ const freshManager = new UpdaterManager(mockApp);
282
+
283
+ // Setup fresh event capture
284
+ const freshEvents = new Map<string, (...args: any[]) => void>();
285
+ vi.mocked(autoUpdater.on).mockImplementation((event: string, handler: any) => {
286
+ freshEvents.set(event, handler);
287
+ return autoUpdater;
288
+ });
289
+ await freshManager.initialize();
290
+
291
+ // Trigger a manual check to set isManualCheck = true
292
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
293
+ await freshManager.checkForUpdates({ manual: true });
294
+
295
+ // Manually set updateAvailable without triggering auto-download
296
+ (freshManager as any).updateAvailable = true;
297
+
298
+ // Clear previous broadcast calls
299
+ mockBroadcast.mockClear();
300
+
301
+ // Setup error
302
+ const error = new Error('Download failed');
303
+ vi.mocked(autoUpdater.downloadUpdate).mockRejectedValue(error);
304
+
305
+ await freshManager.downloadUpdate();
306
+
307
+ expect(mockBroadcast).toHaveBeenCalledWith('updateError', 'Download failed');
308
+ });
309
+ });
310
+
311
+ describe('installNow', () => {
312
+ // Note: installNow uses require('electron') which is difficult to mock in vitest.
313
+ // These tests are skipped because vi.mock doesn't work with dynamic require().
314
+ // The functionality should be tested in integration tests or E2E tests.
315
+
316
+ it.skip('should set app.isQuiting to true', () => {
317
+ updaterManager.installNow();
318
+ expect(mockApp.isQuiting).toBe(true);
319
+ });
320
+
321
+ it.skip('should close all windows', () => {
322
+ const mockWindow1 = { close: vi.fn(), isDestroyed: vi.fn().mockReturnValue(false) };
323
+ const mockWindow2 = { close: vi.fn(), isDestroyed: vi.fn().mockReturnValue(false) };
324
+ mockGetAllWindows.mockReturnValue([mockWindow1, mockWindow2]);
325
+ updaterManager.installNow();
326
+ expect(mockWindow1.close).toHaveBeenCalled();
327
+ expect(mockWindow2.close).toHaveBeenCalled();
328
+ });
329
+
330
+ it.skip('should not close destroyed windows', () => {
331
+ const mockWindow = { close: vi.fn(), isDestroyed: vi.fn().mockReturnValue(true) };
332
+ mockGetAllWindows.mockReturnValue([mockWindow]);
333
+ updaterManager.installNow();
334
+ expect(mockWindow.close).not.toHaveBeenCalled();
335
+ });
336
+
337
+ it.skip('should release single instance lock', () => {
338
+ updaterManager.installNow();
339
+ expect(mockReleaseSingleInstanceLock).toHaveBeenCalled();
340
+ });
341
+
342
+ it.skip('should call quitAndInstall with correct parameters after delay', async () => {
343
+ updaterManager.installNow();
344
+ expect(autoUpdater.quitAndInstall).not.toHaveBeenCalled();
345
+ await vi.advanceTimersByTimeAsync(100);
346
+ expect(autoUpdater.quitAndInstall).toHaveBeenCalledWith(true, true);
347
+ });
348
+ });
349
+
350
+ describe('installLater', () => {
351
+ it('should set autoInstallOnAppQuit to true', () => {
352
+ updaterManager.installLater();
353
+
354
+ expect(autoUpdater.autoInstallOnAppQuit).toBe(true);
355
+ });
356
+
357
+ it('should broadcast updateWillInstallLater', () => {
358
+ updaterManager.installLater();
359
+
360
+ expect(mockBroadcast).toHaveBeenCalledWith('updateWillInstallLater');
361
+ });
362
+ });
363
+
364
+ describe('event handlers', () => {
365
+ beforeEach(async () => {
366
+ await updaterManager.initialize();
367
+ });
368
+
369
+ describe('update-available', () => {
370
+ it('should broadcast manualUpdateAvailable when manual check', async () => {
371
+ // Trigger manual check first
372
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
373
+ await updaterManager.checkForUpdates({ manual: true });
374
+
375
+ const updateInfo = { version: '2.0.0' };
376
+ const handler = registeredEvents.get('update-available');
377
+ handler?.(updateInfo);
378
+
379
+ expect(mockBroadcast).toHaveBeenCalledWith('manualUpdateAvailable', updateInfo);
380
+ });
381
+
382
+ it('should auto download when auto check finds update', async () => {
383
+ // Trigger auto check first
384
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
385
+ await updaterManager.checkForUpdates({ manual: false });
386
+
387
+ vi.mocked(autoUpdater.downloadUpdate).mockResolvedValue([] as any);
388
+
389
+ const handler = registeredEvents.get('update-available');
390
+ handler?.({ version: '2.0.0' });
391
+
392
+ expect(autoUpdater.downloadUpdate).toHaveBeenCalled();
393
+ });
394
+ });
395
+
396
+ describe('update-not-available', () => {
397
+ it('should broadcast manualUpdateNotAvailable when manual check', async () => {
398
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
399
+ await updaterManager.checkForUpdates({ manual: true });
400
+
401
+ const info = { version: '1.0.0' };
402
+ const handler = registeredEvents.get('update-not-available');
403
+ handler?.(info);
404
+
405
+ expect(mockBroadcast).toHaveBeenCalledWith('manualUpdateNotAvailable', info);
406
+ });
407
+
408
+ it('should not broadcast when auto check', async () => {
409
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
410
+ await updaterManager.checkForUpdates({ manual: false });
411
+
412
+ const handler = registeredEvents.get('update-not-available');
413
+ handler?.({ version: '1.0.0' });
414
+
415
+ expect(mockBroadcast).not.toHaveBeenCalledWith(
416
+ 'manualUpdateNotAvailable',
417
+ expect.anything(),
418
+ );
419
+ });
420
+ });
421
+
422
+ describe('download-progress', () => {
423
+ it('should broadcast progress when manual check', async () => {
424
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
425
+ await updaterManager.checkForUpdates({ manual: true });
426
+
427
+ const progressObj = {
428
+ bytesPerSecond: 1024,
429
+ percent: 50,
430
+ total: 1024 * 1024,
431
+ transferred: 512 * 1024,
432
+ };
433
+ const handler = registeredEvents.get('download-progress');
434
+ handler?.(progressObj);
435
+
436
+ expect(mockBroadcast).toHaveBeenCalledWith('updateDownloadProgress', progressObj);
437
+ });
438
+ });
439
+
440
+ describe('update-downloaded', () => {
441
+ it('should broadcast updateDownloaded', async () => {
442
+ await updaterManager.initialize();
443
+
444
+ const info = { version: '2.0.0' };
445
+ const handler = registeredEvents.get('update-downloaded');
446
+ handler?.(info);
447
+
448
+ expect(mockBroadcast).toHaveBeenCalledWith('updateDownloaded', info);
449
+ });
450
+ });
451
+
452
+ describe('error', () => {
453
+ it('should broadcast updateError when manual check', async () => {
454
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
455
+ await updaterManager.checkForUpdates({ manual: true });
456
+
457
+ const error = new Error('Update error');
458
+ const handler = registeredEvents.get('error');
459
+ handler?.(error);
460
+
461
+ expect(mockBroadcast).toHaveBeenCalledWith('updateError', 'Update error');
462
+ });
463
+
464
+ it('should not broadcast when auto check', async () => {
465
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
466
+ await updaterManager.checkForUpdates({ manual: false });
467
+
468
+ const error = new Error('Update error');
469
+ const handler = registeredEvents.get('error');
470
+ handler?.(error);
471
+
472
+ expect(mockBroadcast).not.toHaveBeenCalledWith('updateError', expect.anything());
473
+ });
474
+ });
475
+ });
476
+
477
+ describe('simulation methods (dev mode)', () => {
478
+ it('simulateUpdateAvailable should do nothing when not in dev mode', () => {
479
+ // Current mock has isDev = false
480
+ updaterManager.simulateUpdateAvailable();
481
+
482
+ // Should not broadcast anything since isDev is false
483
+ expect(mockBroadcast).not.toHaveBeenCalledWith(
484
+ 'manualUpdateAvailable',
485
+ expect.objectContaining({ version: '1.0.0' }),
486
+ );
487
+ });
488
+
489
+ it('simulateUpdateDownloaded should do nothing when not in dev mode', () => {
490
+ updaterManager.simulateUpdateDownloaded();
491
+
492
+ expect(mockBroadcast).not.toHaveBeenCalledWith(
493
+ 'updateDownloaded',
494
+ expect.objectContaining({ version: '1.0.0' }),
495
+ );
496
+ });
497
+
498
+ it('simulateDownloadProgress should do nothing when not in dev mode', () => {
499
+ updaterManager.simulateDownloadProgress();
500
+
501
+ expect(mockBroadcast).not.toHaveBeenCalledWith('updateDownloadStart');
502
+ });
503
+ });
504
+
505
+ describe('mainWindow getter', () => {
506
+ it('should return main window from browserManager', () => {
507
+ const mainWindow = updaterManager['mainWindow'];
508
+
509
+ expect(mockApp.browserManager.getMainWindow).toHaveBeenCalled();
510
+ expect(mainWindow.broadcast).toBe(mockBroadcast);
511
+ });
512
+ });
513
+ });
package/changelog/v1.json CHANGED
@@ -1,4 +1,13 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Remove internal apiMode param from chat completion API requests."
6
+ ]
7
+ },
8
+ "date": "2025-12-01",
9
+ "version": "2.0.0-next.142"
10
+ },
2
11
  {
3
12
  "children": {
4
13
  "fixes": [
@@ -121,6 +121,37 @@ If you need to use Azure OpenAI to provide model services, you can refer to the
121
121
  - Default: `-`
122
122
  - Example: `-all,+gemini-1.5-flash-latest,+gemini-1.5-pro-latest`
123
123
 
124
+ ## Vertex AI
125
+
126
+ ### `VERTEXAI_CREDENTIALS`
127
+
128
+ - Type: Required
129
+ - Description: A JSON string of your Google Cloud service account key, you can get the key from [here](/docs/usage/providers/vertexai).
130
+ - Default: -
131
+ - Example: `{"type": "service_account", "project_id": "your-gcp-project-id", ...}`
132
+
133
+ ### `VERTEXAI_PROJECT`
134
+
135
+ - Type: Optional
136
+ - Description: Your Google Cloud project ID. If not set, it will be obtained from the `project_id` field in `VERTEXAI_CREDENTIALS`.
137
+ - Default: -
138
+ - Example: `your-gcp-project-id`
139
+
140
+ ### `VERTEXAI_LOCATION`
141
+
142
+ - Type: Optional
143
+ - Description: The region where your Vertex AI model is located.
144
+ - Default: `global`
145
+ - Example: `us-central1`
146
+
147
+
148
+ ### `VERTEXAI_MODEL_LIST`
149
+
150
+ - Type: Optional
151
+ - Description: Used to control the model list, use `+` to add a model, use `-` to hide a model, use `model_name=display_name` to customize the display name of a model, separated by commas. Definition syntax rules see [model-list][model-list]
152
+ - Default: `-`
153
+ - Example: `-all,+gemini-1.5-flash-latest,+gemini-1.5-pro-latest`
154
+
124
155
  ## Anthropic AI
125
156
 
126
157
  ### `ANTHROPIC_API_KEY`
@@ -119,6 +119,36 @@ LobeChat 在部署时提供了丰富的模型服务商相关的环境变量,
119
119
  - 默认值:`-`
120
120
  - 示例:`-all,+gemini-1.5-flash-latest,+gemini-1.5-pro-latest`
121
121
 
122
+ ## Vertex AI
123
+
124
+ ### `VERTEXAI_CREDENTIALS`
125
+
126
+ - 类型:必选
127
+ - 描述:Google Cloud 服务账号密钥的 JSON 字符串。用于认证和授权访问 Vertex AI 服务,获取方法请参考 [这里](/zh/docs/usage/providers/vertexai)
128
+ - 默认值:-
129
+ - 示例:`{"type": "service_account", "project_id": "your-gcp-project-id", ...}`
130
+
131
+ ### `VERTEXAI_PROJECT`
132
+
133
+ - 类型:可选
134
+ - 描述:你的 Google Cloud 项目 ID。如果未设置,将从 `VERTEXAI_CREDENTIALS` 中的 `project_id` 字段获取。
135
+ - 默认值:-
136
+ - 示例:`your-gcp-project-id`
137
+
138
+ ### `VERTEXAI_LOCATION`
139
+
140
+ - 类型:可选
141
+ - 描述:你的 Vertex AI 模型所在的区域。
142
+ - 默认值:`global`
143
+ - 示例:`us-central1`
144
+
145
+ ### `VERTEXAI_MODEL_LIST`
146
+
147
+ - 类型:可选
148
+ - 描述:用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名<扩展配置>` 来自定义模型的展示名,用英文逗号隔开。模型定义语法规则见 [模型列表][model-list]
149
+ - 默认值:`-`
150
+ - 示例:`-all,+gemini-1.5-flash-latest,+gemini-1.5-pro-latest`
151
+
122
152
  ## Anthropic AI
123
153
 
124
154
  ### `ANTHROPIC_API_KEY`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.141",
3
+ "version": "2.0.0-next.142",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -368,8 +368,11 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
368
368
  this,
369
369
  ) as any;
370
370
  } else {
371
+ // Remove internal apiMode parameter before sending to API
372
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
373
+ const { apiMode: _, ...cleanedPayload } = postPayload as any;
371
374
  const finalPayload = {
372
- ...postPayload,
375
+ ...cleanedPayload,
373
376
  messages,
374
377
  ...(chatCompletion?.noUserId ? {} : { user: options?.user }),
375
378
  stream_options:
@@ -385,11 +388,11 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
385
388
  console.log(JSON.stringify(finalPayload), '\n');
386
389
  }
387
390
 
388
- response = await this.client.chat.completions.create(finalPayload, {
391
+ response = (await this.client.chat.completions.create(finalPayload, {
389
392
  // https://github.com/lobehub/lobe-chat/pull/318
390
393
  headers: { Accept: '*/*', ...options?.requestHeaders },
391
394
  signal: options?.signal,
392
- });
395
+ })) as unknown as Stream<OpenAI.Chat.Completions.ChatCompletionChunk>;
393
396
  }
394
397
 
395
398
  if (postPayload.stream) {
@@ -3,7 +3,7 @@ import { ModelProviderCard } from '@/types/llm';
3
3
  // ref: https://ai.google.dev/gemini-api/docs/models/gemini
4
4
  const VertexAI: ModelProviderCard = {
5
5
  chatModels: [],
6
- checkModel: 'gemini-1.5-flash-001',
6
+ checkModel: 'gemini-2.0-flash',
7
7
  description:
8
8
  'Google 的 Gemini 系列是其最先进、通用的 AI模型,由 Google DeepMind 打造,专为多模态设计,支持文本、代码、图像、音频和视频的无缝理解与处理。适用于从数据中心到移动设备的多种环境,极大提升了AI模型的效率与应用广泛性。',
9
9
  id: 'vertexai',
package/src/envs/llm.ts CHANGED
@@ -28,6 +28,8 @@ export const getLLMConfig = () => {
28
28
  ENABLED_GOOGLE: z.boolean(),
29
29
  GOOGLE_API_KEY: z.string().optional(),
30
30
 
31
+ ENABLED_VERTEXAI: z.boolean(),
32
+
31
33
  ENABLED_MOONSHOT: z.boolean(),
32
34
  MOONSHOT_API_KEY: z.string().optional(),
33
35
 
@@ -237,6 +239,8 @@ export const getLLMConfig = () => {
237
239
  ENABLED_GOOGLE: !!process.env.GOOGLE_API_KEY,
238
240
  GOOGLE_API_KEY: process.env.GOOGLE_API_KEY,
239
241
 
242
+ ENABLED_VERTEXAI: !!process.env.VERTEXAI_CREDENTIALS,
243
+
240
244
  ENABLED_VOLCENGINE: !!process.env.VOLCENGINE_API_KEY,
241
245
  VOLCENGINE_API_KEY: process.env.VOLCENGINE_API_KEY,
242
246
 
@@ -162,18 +162,18 @@ const buildVertexOptions = (
162
162
  payload: ClientSecretPayload,
163
163
  params: Partial<GoogleGenAIOptions> = {},
164
164
  ): GoogleGenAIOptions => {
165
- const rawCredentials = payload.apiKey ?? process.env.VERTEXAI_CREDENTIALS ?? '';
165
+ const rawCredentials = payload.apiKey || process.env.VERTEXAI_CREDENTIALS || '';
166
166
  const credentials = safeParseJSON<Record<string, string>>(rawCredentials);
167
167
 
168
168
  const projectFromParams = params.project as string | undefined;
169
169
  const projectFromCredentials = credentials?.project_id;
170
170
  const projectFromEnv = process.env.VERTEXAI_PROJECT;
171
171
 
172
- const project = projectFromParams ?? projectFromCredentials ?? projectFromEnv;
172
+ const project = projectFromParams || projectFromCredentials || projectFromEnv;
173
173
  const location =
174
- (params.location as string | undefined) ?? payload.vertexAIRegion ?? process.env.VERTEXAI_LOCATION ?? undefined;
174
+ (params.location as string | undefined) || payload.vertexAIRegion || process.env.VERTEXAI_LOCATION || undefined;
175
175
 
176
- const googleAuthOptions = params.googleAuthOptions ?? (credentials ? { credentials } : undefined);
176
+ const googleAuthOptions = params.googleAuthOptions || (credentials ? { credentials } : undefined);
177
177
 
178
178
  const options: GoogleGenAIOptions = {
179
179
  ...params,