@lobehub/chat 1.137.7 → 1.137.8

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 (40) hide show
  1. package/.cursor/rules/testing-guide/zustand-store-action-test.mdc +579 -0
  2. package/.github/workflows/e2e.yml +1 -1
  3. package/CHANGELOG.md +33 -0
  4. package/changelog/v1.json +12 -0
  5. package/locales/en-US/modelProvider.json +5 -0
  6. package/locales/zh-CN/modelProvider.json +5 -0
  7. package/package.json +1 -1
  8. package/packages/context-engine/src/tools/ToolsEngine.ts +3 -3
  9. package/packages/context-engine/src/tools/__tests__/ToolNameResolver.test.ts +3 -0
  10. package/packages/context-engine/src/tools/__tests__/ToolsEngine.test.ts +79 -0
  11. package/packages/types/src/auth.ts +2 -0
  12. package/packages/types/src/user/settings/keyVaults.ts +6 -1
  13. package/src/app/[variants]/(main)/settings/provider/detail/vertexai/index.tsx +64 -1
  14. package/src/features/ChatInput/ActionBar/index.tsx +9 -1
  15. package/src/features/ChatInput/InputEditor/index.tsx +0 -2
  16. package/src/features/ChatInput/TypoBar/index.tsx +1 -9
  17. package/src/libs/trpc/lambda/context.ts +3 -1
  18. package/src/locales/default/modelProvider.ts +5 -0
  19. package/src/server/modules/ModelRuntime/index.ts +1 -1
  20. package/src/services/_auth.ts +8 -2
  21. package/src/store/aiInfra/slices/aiModel/action.test.ts +595 -0
  22. package/src/store/chat/slices/thread/action.test.ts +1099 -0
  23. package/src/store/discover/slices/assistant/action.test.ts +228 -0
  24. package/src/store/discover/slices/mcp/action.test.ts +130 -0
  25. package/src/store/discover/slices/model/action.test.ts +253 -0
  26. package/src/store/discover/slices/plugin/action.test.ts +149 -0
  27. package/src/store/discover/slices/provider/action.test.ts +279 -0
  28. package/src/store/file/slices/chat/action.test.ts +1 -8
  29. package/src/store/file/slices/chunk/action.test.ts +478 -0
  30. package/src/store/file/slices/fileManager/action.test.ts +687 -0
  31. package/src/store/file/slices/tts/action.test.ts +5 -17
  32. package/src/store/file/slices/upload/action.test.ts +706 -0
  33. package/src/store/knowledgeBase/slices/content/action.test.ts +292 -0
  34. package/src/store/knowledgeBase/slices/crud/action.test.ts +466 -0
  35. package/src/store/serverConfig/action.test.ts +166 -0
  36. package/src/store/serverConfig/selectors.test.ts +0 -2
  37. package/src/store/test-coverage.md +593 -0
  38. package/src/store/tool/slices/mcpStore/action.test.ts +1146 -0
  39. package/src/store/tool/slices/oldStore/action.test.ts +13 -46
  40. package/src/store/tool/slices/oldStore/action.ts +0 -2
