@lobehub/chat 1.128.0 → 1.128.1

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 (52) hide show
  1. package/.github/workflows/test.yml +8 -1
  2. package/CHANGELOG.md +25 -0
  3. package/changelog/v1.json +9 -0
  4. package/next.config.ts +8 -1
  5. package/package.json +71 -69
  6. package/packages/context-engine/ARCHITECTURE.md +425 -0
  7. package/packages/context-engine/package.json +40 -0
  8. package/packages/context-engine/src/base/BaseProcessor.ts +87 -0
  9. package/packages/context-engine/src/base/BaseProvider.ts +22 -0
  10. package/packages/context-engine/src/index.ts +32 -0
  11. package/packages/context-engine/src/pipeline.ts +219 -0
  12. package/packages/context-engine/src/processors/HistoryTruncate.ts +76 -0
  13. package/packages/context-engine/src/processors/InputTemplate.ts +83 -0
  14. package/packages/context-engine/src/processors/MessageCleanup.ts +87 -0
  15. package/packages/context-engine/src/processors/MessageContent.ts +298 -0
  16. package/packages/context-engine/src/processors/PlaceholderVariables.ts +196 -0
  17. package/packages/context-engine/src/processors/ToolCall.ts +186 -0
  18. package/packages/context-engine/src/processors/ToolMessageReorder.ts +113 -0
  19. package/packages/context-engine/src/processors/__tests__/HistoryTruncate.test.ts +175 -0
  20. package/packages/context-engine/src/processors/__tests__/InputTemplate.test.ts +243 -0
  21. package/packages/context-engine/src/processors/__tests__/MessageContent.test.ts +394 -0
  22. package/packages/context-engine/src/processors/__tests__/PlaceholderVariables.test.ts +334 -0
  23. package/packages/context-engine/src/processors/__tests__/ToolMessageReorder.test.ts +186 -0
  24. package/packages/context-engine/src/processors/index.ts +15 -0
  25. package/packages/context-engine/src/providers/HistorySummary.ts +102 -0
  26. package/packages/context-engine/src/providers/InboxGuide.ts +102 -0
  27. package/packages/context-engine/src/providers/SystemRoleInjector.ts +64 -0
  28. package/packages/context-engine/src/providers/ToolSystemRole.ts +118 -0
  29. package/packages/context-engine/src/providers/__tests__/HistorySummaryProvider.test.ts +112 -0
  30. package/packages/context-engine/src/providers/__tests__/InboxGuideProvider.test.ts +121 -0
  31. package/packages/context-engine/src/providers/__tests__/SystemRoleInjector.test.ts +200 -0
  32. package/packages/context-engine/src/providers/__tests__/ToolSystemRoleProvider.test.ts +140 -0
  33. package/packages/context-engine/src/providers/index.ts +11 -0
  34. package/packages/context-engine/src/types.ts +201 -0
  35. package/packages/context-engine/vitest.config.mts +10 -0
  36. package/packages/database/package.json +1 -1
  37. package/packages/prompts/src/prompts/systemRole/index.ts +1 -1
  38. package/packages/utils/src/index.ts +2 -0
  39. package/packages/utils/src/uriParser.test.ts +29 -0
  40. package/packages/utils/src/uriParser.ts +24 -0
  41. package/src/services/{__tests__ → chat}/chat.test.ts +22 -1032
  42. package/src/services/chat/clientModelRuntime.test.ts +385 -0
  43. package/src/services/chat/clientModelRuntime.ts +34 -0
  44. package/src/services/chat/contextEngineering.test.ts +848 -0
  45. package/src/services/chat/contextEngineering.ts +123 -0
  46. package/src/services/chat/helper.ts +61 -0
  47. package/src/services/{chat.ts → chat/index.ts} +24 -366
  48. package/src/services/chat/types.ts +9 -0
  49. package/src/services/models.ts +1 -1
  50. package/src/store/aiInfra/slices/aiModel/selectors.ts +2 -2
  51. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +1 -40
  52. /package/src/services/{__tests__ → chat}/__snapshots__/chat.test.ts.snap +0 -0
@@ -1,46 +1,22 @@
1
- import {
2
- LobeAnthropicAI,
3
- LobeAzureOpenAI,
4
- LobeBedrockAI,
5
- LobeDeepSeekAI,
6
- LobeGoogleAI,
7
- LobeGroq,
8
- LobeMistralAI,
9
- LobeMoonshotAI,
10
- LobeOllamaAI,
11
- LobeOpenAI,
12
- LobeOpenAICompatibleRuntime,
13
- LobeOpenRouterAI,
14
- LobePerplexityAI,
15
- LobeQwenAI,
16
- LobeTogetherAI,
17
- LobeZeroOneAI,
18
- LobeZhipuAI,
19
- ModelProvider,
20
- ModelRuntime,
21
- } from '@lobechat/model-runtime';
22
- import { ChatErrorType } from '@lobechat/types';
23
1
  import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
24
2
  import { act } from '@testing-library/react';
25
- import { merge } from 'lodash-es';
26
- import OpenAI from 'openai';
27
3
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
28
4
 
29
5
  import { DEFAULT_USER_AVATAR } from '@/const/meta';
30
6
  import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
31
- import { agentChatConfigSelectors } from '@/store/agent/selectors';
7
+ import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
32
8
  import { aiModelSelectors } from '@/store/aiInfra';
33
9
  import { useToolStore } from '@/store/tool';
34
10
  import { toolSelectors } from '@/store/tool/selectors';
35
- import { UserStore } from '@/store/user';
36
- import { UserSettingsState, initialSettingsState } from '@/store/user/slices/settings/initialState';
37
11
  import { DalleManifest } from '@/tools/dalle';
38
12
  import { WebBrowsingManifest } from '@/tools/web-browsing';
13
+ import { ChatErrorType } from '@/types/index';
39
14
  import { ChatImageItem, ChatMessage } from '@/types/message';
40
15
  import { ChatStreamPayload, type OpenAIChatMessage } from '@/types/openai/chat';
41
16
  import { LobeTool } from '@/types/tool';
42
17
 
43
- import { chatService, initializeWithClientStore } from '../chat';
18
+ import * as helpers from './helper';
19
+ import { chatService } from './index';
44
20
 
45
21
  // Mocking external dependencies