@@ -0,0 +1,579 @@
1
+ ---
2
+ description: Best practices for testing Zustand store actions
3
+ globs: "src/store/**/*.test.ts"
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Zustand Store Action Testing Guide
8
+
9
+ This guide provides best practices for testing Zustand store actions, based on our proven testing patterns.
10
+
11
+ ## Basic Test Structure
12
+
13
+ ```typescript
14
+ import { act, renderHook } from '@testing-library/react';
15
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
16
+
17
+ import { messageService } from '@/services/message';
18
+ import { useChatStore } from '../../store';
19
+
20
+ // Keep zustand mock as it's needed globally
21
+ vi.mock('zustand/traditional');
22
+
23
+ beforeEach(() => {
24
+ // Reset store state
25
+ vi.clearAllMocks();
26
+ useChatStore.setState(
27
+ {
28
+ activeId: 'test-session-id',
29
+ messagesMap: {},
30
+ loadingIds: [],
31
+ },
32
+ false,
33
+ );
34
+
35
+ // ✅ Setup only spies that MOST tests need
36
+ vi.spyOn(messageService, 'createMessage').mockResolvedValue('new-message-id');
37
+ // ❌ Don't setup spies that only few tests need - spy only when needed
38
+
39
+ // Setup common mock methods
40
+ act(() => {
41
+ useChatStore.setState({
42
+ refreshMessages: vi.fn(),
43
+ internal_coreProcessMessage: vi.fn(),
44
+ });
45
+ });
46
+ });
47
+
48
+ afterEach(() => {
49
+ vi.restoreAllMocks();
50
+ });
51
+
52
+ describe('action name', () => {
53
+ describe('validation', () => {
54
+ // Validation tests
55
+ });
56
+
57
+ describe('normal flow', () => {
58
+ // Happy path tests
59
+ });
60
+
61
+ describe('error handling', () => {
62
+ // Error case tests
63
+ });
64
+ });
65
+ ```
66
+
67
+ ## Testing Best Practices
68
+
69
+ ### 1. Test Layering - Spy Direct Dependencies Only
70
+
71
+ ✅ **Good**: Spy on the direct dependency
72
+
73
+ ```typescript
74
+ // When testing internal_coreProcessMessage, spy its direct dependency
75
+ const fetchAIChatSpy = vi
76
+ .spyOn(result.current, 'internal_fetchAIChatMessage')
77
+ .mockResolvedValue({ isFunctionCall: false, content: 'AI response' });
78
+ ```
79
+
80
+ ❌ **Bad**: Spy on lower-level implementation details
81
+
82
+ ```typescript
83
+ // Don't spy on services that internal_fetchAIChatMessage uses
84
+ const streamSpy = vi
85
+ .spyOn(chatService, 'createAssistantMessageStream')
86
+ .mockImplementation(...);
87
+ ```
88
+
89
+ **Why**: Each test should only mock its direct dependencies, not the entire call chain. This makes tests more maintainable and less brittle.
90
+
91
+ ### 2. Mock Management - Minimize Global Spies
92
+
93
+ ✅ **Good**: Spy only when needed
94
+
95
+ ```typescript
96
+ beforeEach(() => {
97
+ // ✅ Only spy services that most tests need
98
+ vi.spyOn(messageService, 'createMessage').mockResolvedValue('new-message-id');
99
+ // ✅ Don't spy chatService globally
100
+ });
101
+
102
+ it('should process message', async () => {
103
+ // ✅ Spy chatService only in tests that need it
104
+ const streamSpy = vi.spyOn(chatService, 'createAssistantMessageStream')
105
+ .mockImplementation(...);
106
+
107
+ // test logic
108
+
109
+ streamSpy.mockRestore();
110
+ });
111
+ ```
112
+
113
+ ❌ **Bad**: Setup all spies globally
114
+
115
+ ```typescript
116
+ beforeEach(() => {
117
+ vi.spyOn(messageService, 'createMessage').mockResolvedValue('id');
118
+ vi.spyOn(chatService, 'createAssistantMessageStream').mockResolvedValue({}); // ❌ Not all tests need this
119
+ vi.spyOn(fileService, 'uploadFile').mockResolvedValue({}); // ❌ Creates implicit coupling
120
+ });
121
+ ```
122
+
123
+ ### 3. Service Mocking - Mock the Correct Layer
124
+
125
+ ✅ **Good**: Mock the service method
126
+
127
+ ```typescript
128
+ it('should fetch AI chat response', async () => {
129
+ const streamSpy = vi
130
+ .spyOn(chatService, 'createAssistantMessageStream')
131
+ .mockImplementation(async ({ onMessageHandle, onFinish }) => {
132
+ await onMessageHandle?.({ type: 'text', text: 'Hello' } as any);
133
+ await onFinish?.('Hello', {});
134
+ });
135
+
136
+ // test logic
137
+ });
138
+ ```
139
+
140
+ ❌ **Bad**: Mock global fetch
141
+
142
+ ```typescript
143
+ it('should fetch AI chat response', async () => {
144
+ global.fetch = vi.fn().mockResolvedValue(...); // ❌ Too low level
145
+ });
146
+ ```
147
+
148
+ ### 4. Test Organization - Use Descriptive Nesting
149
+
150
+ ✅ **Good**: Clear nested structure
151
+
152
+ ```typescript
153
+ describe('sendMessage', () => {
154
+ describe('validation', () => {
155
+ it('should not send when session is inactive', async () => {});
156
+ it('should not send when message is empty', async () => {});
157
+ });
158
+
159
+ describe('message creation', () => {
160
+ it('should create user message and trigger AI processing', async () => {});
161
+ it('should send message with files attached', async () => {});
162
+ });
163
+
164
+ describe('error handling', () => {
165
+ it('should handle message creation errors gracefully', async () => {});
166
+ });
167
+ });
168
+ ```
169
+
170
+ ❌ **Bad**: Flat structure
171
+
172
+ ```typescript
173
+ describe('sendMessage', () => {
174
+ it('test 1', async () => {});
175
+ it('test 2', async () => {});
176
+ it('test 3', async () => {});
177
+ });
178
+ ```
179
+
180
+ ### 5. Testing Async Actions
181
+
182
+ Always wrap async operations in `act()`:
183
+
184
+ ```typescript
185
+ it('should send message', async () => {
186
+ const { result } = renderHook(() => useChatStore());
187
+
188
+ await act(async () => {
189
+ await result.current.sendMessage({ message: 'Hello' });
190
+ });
191
+
192
+ expect(messageService.createMessage).toHaveBeenCalled();
193
+ });
194
+ ```
195
+
196
+ ### 6. State Setup - Use act() for setState
197
+
198
+ ```typescript
199
+ it('should handle disabled state', async () => {
200
+ act(() => {
201
+ useChatStore.setState({ activeId: undefined });
202
+ });
203
+
204
+ const { result } = renderHook(() => useChatStore());
205
+ // test logic
206
+ });
207
+ ```
208
+
209
+ ### 7. Testing Complex Flows
210
+
211
+ For complex flows with multiple steps, use clear spy setup:
212
+
213
+ ```typescript
214
+ it('should handle topic creation flow', async () => {
215
+ // Setup store state
216
+ act(() => {
217
+ useChatStore.setState({
218
+ activeTopicId: undefined,
219
+ messagesMap: {
220
+ 'test-session-id': [
221
+ { id: 'msg-1', role: 'user', content: 'Message 1' },
222
+ { id: 'msg-2', role: 'assistant', content: 'Response 1' },
223
+ { id: 'msg-3', role: 'user', content: 'Message 2' },
224
+ ],
225
+ },
226
+ });
227
+ });
228
+
229
+ const { result } = renderHook(() => useChatStore());
230
+
231
+ // Spy on action dependencies
232
+ const createTopicSpy = vi.spyOn(result.current, 'createTopic')
233
+ .mockResolvedValue('new-topic-id');
234
+ const toggleLoadingSpy = vi.spyOn(result.current, 'internal_toggleMessageLoading');
235
+
236
+ // Execute
237
+ await act(async () => {
238
+ await result.current.sendMessage({ message: 'Test message' });
239
+ });
240
+
241
+ // Assert
242
+ expect(createTopicSpy).toHaveBeenCalled();
243
+ expect(toggleLoadingSpy).toHaveBeenCalledWith(true, expect.any(String));
244
+ });
245
+ ```
246
+
247
+ ### 8. Streaming Response Mocking
248
+
249
+ When testing streaming responses, simulate the flow properly:
250
+
251
+ ```typescript
252
+ it('should handle streaming chunks', async () => {
253
+ const { result } = renderHook(() => useChatStore());
254
+ const messages = [
255
+ { id: 'msg-1', role: 'user', content: 'Hello', sessionId: 'test-session' },
256
+ ];
257
+
258
+ const streamSpy = vi
259
+ .spyOn(chatService, 'createAssistantMessageStream')
260
+ .mockImplementation(async ({ onMessageHandle, onFinish }) => {
261
+ // Simulate streaming chunks
262
+ await onMessageHandle?.({ type: 'text', text: 'Hello' } as any);
263
+ await onMessageHandle?.({ type: 'text', text: ' World' } as any);
264
+ await onFinish?.('Hello World', {});
265
+ });
266
+
267
+ await act(async () => {
268
+ await result.current.internal_fetchAIChatMessage({
269
+ messages,
270
+ messageId: 'test-message-id',
271
+ model: 'gpt-4o-mini',
272
+ provider: 'openai',
273
+ });
274
+ });
275
+
276
+ expect(result.current.internal_dispatchMessage).toHaveBeenCalled();
277
+
278
+ streamSpy.mockRestore();
279
+ });
280
+ ```
281
+
282
+ ### 9. Error Handling Tests
283
+
284
+ Always test error scenarios:
285
+
286
+ ```typescript
287
+ it('should handle errors gracefully', async () => {
288
+ const { result } = renderHook(() => useChatStore());
289
+
290
+ vi.spyOn(messageService, 'createMessage').mockRejectedValue(
291
+ new Error('create message error'),
292
+ );
293
+
294
+ await act(async () => {
295
+ try {
296
+ await result.current.sendMessage({ message: 'Test message' });
297
+ } catch {
298
+ // Expected to throw
299
+ }
300
+ });
301
+
302
+ expect(result.current.internal_coreProcessMessage).not.toHaveBeenCalled();
303
+ });
304
+ ```
305
+
306
+ ### 10. Cleanup After Tests
307
+
308
+ Always restore mocks after each test:
309
+
310
+ ```typescript
311
+ afterEach(() => {
312
+ vi.restoreAllMocks();
313
+ });
314
+
315
+ // For individual test cleanup:
316
+ it('should test something', async () => {
317
+ const spy = vi.spyOn(service, 'method').mockImplementation(...);
318
+
319
+ // test logic
320
+
321
+ spy.mockRestore(); // Optional: cleanup immediately after test
322
+ });
323
+ ```
324
+
325
+ ## Common Patterns
326
+
327
+ ### Testing Store Methods That Call Other Store Methods
328
+
329
+ ```typescript
330
+ it('should call internal methods', async () => {
331
+ const { result } = renderHook(() => useChatStore());
332
+
333
+ const internalMethodSpy = vi.spyOn(result.current, 'internal_method')
334
+ .mockResolvedValue();
335
+
336
+ await act(async () => {
337
+ await result.current.publicMethod();
338
+ });
339
+
340
+ expect(internalMethodSpy).toHaveBeenCalledWith(
341
+ expect.any(String),
342
+ expect.objectContaining({ key: 'value' }),
343
+ );
344
+ });
345
+ ```
346
+
347
+ ### Testing Conditional Logic
348
+
349
+ ```typescript
350
+ describe('conditional behavior', () => {
351
+ it('should execute when condition is true', async () => {
352
+ const { result } = renderHook(() => useChatStore());
353
+ vi.spyOn(result.current, 'internal_shouldUseRAG').mockReturnValue(true);
354
+
355
+ await act(async () => {
356
+ await result.current.sendMessage({ message: 'test' });
357
+ });
358
+
359
+ expect(result.current.internal_retrieveChunks).toHaveBeenCalled();
360
+ });
361
+
362
+ it('should not execute when condition is false', async () => {
363
+ const { result } = renderHook(() => useChatStore());
364
+ vi.spyOn(result.current, 'internal_shouldUseRAG').mockReturnValue(false);
365
+
366
+ await act(async () => {
367
+ await result.current.sendMessage({ message: 'test' });
368
+ });
369
+
370
+ expect(result.current.internal_retrieveChunks).not.toHaveBeenCalled();
371
+ });
372
+ });
373
+ ```
374
+
375
+ ### Testing AbortController
376
+
377
+ ```typescript
378
+ it('should abort generation and clear loading state', () => {
379
+ const abortController = new AbortController();
380
+
381
+ act(() => {
382
+ useChatStore.setState({ chatLoadingIdsAbortController: abortController });
383
+ });
384
+
385
+ const { result } = renderHook(() => useChatStore());
386
+ const toggleLoadingSpy = vi.spyOn(result.current, 'internal_toggleChatLoading');
387
+
388
+ act(() => {
389
+ result.current.stopGenerateMessage();
390
+ });
391
+
392
+ expect(abortController.signal.aborted).toBe(true);
393
+ expect(toggleLoadingSpy).toHaveBeenCalledWith(false, undefined, expect.any(String));
394
+ });
395
+ ```
396
+
397
+ ## Anti-Patterns to Avoid
398
+
399
+ ❌ **Don't**: Mock the entire store
400
+
401
+ ```typescript
402
+ vi.mock('../../store', () => ({
403
+ useChatStore: vi.fn(() => ({
404
+ sendMessage: vi.fn(),
405
+ })),
406
+ }));
407
+ ```
408
+
409
+ ❌ **Don't**: Test implementation details
410
+
411
+ ```typescript
412
+ // Bad: testing internal state structure
413
+ expect(result.current.messagesMap).toHaveProperty('test-session');
414
+
415
+ // Good: testing behavior
416
+ expect(result.current.refreshMessages).toHaveBeenCalled();
417
+ ```
418
+
419
+ ❌ **Don't**: Create tight coupling between tests
420
+
421
+ ```typescript
422
+ // Bad: Tests depend on order
423
+ let messageId: string;
424
+
425
+ it('test 1', () => {
426
+ messageId = 'some-id'; // Side effect
427
+ });
428
+
429
+ it('test 2', () => {
430
+ expect(messageId).toBeDefined(); // Depends on test 1
431
+ });
432
+ ```
433
+
434
+ ❌ **Don't**: Over-mock services
435
+
436
+ ```typescript
437
+ // Bad: Mocking everything
438
+ beforeEach(() => {
439
+ vi.mock('@/services/chat');
440
+ vi.mock('@/services/message');
441
+ vi.mock('@/services/file');
442
+ vi.mock('@/services/agent');
443
+ // ... too many global mocks
444
+ });
445
+ ```
446
+
447
+ ## Testing SWR Hooks in Zustand Stores
448
+
449
+ Some Zustand store slices use SWR hooks for data fetching. These require a different testing approach.
450
+
451
+ ### Basic SWR Hook Test Structure
452
+
453
+ ```typescript
454
+ import { renderHook, waitFor } from '@testing-library/react';
455
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
456
+
457
+ import { discoverService } from '@/services/discover';
458
+ import { globalHelpers } from '@/store/global/helpers';
459
+ import { useDiscoverStore as useStore } from '../../store';
460
+
461
+ vi.mock('zustand/traditional');
462
+
463
+ beforeEach(() => {
464
+ vi.clearAllMocks();
465
+ });
466
+
467
+ describe('SWR Hook Actions', () => {
468
+ it('should fetch data and return correct response', async () => {
469
+ const mockData = [{ id: '1', name: 'Item 1' }];
470
+
471
+ // Mock the service call (the fetcher)
472
+ vi.spyOn(discoverService, 'getPluginCategories').mockResolvedValue(mockData as any);
473
+ vi.spyOn(globalHelpers, 'getCurrentLanguage').mockReturnValue('en-US');
474
+
475
+ const params = {} as any;
476
+ const { result } = renderHook(() => useStore.getState().usePluginCategories(params));
477
+
478
+ // Use waitFor to wait for async data loading
479
+ await waitFor(() => {
480
+ expect(result.current.data).toEqual(mockData);
481
+ });
482
+
483
+ expect(discoverService.getPluginCategories).toHaveBeenCalledWith(params);
484
+ });
485
+ });
486
+ ```
487
+
488
+ **Key points**:
489
+ - **DO NOT mock useSWR** - let it use the real implementation
490
+ - Only mock the **service methods** (fetchers)
491
+ - Use `waitFor` from `@testing-library/react` to wait for async operations
492
+ - Check `result.current.data` directly after waitFor completes
493
+
494
+ ### Testing SWR Key Generation
495
+
496
+ ```typescript
497
+ it('should generate correct SWR key with locale and params', () => {
498
+ vi.spyOn(globalHelpers, 'getCurrentLanguage').mockReturnValue('zh-CN');
499
+
500
+ const useSWRMock = vi.mocked(useSWR);
501
+ let capturedKey: string | null = null;
502
+ useSWRMock.mockImplementation(((key: string) => {
503
+ capturedKey = key;
504
+ return { data: undefined, error: undefined, isValidating: false, mutate: vi.fn() };
505
+ }) as any);
506
+
507
+ const params = { page: 2, category: 'tools' } as any;
508
+ renderHook(() => useStore.getState().usePluginList(params));
509
+
510
+ expect(capturedKey).toBe('plugin-list-zh-CN-2-tools');
511
+ });
512
+ ```
513
+
514
+ ### Testing SWR Configuration
515
+
516
+ ```typescript
517
+ it('should have correct SWR configuration', () => {
518
+ const useSWRMock = vi.mocked(useSWR);
519
+ let capturedOptions: any = null;
520
+ useSWRMock.mockImplementation(((key: string, fetcher: any, options: any) => {
521
+ capturedOptions = options;
522
+ return { data: undefined, error: undefined, isValidating: false, mutate: vi.fn() };
523
+ }) as any);
524
+
525
+ renderHook(() => useStore.getState().usePluginIdentifiers());
526
+
527
+ expect(capturedOptions).toMatchObject({ revalidateOnFocus: false });
528
+ });
529
+ ```
530
+
531
+ ### Testing Conditional Fetching
532
+
533
+ ```typescript
534
+ it('should not fetch when required parameter is missing', () => {
535
+ const useSWRMock = vi.mocked(useSWR);
536
+ let capturedKey: string | null = null;
537
+ useSWRMock.mockImplementation(((key: string | null) => {
538
+ capturedKey = key;
539
+ return { data: undefined, error: undefined, isValidating: false, mutate: vi.fn() };
540
+ }) as any);
541
+
542
+ // When identifier is undefined, SWR key should be null
543
+ renderHook(() => useStore.getState().usePluginDetail({ identifier: undefined }));
544
+
545
+ expect(capturedKey).toBeNull();
546
+ });
547
+ ```
548
+
549
+ ### Key Differences from Regular Action Tests
550
+
551
+ 1. **Mock useSWR globally**: Use `vi.mock('swr')` at the top level
552
+ 2. **Mock the fetcher, not the result**:
553
+ - ✅ **Correct**: `const data = fetcher?.()` - call fetcher and return its Promise
554
+ - ❌ **Wrong**: `return { data: mockData }` - hardcode the result
555
+ 3. **Await Promise results**: The `data` field is a Promise, use `await result.current.data`
556
+ 4. **No act() wrapper needed**: SWR hooks don't trigger React state updates in these tests
557
+ 5. **Test SWR key generation**: Verify keys include locale and parameters
558
+ 6. **Test configuration**: Verify revalidation and other SWR options
559
+ 7. **Type assertions**: Use `as any` for test mock data where type definitions are strict
560
+
561
+ **Why this matters**:
562
+ - The fetcher (service method) is what we're testing - it must be called
563
+ - Hardcoding the return value bypasses the actual fetcher logic
564
+ - SWR returns Promises in real usage, tests should mirror this behavior
565
+
566
+ ## Benefits of This Approach
567
+
568
+ ✅ **Clear test layers** - Each test only spies on direct dependencies
569
+ ✅ **Correct mocks** - Mocks match actual implementation
570
+ ✅ **Better maintainability** - Changes to implementation require fewer test updates
571
+ ✅ **Improved coverage** - Structured approach ensures all branches are tested
572
+ ✅ **Reduced coupling** - Tests are independent and can run in any order
573
+
574
+ ## Reference
575
+
576
+ See example implementation in:
577
+ - `src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts` (Regular actions)
578
+ - `src/store/discover/slices/plugin/action.test.ts` (SWR hooks)
579
+ - `src/store/discover/slices/mcp/action.test.ts` (SWR hooks)
@@ -22,7 +22,7 @@ jobs:
22
22
  - name: Setup Bun