46
22
  vi.mock('i18next', () => ({
@@ -52,35 +28,25 @@ vi.stubGlobal(
52
28
  vi.fn(() => Promise.resolve(new Response(JSON.stringify({ some: 'data' })))),
53
29
  );
54
30
 
31
+ // Mock image processing utilities
55
32
  vi.mock('@/utils/fetch', async (importOriginal) => {
56
33
  const module = await importOriginal();
57
34
 
58
35
  return { ...(module as any), getMessageError: vi.fn() };
59
36
  });
60
-
61
- // Mock image processing utilities
62
- vi.mock('@/utils/url', () => ({
37
+ vi.mock('@lobechat/utils', () => ({
63
38
  isLocalUrl: vi.fn(),
64
- }));
65
-
66
- vi.mock('@/utils/imageToBase64', () => ({
67
39
  imageUrlToBase64: vi.fn(),
40
+ parseDataUri: vi.fn(),
68
41
  }));
69
42
 
70
- vi.mock('@lobechat/model-runtime', async (importOriginal) => {
71
- const actual = await importOriginal();
72
-
73
- return {
74
- ...(actual as any),
75
- parseDataUri: vi.fn(),
76
- };
77
- });
78
-
79
43
  afterEach(() => {
80
44
  vi.restoreAllMocks();
81
45
  });
82
46
 
83
47
  beforeEach(async () => {
48
+ // Reset all mocks
49
+ vi.clearAllMocks();
84
50
  // 清除所有模块的缓存
85
51
  vi.resetModules();
86
52
 
@@ -90,21 +56,6 @@ beforeEach(async () => {
90
56
  isDeprecatedEdition: true,
91
57
  isDesktop: false,
92
58
  }));
93
-
94
- // Reset all mocks
95
- vi.clearAllMocks();
96
-
97
- // Set default mock return values for image processing utilities
98
- const { isLocalUrl } = await import('@/utils/url');
99
- const { imageUrlToBase64 } = await import('@/utils/imageToBase64');
100
- const { parseDataUri } = await import('@lobechat/model-runtime');
101
-
102
- vi.mocked(parseDataUri).mockReturnValue({ type: 'url', base64: null, mimeType: null });
103
- vi.mocked(isLocalUrl).mockReturnValue(false);
104
- vi.mocked(imageUrlToBase64).mockResolvedValue({
105
- base64: 'mock-base64',
106
- mimeType: 'image/jpeg',
107
- });
108
59
  });
109
60
 
110
61
  // mock auth
@@ -340,6 +291,11 @@ describe('ChatService', () => {
340
291
 
341
292
  describe('should handle content correctly for vision models', () => {
342
293
  it('should include image content when with vision model', async () => {
294
+ // Mock utility functions used in processImageList
295
+ const { parseDataUri, isLocalUrl } = await import('@lobechat/utils');
296
+ vi.mocked(parseDataUri).mockReturnValue({ type: 'url', base64: null, mimeType: null });
297
+ vi.mocked(isLocalUrl).mockReturnValue(false); // Not a local URL
298
+
343
299
  const messages = [
344
300
  {
345
301
  content: 'Hello',
@@ -359,6 +315,7 @@ describe('ChatService', () => {
359
315
  messages,
360
316
  plugins: [],
361
317
  model: 'gpt-4-vision-preview',
318
+ provider: 'openai',
362
319
  });
363
320
 
364
321
  expect(getChatCompletionSpy).toHaveBeenCalledWith(
@@ -379,6 +336,9 @@ describe('ChatService', () => {
379
336
  },
380
337
  ],
381
338
  model: 'gpt-4-vision-preview',
339
+ provider: 'openai',
340
+ enabledSearch: undefined,
341
+ tools: undefined,
382
342
  },
383
343
  undefined,
384
344
  );
@@ -407,9 +367,7 @@ describe('ChatService', () => {
407
367
 
408
368
  describe('local image URL conversion', () => {
409
369
  it('should convert local image URLs to base64 and call processImageList', async () => {
410
- const { isLocalUrl } = await import('@/utils/url');
411
- const { imageUrlToBase64 } = await import('@/utils/imageToBase64');
412
- const { parseDataUri } = await import('@lobechat/model-runtime');
370
+ const { imageUrlToBase64, parseDataUri, isLocalUrl } = await import('@lobechat/utils');
413
371
 
414
372
  // Mock for local URL
415
373
  vi.mocked(parseDataUri).mockReturnValue({ type: 'url', base64: null, mimeType: null });
@@ -438,7 +396,7 @@ describe('ChatService', () => {
438
396
  ] as ChatMessage[];
439
397
 
440
398
  // Spy on processImageList method
441
- const processImageListSpy = vi.spyOn(chatService as any, 'processImageList');
399
+ // const processImageListSpy = vi.spyOn(chatService as any, 'processImageList');
442
400
  const getChatCompletionSpy = vi.spyOn(chatService, 'getChatCompletion');
443
401
 
444
402
  await chatService.createAssistantMessage({
@@ -447,19 +405,6 @@ describe('ChatService', () => {
447
405
  model: 'gpt-4-vision-preview',
448
406
  });
449
407
 
450
- // Verify processImageList was called with correct arguments
451
- expect(processImageListSpy).toHaveBeenCalledWith({
452
- imageList: [
453
- {
454
- id: 'file1',
455
- url: 'http://127.0.0.1:3000/uploads/image.png',
456
- alt: 'local-image.png',
457
- },
458
- ],
459
- model: 'gpt-4-vision-preview',
460
- provider: undefined,
461
- });
462
-
463
408
  // Verify the utility functions were called
464
409
  expect(parseDataUri).toHaveBeenCalledWith('http://127.0.0.1:3000/uploads/image.png');
465
410
  expect(isLocalUrl).toHaveBeenCalledWith('http://127.0.0.1:3000/uploads/image.png');
@@ -493,9 +438,7 @@ describe('ChatService', () => {
493
438
  });
494
439
 
495
440
  it('should not convert remote URLs to base64 and call processImageList', async () => {
496
- const { isLocalUrl } = await import('@/utils/url');
497
- const { imageUrlToBase64 } = await import('@/utils/imageToBase64');
498
- const { parseDataUri } = await import('@lobechat/model-runtime');
441
+ const { imageUrlToBase64, parseDataUri, isLocalUrl } = await import('@lobechat/utils');
499
442
 
500
443
  // Mock for remote URL
501
444
  vi.mocked(parseDataUri).mockReturnValue({ type: 'url', base64: null, mimeType: null });
@@ -521,7 +464,6 @@ describe('ChatService', () => {
521
464
  ] as ChatMessage[];
522
465
 
523
466
  // Spy on processImageList method
524
- const processImageListSpy = vi.spyOn(chatService as any, 'processImageList');
525
467
  const getChatCompletionSpy = vi.spyOn(chatService, 'getChatCompletion');
526
468
 
527
469
  await chatService.createAssistantMessage({
@@ -530,19 +472,6 @@ describe('ChatService', () => {
530
472
  model: 'gpt-4-vision-preview',
531
473
  });
532
474
 
533
- // Verify processImageList was called
534
- expect(processImageListSpy).toHaveBeenCalledWith({
535
- imageList: [
536
- {
537
- id: 'file1',
538
- url: 'https://example.com/remote-image.jpg',
539
- alt: 'remote-image.jpg',
540
- },
541
- ],
542
- model: 'gpt-4-vision-preview',
543
- provider: undefined,
544
- });
545
-
546
475
  // Verify the utility functions were called
547
476
  expect(parseDataUri).toHaveBeenCalledWith('https://example.com/remote-image.jpg');
548
477
  expect(isLocalUrl).toHaveBeenCalledWith('https://example.com/remote-image.jpg');
@@ -573,9 +502,7 @@ describe('ChatService', () => {
573
502
  });
574
503
 
575
504
  it('should handle mixed local and remote URLs correctly', async () => {
576
- const { isLocalUrl } = await import('@/utils/url');
577
- const { imageUrlToBase64 } = await import('@/utils/imageToBase64');
578
- const { parseDataUri } = await import('@lobechat/model-runtime');
505
+ const { imageUrlToBase64, parseDataUri, isLocalUrl } = await import('@lobechat/utils');
579
506
 
580
507
  // Mock parseDataUri to always return url type
581
508
  vi.mocked(parseDataUri).mockReturnValue({ type: 'url', base64: null, mimeType: null });
@@ -619,7 +546,6 @@ describe('ChatService', () => {
619
546
  },
620
547
  ] as ChatMessage[];
621
548
 
622
- const processImageListSpy = vi.spyOn(chatService as any, 'processImageList');
623
549
  const getChatCompletionSpy = vi.spyOn(chatService, 'getChatCompletion');
624
550
 
625
551
  await chatService.createAssistantMessage({
@@ -628,17 +554,6 @@ describe('ChatService', () => {
628
554
  model: 'gpt-4-vision-preview',
629
555
  });
630
556
 
631
- // Verify processImageList was called
632
- expect(processImageListSpy).toHaveBeenCalledWith({
633
- imageList: [
634
- { id: 'local1', url: 'http://127.0.0.1:3000/local1.jpg', alt: 'local1.jpg' },
635
- { id: 'remote1', url: 'https://example.com/remote1.png', alt: 'remote1.png' },
636
- { id: 'local2', url: 'http://127.0.0.1:8080/local2.gif', alt: 'local2.gif' },
637
- ],
638
- model: 'gpt-4-vision-preview',
639
- provider: undefined,
640
- });
641
-
642
557
  // Verify isLocalUrl was called for each image
643
558
  expect(isLocalUrl).toHaveBeenCalledWith('http://127.0.0.1:3000/local1.jpg');
644
559
  expect(isLocalUrl).toHaveBeenCalledWith('https://example.com/remote1.png');
@@ -1274,428 +1189,6 @@ describe('ChatService', () => {
1274
1189
  expect(onLoadingChange).toHaveBeenCalledWith(false); // Confirm loading state is set to false
1275
1190
  });
1276
1191
  });
1277
-
1278
- describe('reorderToolMessages', () => {
1279
- it('should reorderToolMessages', () => {
1280
- const input: OpenAIChatMessage[] = [
1281
- {
1282
- content: '## Tools\n\nYou can use these tools',
1283
- role: 'system',
1284
- },
1285
- {
1286
- content: '',
1287
- role: 'assistant',
1288
- tool_calls: [
1289
- {
1290
- function: {
1291
- arguments:
1292
- '{"query":"LobeChat","searchEngines":["brave","google","duckduckgo","qwant"]}',
1293
- name: 'lobe-web-browsing____searchWithSearXNG____builtin',
1294
- },
1295
- id: 'call_6xCmrOtFOyBAcqpqO1TGfw2B',
1296
- type: 'function',
1297
- },
1298
- {
1299
- function: {
1300
- arguments:
1301
- '{"query":"LobeChat","searchEngines":["brave","google","duckduckgo","qwant"]}',
1302
- name: 'lobe-web-browsing____searchWithSearXNG____builtin',
1303
- },
1304
- id: 'tool_call_nXxXHW8Z',
1305
- type: 'function',
1306
- },
1307
- ],
1308
- },
1309
- {
1310
- content: '[]',
1311
- name: 'lobe-web-browsing____searchWithSearXNG____builtin',
1312
- role: 'tool',
1313
- tool_call_id: 'call_6xCmrOtFOyBAcqpqO1TGfw2B',
1314
- },
1315
- {
1316
- content: 'LobeHub 是一个专注于设计和开发现代人工智能生成内容(AIGC)工具和组件的团队。',
1317
- role: 'assistant',
1318
- },
1319
- {
1320
- content: '[]',
1321
- name: 'lobe-web-browsing____searchWithSearXNG____builtin',
1322
- role: 'tool',
1323
- tool_call_id: 'tool_call_nXxXHW8Z',
1324
- },
1325
- {
1326
- content: '[]',
1327
- name: 'lobe-web-browsing____searchWithSearXNG____builtin',
1328
- role: 'tool',
1329
- tool_call_id: 'tool_call_2f3CEKz9',
1330
- },
1331
- {
1332
- content: '### LobeHub 智能AI聚合神器\n\nLobeHub 是一个强大的AI聚合平台',
1333
- role: 'assistant',
1334
- },
1335
- ];
1336
- const output = chatService['reorderToolMessages'](input);
1337
-
1338
- expect(output).toEqual([
1339
- {
1340
- content: '## Tools\n\nYou can use these tools',
1341
- role: 'system',
1342
- },
1343
- {
1344
- content: '',
1345
- role: 'assistant',
1346
- tool_calls: [
1347
- {
1348
- function: {
1349
- arguments:
1350
- '{"query":"LobeChat","searchEngines":["brave","google","duckduckgo","qwant"]}',
1351
- name: 'lobe-web-browsing____searchWithSearXNG____builtin',
1352
- },
1353
- id: 'call_6xCmrOtFOyBAcqpqO1TGfw2B',
1354
- type: 'function',
1355
- },
1356
- {
1357
- function: {
1358
- arguments:
1359
- '{"query":"LobeChat","searchEngines":["brave","google","duckduckgo","qwant"]}',
1360
- name: 'lobe-web-browsing____searchWithSearXNG____builtin',
1361
- },
1362
- id: 'tool_call_nXxXHW8Z',
1363
- type: 'function',
1364
- },
1365
- ],
1366
- },
1367
- {
1368
- content: '[]',
1369
- name: 'lobe-web-browsing____searchWithSearXNG____builtin',
1370
- role: 'tool',
1371
- tool_call_id: 'call_6xCmrOtFOyBAcqpqO1TGfw2B',
1372
- },
1373
- {
1374
- content: '[]',
1375
- name: 'lobe-web-browsing____searchWithSearXNG____builtin',
1376
- role: 'tool',
1377
- tool_call_id: 'tool_call_nXxXHW8Z',
1378
- },
1379
- {
1380
- content: 'LobeHub 是一个专注于设计和开发现代人工智能生成内容(AIGC)工具和组件的团队。',
1381
- role: 'assistant',
1382
- },
1383
- {
1384
- content: '### LobeHub 智能AI聚合神器\n\nLobeHub 是一个强大的AI聚合平台',
1385
- role: 'assistant',
1386
- },
1387
- ]);
1388
- });
1389
- });
1390
-
1391
- describe('processMessage', () => {
1392
- describe('handle with files content in server mode', () => {
1393
- it('should includes files', async () => {
1394
- // 重新模拟模块,设置 isServerMode 为 true
1395
- vi.doMock('@/const/version', () => ({
1396
- isServerMode: true,
1397
- isDeprecatedEdition: false,
1398
- isDesktop: false,
1399
- }));
1400
-
1401
- // 需要在修改模拟后重新导入相关模块
1402
- const { chatService } = await import('../chat');
1403
-
1404
- // Mock processImageList to return expected image content
1405
- const processImageListSpy = vi.spyOn(chatService as any, 'processImageList');
1406
- processImageListSpy.mockImplementation(async () => {
1407
- // Mock the expected return value for an image
1408
- return [
1409
- {
1410
- image_url: { detail: 'auto', url: 'http://example.com/xxx0asd-dsd.png' },
1411
- type: 'image_url',
1412
- },
1413
- ];
1414
- });
1415
-
1416
- const messages = [
1417
- {
1418
- content: 'Hello',
1419
- role: 'user',
1420
- imageList: [
1421
- {
1422
- id: 'imagecx1',
1423
- url: 'http://example.com/xxx0asd-dsd.png',
1424
- alt: 'ttt.png',
1425
- },
1426
- ],
1427
- fileList: [
1428
- {
1429
- fileType: 'plain/txt',
1430
- size: 100000,
1431
- id: 'file1',
1432
- url: 'http://abc.com/abc.txt',
1433
- name: 'abc.png',
1434
- },
1435
- {
1436
- id: 'file_oKMve9qySLMI',
1437
- name: '2402.16667v1.pdf',
1438
- type: 'application/pdf',
1439
- size: 11256078,
1440
- url: 'https://xxx.com/ppp/480497/5826c2b8-fde0-4de1-a54b-a224d5e3d898.pdf',
1441
- },
1442
- ],
1443
- }, // Message with files
1444
- { content: 'Hey', role: 'assistant' }, // Regular user message
1445
- ] as ChatMessage[];
1446
-
1447
- const output = await chatService['processMessages']({
1448
- messages,
1449
- model: 'gpt-4o',
1450
- provider: 'openai',
1451
- });
1452
-
1453
- expect(output).toEqual([
1454
- {
1455
- content: [
1456
- {
1457
- text: `Hello
1458
-
1459
- <!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
1460
- <context.instruction>following part contains context information injected by the system. Please follow these instructions:
1461
-
1462
- 1. Always prioritize handling user-visible content.
1463
- 2. the context is only required when user's queries rely on it.
1464
- </context.instruction>
1465
- <files_info>
1466
- <images>
1467
- <images_docstring>here are user upload images you can refer to</images_docstring>
1468
- <image name="ttt.png" url="http://example.com/xxx0asd-dsd.png"></image>
1469
- </images>
1470
- <files>
1471
- <files_docstring>here are user upload files you can refer to</files_docstring>
1472
- <file id="file1" name="abc.png" type="plain/txt" size="100000" url="http://abc.com/abc.txt"></file>
1473
- <file id="file_oKMve9qySLMI" name="2402.16667v1.pdf" type="undefined" size="11256078" url="https://xxx.com/ppp/480497/5826c2b8-fde0-4de1-a54b-a224d5e3d898.pdf"></file>
1474
- </files>
1475
- </files_info>
1476
- <!-- END SYSTEM CONTEXT -->`,
1477
- type: 'text',
1478
- },
1479
- {
1480
- image_url: { detail: 'auto', url: 'http://example.com/xxx0asd-dsd.png' },
1481
- type: 'image_url',
1482
- },
1483
- ],
1484
- role: 'user',
1485
- },
1486
- {
1487
- content: 'Hey',
1488
- role: 'assistant',
1489
- },
1490
- ]);
1491
- });
1492
-
1493
- it('should include image files in server mode', async () => {
1494
- // 重新模拟模块,设置 isServerMode 为 true
1495
- vi.doMock('@/const/version', () => ({
1496
- isServerMode: true,
1497
- isDeprecatedEdition: true,
1498
- isDesktop: false,
1499
- }));
1500
-
1501
- // 需要在修改模拟后重新导入相关模块
1502
- const { chatService } = await import('../chat');
1503
- const messages = [
1504
- {
1505
- content: 'Hello',
1506
- role: 'user',
1507
- imageList: [
1508
- {
1509
- id: 'file1',
1510
- url: 'http://example.com/image.jpg',
1511
- alt: 'abc.png',
1512
- },
1513
- ],
1514
- }, // Message with files
1515
- { content: 'Hey', role: 'assistant' }, // Regular user message
1516
- ] as ChatMessage[];
1517
-
1518
- const getChatCompletionSpy = vi.spyOn(chatService, 'getChatCompletion');
1519
- await chatService.createAssistantMessage({
1520
- messages,
1521
- plugins: [],
1522
- model: 'gpt-4-vision-preview',
1523
- });
1524
-
1525
- expect(getChatCompletionSpy).toHaveBeenCalledWith(
1526
- {
1527
- messages: [
1528
- {
1529
- content: [
1530
- {
1531
- text: `Hello
1532
-
1533
- <!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->
1534
- <context.instruction>following part contains context information injected by the system. Please follow these instructions:
1535
-
1536
- 1. Always prioritize handling user-visible content.
1537
- 2. the context is only required when user's queries rely on it.
1538
- </context.instruction>
1539
- <files_info>
1540
- <images>
1541
- <images_docstring>here are user upload images you can refer to</images_docstring>
1542
- <image name="abc.png" url="http://example.com/image.jpg"></image>
1543
- </images>
1544
-
1545
- </files_info>
1546
- <!-- END SYSTEM CONTEXT -->`,
1547
- type: 'text',
1548
- },
1549
- {
1550
- image_url: { detail: 'auto', url: 'http://example.com/image.jpg' },
1551
- type: 'image_url',
1552
- },
1553
- ],
1554
- role: 'user',
1555
- },
1556
- {
1557
- content: 'Hey',
1558
- role: 'assistant',
1559
- },
1560
- ],
1561
- model: 'gpt-4-vision-preview',
1562
- },
1563
- undefined,
1564
- );
1565
- });
1566
- });
1567
-
1568
- it('should handle empty tool calls messages correctly', async () => {
1569
- const messages = [
1570
- {
1571
- content: '## Tools\n\nYou can use these tools',
1572
- role: 'system',
1573
- },
1574
- {
1575
- content: '',
1576
- role: 'assistant',
1577
- tool_calls: [],
1578
- },
1579
- ] as ChatMessage[];
1580
-
1581
- const result = await chatService['processMessages']({
1582
- messages,
1583
- model: 'gpt-4',
1584
- provider: 'openai',
1585
- });
1586
-
1587
- expect(result).toEqual([
1588
- {
1589
- content: '## Tools\n\nYou can use these tools',
1590
- role: 'system',
1591
- },
1592
- {
1593
- content: '',
1594
- role: 'assistant',
1595
- },
1596
- ]);
1597
- });
1598
-
1599
- it('should handle assistant messages with reasoning correctly', async () => {
1600
- const messages = [
1601
- {
1602
- role: 'assistant',
1603
- content: 'The answer is 42.',
1604
- reasoning: {
1605
- content: 'I need to calculate the answer to life, universe, and everything.',
1606
- signature: 'thinking_process',
1607
- },
1608
- },
1609
- ] as ChatMessage[];
1610
-
1611
- const result = await chatService['processMessages']({
1612
- messages,
1613
- model: 'gpt-4',
1614
- provider: 'openai',
1615
- });
1616
-
1617
- expect(result).toEqual([
1618
- {
1619
- content: [
1620
- {
1621
- signature: 'thinking_process',
1622
- thinking: 'I need to calculate the answer to life, universe, and everything.',
1623
- type: 'thinking',
1624
- },
1625
- {
1626
- text: 'The answer is 42.',
1627
- type: 'text',
1628
- },
1629
- ],
1630
- role: 'assistant',
1631
- },
1632
- ]);
1633
- });
1634
-
1635
- it('should inject INBOX_GUIDE_SYSTEMROLE for welcome questions in inbox session', async () => {
1636
- // Don't mock INBOX_GUIDE_SYSTEMROLE, use the real one
1637
- const messages: ChatMessage[] = [
1638
- {
1639
- role: 'user',
1640
- content: 'Hello, this is my first question',
1641
- createdAt: Date.now(),
1642
- id: 'test-welcome',
1643
- meta: {},
1644
- updatedAt: Date.now(),
1645
- },
1646
- ];
1647
-
1648
- const result = await chatService['processMessages'](
1649
- {
1650
- messages,
1651
- model: 'gpt-4',
1652
- provider: 'openai',
1653
- },
1654
- {
1655
- isWelcomeQuestion: true,
1656
- trace: { sessionId: 'inbox' },
1657
- },
1658
- );
1659
-
1660
- // Should have system message with inbox guide content
1661
- const systemMessage = result.find((msg) => msg.role === 'system');
1662
- expect(systemMessage).toBeDefined();
1663
- // Check for characteristic content of the actual INBOX_GUIDE_SYSTEMROLE
1664
- expect(systemMessage!.content).toContain('LobeChat Support Assistant');
1665
- expect(systemMessage!.content).toContain('LobeHub');
1666
- });
1667
-
1668
- it('should inject historySummary into system message when provided', async () => {
1669
- const historySummary = 'Previous conversation summary: User discussed AI topics.';
1670
-
1671
- const messages: ChatMessage[] = [
1672
- {
1673
- role: 'user',
1674
- content: 'Continue our discussion',
1675
- createdAt: Date.now(),
1676
- id: 'test-history',
1677
- meta: {},
1678
- updatedAt: Date.now(),
1679
- },
1680
- ];
1681
-
1682
- const result = await chatService['processMessages'](
1683
- {
1684
- messages,
1685
- model: 'gpt-4',
1686
- provider: 'openai',
1687
- },
1688
- {
1689
- historySummary,
1690
- },
1691
- );
1692
-
1693
- // Should have system message with history summary
1694
- const systemMessage = result.find((msg) => msg.role === 'system');
1695
- expect(systemMessage).toBeDefined();
1696
- expect(systemMessage!.content).toContain(historySummary);
1697
- });
1698
- });
1699
1192
  });
1700
1193
 
1701
1194
  /**
@@ -1707,218 +1200,6 @@ vi.mock('../_auth', async (importOriginal) => {
1707
1200
  });
1708
1201
 
1709
1202
  describe('ChatService private methods', () => {
1710
- describe('processImageList', () => {
1711
- beforeEach(() => {
1712
- vi.resetModules();
1713
- });
1714
-
1715
- it('should return empty array if model cannot use vision (non-deprecated)', async () => {
1716
- vi.doMock('@/const/version', () => ({
1717
- isServerMode: false,
1718
- isDeprecatedEdition: false,
1719
- isDesktop: false,
1720
- }));
1721
- const { aiModelSelectors } = await import('@/store/aiInfra');
1722
- vi.spyOn(aiModelSelectors, 'isModelSupportVision').mockReturnValue(() => false);
1723
-
1724
- const { chatService } = await import('../chat');
1725
- const result = await chatService['processImageList']({
1726
- imageList: [{ url: 'image_url', alt: '', id: 'test' } as ChatImageItem],
1727
- model: 'any-model',
1728
- provider: 'any-provider',
1729
- });
1730
- expect(result).toEqual([]);
1731
- });
1732
-
1733
- it('should process images if model can use vision (non-deprecated)', async () => {
1734
- vi.doMock('@/const/version', () => ({
1735
- isServerMode: false,
1736
- isDeprecatedEdition: false,
1737
- isDesktop: false,
1738
- }));
1739
- const { aiModelSelectors } = await import('@/store/aiInfra');
1740
- vi.spyOn(aiModelSelectors, 'isModelSupportVision').mockReturnValue(() => true);
1741
-
1742
- const { chatService } = await import('../chat');
1743
- const result = await chatService['processImageList']({
1744
- imageList: [{ url: 'image_url', alt: '', id: 'test' } as ChatImageItem],
1745
- model: 'any-model',
1746
- provider: 'any-provider',
1747
- });
1748
- expect(result.length).toBe(1);
1749
- expect(result[0].type).toBe('image_url');
1750
- });
1751
-
1752
- it('should return empty array when vision disabled in deprecated edition', async () => {
1753
- vi.doMock('@/const/version', () => ({
1754
- isServerMode: false,
1755
- isDeprecatedEdition: true,
1756
- isDesktop: false,
1757
- }));
1758
-
1759
- const { modelProviderSelectors } = await import('@/store/user/selectors');
1760
- const spy = vi
1761
- .spyOn(modelProviderSelectors, 'isModelEnabledVision')
1762
- .mockReturnValue(() => false);
1763
-
1764
- const { chatService } = await import('../chat');
1765
- const result = await chatService['processImageList']({
1766
- imageList: [{ url: 'image_url', alt: '', id: 'test' } as ChatImageItem],
1767
- model: 'any-model',
1768
- provider: 'any-provider',
1769
- });
1770
-
1771
- expect(spy).toHaveBeenCalled();
1772
- expect(result).toEqual([]);
1773
- });
1774
-
1775
- it('should process images when vision enabled in deprecated edition', async () => {
1776
- vi.doMock('@/const/version', () => ({
1777
- isServerMode: false,
1778
- isDeprecatedEdition: true,
1779
- isDesktop: false,
1780
- }));
1781
-
1782
- const { modelProviderSelectors } = await import('@/store/user/selectors');
1783
- const spy = vi
1784
- .spyOn(modelProviderSelectors, 'isModelEnabledVision')
1785
- .mockReturnValue(() => true);
1786
-
1787
- const { chatService } = await import('../chat');
1788
- const result = await chatService['processImageList']({
1789
- imageList: [{ url: 'image_url' } as ChatImageItem],
1790
- model: 'any-model',
1791
- provider: 'any-provider',
1792
- });
1793
-
1794
- expect(spy).toHaveBeenCalled();
1795
- expect(result.length).toBe(1);
1796
- expect(result[0].type).toBe('image_url');
1797
- });
1798
- });
1799
-
1800
- describe('processMessages', () => {
1801
- describe('getAssistantContent', () => {
1802
- it('should handle assistant message with imageList and content', async () => {
1803
- const messages: ChatMessage[] = [
1804
- {
1805
- role: 'assistant',
1806
- content: 'Here is an image.',
1807
- imageList: [{ id: 'img1', url: 'http://example.com/image.png', alt: 'test.png' }],
1808
- createdAt: Date.now(),
1809
- id: 'test-id',
1810
- meta: {},
1811
- updatedAt: Date.now(),
1812
- },
1813
- ];
1814
- const result = await chatService['processMessages']({
1815
- messages,
1816
- model: 'gpt-4-vision-preview',
1817
- provider: 'openai',
1818
- });
1819
-
1820
- expect(result[0].content).toEqual([
1821
- { text: 'Here is an image.', type: 'text' },
1822
- { image_url: { detail: 'auto', url: 'http://example.com/image.png' }, type: 'image_url' },
1823
- ]);
1824
- });
1825
-
1826
- it('should handle assistant message with imageList but no content', async () => {
1827
- const messages: ChatMessage[] = [
1828
- {
1829
- role: 'assistant',
1830
- content: '',
1831
- imageList: [{ id: 'img1', url: 'http://example.com/image.png', alt: 'test.png' }],
1832
- createdAt: Date.now(),
1833
- id: 'test-id-2',
1834
- meta: {},
1835
- updatedAt: Date.now(),
1836
- },
1837
- ];
1838
- const result = await chatService['processMessages']({
1839
- messages,
1840
- model: 'gpt-4-vision-preview',
1841
- provider: 'openai',
1842
- });
1843
-
1844
- expect(result[0].content).toEqual([
1845
- { image_url: { detail: 'auto', url: 'http://example.com/image.png' }, type: 'image_url' },
1846
- ]);
1847
- });
1848
- });
1849
-
1850
- it('should not include tool_calls for assistant message if model does not support tools', async () => {
1851
- // Mock isCanUseFC to return false
1852
- vi.spyOn(
1853
- (await import('@/store/aiInfra')).aiModelSelectors,
1854
- 'isModelSupportToolUse',
1855
- ).mockReturnValue(() => false);
1856
-
1857
- const messages: ChatMessage[] = [
1858
- {
1859
- role: 'assistant',
1860
- content: 'I have a tool call.',
1861
- tools: [
1862
- {
1863
- id: 'tool_123',
1864
- type: 'default',
1865
- apiName: 'testApi',
1866
- arguments: '{}',
1867
- identifier: 'test-plugin',
1868
- },
1869
- ],
1870
- createdAt: Date.now(),
1871
- id: 'test-id-3',
1872
- meta: {},
1873
- updatedAt: Date.now(),
1874
- },
1875
- ];
1876
-
1877
- const result = await chatService['processMessages']({
1878
- messages,
1879
- model: 'some-model-without-fc',
1880
- provider: 'openai',
1881
- });
1882
-
1883
- expect(result[0].tool_calls).toBeUndefined();
1884
- expect(result[0].content).toBe('I have a tool call.');
1885
- });
1886
- });
1887
-
1888
- describe('reorderToolMessages', () => {
1889
- it('should correctly reorder when a tool message appears before the assistant message', () => {
1890
- const input: OpenAIChatMessage[] = [
1891
- {
1892
- role: 'system',
1893
- content: 'System message',
1894
- },
1895
- {
1896
- role: 'tool',
1897
- tool_call_id: 'tool_call_1',
1898
- name: 'test-plugin____testApi',
1899
- content: 'Tool result',
1900
- },
1901
- {
1902
- role: 'assistant',
1903
- content: '',
1904
- tool_calls: [
1905
- { id: 'tool_call_1', type: 'function', function: { name: 'testApi', arguments: '{}' } },
1906
- ],
1907
- },
1908
- ];
1909
-
1910
- const output = chatService['reorderToolMessages'](input);
1911
-
1912
- // Verify reordering logic works and covers line 688 hasPushed check
1913
- // In this test, tool messages are duplicated but the second occurrence is skipped
1914
- expect(output.length).toBe(4); // Original has 3, assistant will add corresponding tool message again
1915
- expect(output[0].role).toBe('system');
1916
- expect(output[1].role).toBe('tool');
1917
- expect(output[2].role).toBe('assistant');
1918
- expect(output[3].role).toBe('tool'); // Tool message added by assistant's tool_calls
1919
- });
1920
- });
1921
-
1922
1203
  describe('getChatCompletion', () => {
1923
1204
  it('should merge responseAnimation styles correctly', async () => {
1924
1205
  const { fetchSSE } = await import('@/utils/fetch');
@@ -2078,294 +1359,3 @@ describe('ChatService private methods', () => {
2078
1359
  });
2079
1360
  });
2080
1361
  });
2081
-
2082
- describe('ModelRuntimeOnClient', () => {
2083
- describe('initializeWithClientStore', () => {
2084
- describe('should initialize with options correctly', () => {
2085
- it('OpenAI provider: with apikey and endpoint', async () => {
2086
- // Mock the global store to return the user's OpenAI API key and endpoint
2087
- merge(initialSettingsState, {
2088
- settings: {
2089
- keyVaults: {
2090
- openai: {
2091
- apiKey: 'user-openai-key',
2092
- baseURL: 'user-openai-endpoint',
2093
- },
2094
- },
2095
- },
2096
- } as UserSettingsState) as unknown as UserStore;
2097
- const runtime = await initializeWithClientStore(ModelProvider.OpenAI, {});
2098
- expect(runtime).toBeInstanceOf(ModelRuntime);
2099
- expect(runtime['_runtime']).toBeInstanceOf(LobeOpenAI);
2100
- expect(runtime['_runtime'].baseURL).toBe('user-openai-endpoint');
2101
- });
2102
-
2103
- it('Azure provider: with apiKey, apiVersion, endpoint', async () => {
2104
- merge(initialSettingsState, {
2105
- settings: {
2106
- keyVaults: {
2107
- azure: {
2108
- apiKey: 'user-azure-key',
2109
- endpoint: 'user-azure-endpoint',
2110
- apiVersion: '2024-06-01',
2111
- },
2112
- },
2113
- },
2114
- } as UserSettingsState) as unknown as UserStore;
2115
-
2116
- const runtime = await initializeWithClientStore(ModelProvider.Azure, {});
2117
- expect(runtime).toBeInstanceOf(ModelRuntime);
2118
- expect(runtime['_runtime']).toBeInstanceOf(LobeAzureOpenAI);
2119
- });
2120
-
2121
- it('Google provider: with apiKey', async () => {
2122
- merge(initialSettingsState, {
2123
- settings: {
2124
- keyVaults: {
2125
- google: {
2126
- apiKey: 'user-google-key',
2127
- },
2128
- },
2129
- },
2130
- } as UserSettingsState) as unknown as UserStore;
2131
- const runtime = await initializeWithClientStore(ModelProvider.Google, {});
2132
- expect(runtime).toBeInstanceOf(ModelRuntime);
2133
- expect(runtime['_runtime']).toBeInstanceOf(LobeGoogleAI);
2134
- });
2135
-
2136
- it('Moonshot AI provider: with apiKey', async () => {
2137
- merge(initialSettingsState, {
2138
- settings: {
2139
- keyVaults: {
2140
- moonshot: {
2141
- apiKey: 'user-moonshot-key',
2142
- },
2143
- },
2144
- },
2145
- } as UserSettingsState) as unknown as UserStore;
2146
- const runtime = await initializeWithClientStore(ModelProvider.Moonshot, {});
2147
- expect(runtime).toBeInstanceOf(ModelRuntime);
2148
- expect(runtime['_runtime']).toBeInstanceOf(LobeMoonshotAI);
2149
- });
2150
-
2151
- it('Bedrock provider: with accessKeyId, region, secretAccessKey', async () => {
2152
- merge(initialSettingsState, {
2153
- settings: {
2154
- keyVaults: {
2155
- bedrock: {
2156
- accessKeyId: 'user-bedrock-access-key',
2157
- region: 'user-bedrock-region',
2158
- secretAccessKey: 'user-bedrock-secret',
2159
- },
2160
- },
2161
- },
2162
- } as UserSettingsState) as unknown as UserStore;
2163
- const runtime = await initializeWithClientStore(ModelProvider.Bedrock, {});
2164
- expect(runtime).toBeInstanceOf(ModelRuntime);
2165
- expect(runtime['_runtime']).toBeInstanceOf(LobeBedrockAI);
2166
- });
2167
-
2168
- it('Ollama provider: with endpoint', async () => {
2169
- merge(initialSettingsState, {
2170
- settings: {
2171
- keyVaults: {
2172
- ollama: {
2173
- baseURL: 'http://127.0.0.1:1234',
2174
- },
2175
- },
2176
- },
2177
- } as UserSettingsState) as unknown as UserStore;
2178
- const runtime = await initializeWithClientStore(ModelProvider.Ollama, {});
2179
- expect(runtime).toBeInstanceOf(ModelRuntime);
2180
- expect(runtime['_runtime']).toBeInstanceOf(LobeOllamaAI);
2181
- });
2182
-
2183
- it('Perplexity provider: with apiKey', async () => {
2184
- merge(initialSettingsState, {
2185
- settings: {
2186
- keyVaults: {
2187
- perplexity: {
2188
- apiKey: 'user-perplexity-key',
2189
- },
2190
- },
2191
- },
2192
- } as UserSettingsState) as unknown as UserStore;
2193
- const runtime = await initializeWithClientStore(ModelProvider.Perplexity, {});
2194
- expect(runtime).toBeInstanceOf(ModelRuntime);
2195
- expect(runtime['_runtime']).toBeInstanceOf(LobePerplexityAI);
2196
- });
2197
-
2198
- it('Anthropic provider: with apiKey', async () => {
2199
- merge(initialSettingsState, {
2200
- settings: {
2201
- keyVaults: {
2202
- anthropic: {
2203
- apiKey: 'user-anthropic-key',
2204
- },
2205
- },
2206
- },
2207
- } as UserSettingsState) as unknown as UserStore;
2208
- const runtime = await initializeWithClientStore(ModelProvider.Anthropic, {});
2209
- expect(runtime).toBeInstanceOf(ModelRuntime);
2210
- expect(runtime['_runtime']).toBeInstanceOf(LobeAnthropicAI);
2211
- });
2212
-
2213
- it('Mistral provider: with apiKey', async () => {
2214
- merge(initialSettingsState, {
2215
- settings: {
2216
- keyVaults: {
2217
- mistral: {
2218
- apiKey: 'user-mistral-key',
2219
- },
2220
- },
2221
- },
2222
- } as UserSettingsState) as unknown as UserStore;
2223
- const runtime = await initializeWithClientStore(ModelProvider.Mistral, {});
2224
- expect(runtime).toBeInstanceOf(ModelRuntime);
2225
- expect(runtime['_runtime']).toBeInstanceOf(LobeMistralAI);
2226
- });
2227
-
2228
- it('OpenRouter provider: with apiKey', async () => {
2229
- merge(initialSettingsState, {
2230
- settings: {
2231
- keyVaults: {
2232
- openrouter: {
2233
- apiKey: 'user-openrouter-key',
2234
- },
2235
- },
2236
- },
2237
- } as UserSettingsState) as unknown as UserStore;
2238
- const runtime = await initializeWithClientStore(ModelProvider.OpenRouter, {});
2239
- expect(runtime).toBeInstanceOf(ModelRuntime);
2240
- expect(runtime['_runtime']).toBeInstanceOf(LobeOpenRouterAI);
2241
- });
2242
-
2243
- it('TogetherAI provider: with apiKey', async () => {
2244
- merge(initialSettingsState, {
2245
- settings: {
2246
- keyVaults: {
2247
- togetherai: {
2248
- apiKey: 'user-togetherai-key',
2249
- },
2250
- },
2251
- },
2252
- } as UserSettingsState) as unknown as UserStore;
2253
- const runtime = await initializeWithClientStore(ModelProvider.TogetherAI, {});
2254
- expect(runtime).toBeInstanceOf(ModelRuntime);
2255
- expect(runtime['_runtime']).toBeInstanceOf(LobeTogetherAI);
2256
- });
2257
-
2258
- it('ZeroOneAI provider: with apiKey', async () => {
2259
- merge(initialSettingsState, {
2260
- settings: {
2261
- keyVaults: {
2262
- zeroone: {
2263
- apiKey: 'user-zeroone-key',
2264
- },
2265
- },
2266
- },
2267
- } as UserSettingsState) as unknown as UserStore;
2268
- const runtime = await initializeWithClientStore(ModelProvider.ZeroOne, {});
2269
- expect(runtime).toBeInstanceOf(ModelRuntime);
2270
- expect(runtime['_runtime']).toBeInstanceOf(LobeZeroOneAI);
2271
- });
2272
-
2273
- it('Groq provider: with apiKey,endpoint', async () => {
2274
- merge(initialSettingsState, {
2275
- settings: {
2276
- keyVaults: {
2277
- groq: {
2278
- apiKey: 'user-groq-key',
2279
- baseURL: 'user-groq-endpoint',
2280
- },
2281
- },
2282
- },
2283
- } as UserSettingsState) as unknown as UserStore;
2284
- const runtime = await initializeWithClientStore(ModelProvider.Groq, {});
2285
- expect(runtime).toBeInstanceOf(ModelRuntime);
2286
- const lobeOpenAICompatibleInstance = runtime['_runtime'] as LobeOpenAICompatibleRuntime;
2287
- expect(lobeOpenAICompatibleInstance).toBeInstanceOf(LobeGroq);
2288
- expect(lobeOpenAICompatibleInstance.baseURL).toBe('user-groq-endpoint');
2289
- expect(lobeOpenAICompatibleInstance.client).toBeInstanceOf(OpenAI);
2290
- expect(lobeOpenAICompatibleInstance.client.apiKey).toBe('user-groq-key');
2291
- });
2292
-
2293
- it('DeepSeek provider: with apiKey', async () => {
2294
- merge(initialSettingsState, {
2295
- settings: {
2296
- keyVaults: {
2297
- deepseek: {
2298
- apiKey: 'user-deepseek-key',
2299
- },
2300
- },
2301
- },
2302
- } as UserSettingsState) as unknown as UserStore;
2303
- const runtime = await initializeWithClientStore(ModelProvider.DeepSeek, {});
2304
- expect(runtime).toBeInstanceOf(ModelRuntime);
2305
- expect(runtime['_runtime']).toBeInstanceOf(LobeDeepSeekAI);
2306
- });
2307
-
2308
- it('Qwen provider: with apiKey', async () => {
2309
- merge(initialSettingsState, {
2310
- settings: {
2311
- keyVaults: {
2312
- qwen: {
2313
- apiKey: 'user-qwen-key',
2314
- },
2315
- },
2316
- },
2317
- } as UserSettingsState) as unknown as UserStore;
2318
- const runtime = await initializeWithClientStore(ModelProvider.Qwen, {});
2319
- expect(runtime).toBeInstanceOf(ModelRuntime);
2320
- expect(runtime['_runtime']).toBeInstanceOf(LobeQwenAI);
2321
- });
2322
-
2323
- /**
2324
- * Should not have a unknown provider in client, but has
2325
- * similar cases in server side
2326
- */
2327
- it('Unknown provider: with apiKey', async () => {
2328
- merge(initialSettingsState, {
2329
- settings: {
2330
- keyVaults: {
2331
- unknown: {
2332
- apiKey: 'user-unknown-key',
2333
- endpoint: 'user-unknown-endpoint',
2334
- },
2335
- },
2336
- },
2337
- } as any as UserSettingsState) as unknown as UserStore;
2338
- const runtime = await initializeWithClientStore('unknown' as ModelProvider, {});
2339
- expect(runtime).toBeInstanceOf(ModelRuntime);
2340
- expect(runtime['_runtime']).toBeInstanceOf(LobeOpenAI);
2341
- });
2342
-
2343
- /**
2344
- * The following test cases need to be enforce
2345
- */
2346
-
2347
- it('ZhiPu AI provider: with apiKey', async () => {
2348
- // Mock the generateApiToken function
2349
- vi.mock('@/libs/model-runtime/zhipu/authToken', () => ({
2350
- generateApiToken: vi
2351
- .fn()
2352
- .mockResolvedValue(
2353
- 'eyJhbGciOiJIUzI1NiIsInNpZ25fdHlwZSI6IlNJR04iLCJ0eXAiOiJKV1QifQ.eyJhcGlfa2V5IjoiemhpcHUiLCJleHAiOjE3MTU5MTc2NzMsImlhdCI6MTcxMzMyNTY3M30.gt8o-hUDvJFPJLYcH4EhrT1LAmTXI8YnybHeQjpD9oM',
2354
- ),
2355
- }));
2356
- merge(initialSettingsState, {
2357
- settings: {
2358
- keyVaults: {
2359
- zhipu: {
2360
- apiKey: 'zhipu.user-key',
2361
- },
2362
- },
2363
- },
2364
- } as UserSettingsState) as unknown as UserStore;
2365
- const runtime = await initializeWithClientStore(ModelProvider.ZhiPu, {});
2366
- expect(runtime).toBeInstanceOf(ModelRuntime);
2367
- expect(runtime['_runtime']).toBeInstanceOf(LobeZhipuAI);
2368
- });
2369
- });
2370
- });
2371
- });