23
23
  uses: oven-sh/setup-bun@v2
24
24
  with:
25
- bun-version: latest
25
+ bun-version: 1.2.23
26
26
 
27
27
  - name: Install dependencies (bun)
28
28
  run: bun install
package/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.137.8](https://github.com/lobehub/lobe-chat/compare/v1.137.7...v1.137.8)
6
+
7
+ <sup>Released on **2025-10-15**</sup>
8
+
9
+ #### 🐛 Bug Fixes
10
+
11
+ - **misc**: Fix duplicate tools id issue and fix link dialog issue.
12
+
13
+ #### 💄 Styles
14
+
15
+ - **misc**: Add region support for Vertex AI provider.
16
+
17
+ <br/>
18
+
19
+ <details>
20
+ <summary><kbd>Improvements and Fixes</kbd></summary>
21
+
22
+ #### What's fixed
23
+
24
+ - **misc**: Fix duplicate tools id issue and fix link dialog issue, closes [#9731](https://github.com/lobehub/lobe-chat/issues/9731) ([0a8c80d](https://github.com/lobehub/lobe-chat/commit/0a8c80d))
25
+
26
+ #### Styles
27
+
28
+ - **misc**: Add region support for Vertex AI provider, closes [#9720](https://github.com/lobehub/lobe-chat/issues/9720) ([d17b50c](https://github.com/lobehub/lobe-chat/commit/d17b50c))
29
+
30
+ </details>
31
+
32
+ <div align="right">
33
+
34
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
35
+
36
+ </div>
37
+
5
38
  ### [Version 1.137.7](https://github.com/lobehub/lobe-chat/compare/v1.137.6...v1.137.7)
6
39
 
7
40
  <sup>Released on **2025-10-15**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "fixes": [
5
+ "Fix duplicate tools id issue and fix link dialog issue."
6
+ ],
7
+ "improvements": [
8
+ "Add region support for Vertex AI provider."
9
+ ]
10
+ },
11
+ "date": "2025-10-15",
12
+ "version": "1.137.8"
13
+ },
2
14
  {
3
15
  "children": {
4
16
  "improvements": [
@@ -399,6 +399,11 @@
399
399
  "desc": "Enter your Vertex AI Keys",
400
400
  "placeholder": "{ \"type\": \"service_account\", \"project_id\": \"xxx\", \"private_key_id\": ... }",
401
401
  "title": "Vertex AI Keys"
402
+ },
403
+ "region": {
404
+ "desc": "Select the region for Vertex AI service. Some models like Gemini 2.5 are only available in specific regions (e.g., global)",
405
+ "placeholder": "Select region",
406
+ "title": "Vertex AI Region"
402
407
  }
403
408
  },
404
409
  "zeroone": {
@@ -399,6 +399,11 @@
399
399
  "desc": "填入你的 Vertex Ai Keys",
400
400
  "placeholder": "{ \"type\": \"service_account\", \"project_id\": \"xxx\", \"private_key_id\": ... }",
401
401
  "title": "Vertex AI Keys"
402
+ },
403
+ "region": {
404
+ "desc": "选择 Vertex AI 服务的区域。某些模型如 Gemini 2.5 仅在特定区域可用(如 global)",
405
+ "placeholder": "选择区域",
406
+ "title": "Vertex AI 区域"
402
407
  }
403
408
  },
404
409
  "zeroone": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.137.7",
3
+ "version": "1.137.8",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot 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",
@@ -56,7 +56,7 @@ export class ToolsEngine {
56
56
  const { toolIds = [], model, provider, context } = params;
57
57
 
58
58
  // Merge user-provided tool IDs with default tool IDs
59
- const allToolIds = [...toolIds, ...this.defaultToolIds];
59
+ const allToolIds = [...new Set([...toolIds, ...this.defaultToolIds])];
60
60
 
61
61
  log(
62
62
  'Generating tools for model=%s, provider=%s, pluginIds=%o (includes %d default tools)',
@@ -96,8 +96,8 @@ export class ToolsEngine {
96
96
  generateToolsDetailed(params: GenerateToolsParams): ToolsGenerationResult {
97
97
  const { toolIds = [], model, provider, context } = params;
98
98
 
99
- // Merge user-provided tool IDs with default tool IDs
100
- const allToolIds = [...toolIds, ...this.defaultToolIds];
99
+ // Merge user-provided tool IDs with default tool IDs and deduplicate
100
+ const allToolIds = [...new Set([...toolIds, ...this.defaultToolIds])];
101
101
 
102
102
  log(
103
103
  'Generating detailed tools for model=%s, provider=%s, pluginIds=%o (includes %d default tools)',