@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.
- package/.github/workflows/test.yml +8 -1
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/next.config.ts +8 -1
- package/package.json +71 -69
- package/packages/context-engine/ARCHITECTURE.md +425 -0
- package/packages/context-engine/package.json +40 -0
- package/packages/context-engine/src/base/BaseProcessor.ts +87 -0
- package/packages/context-engine/src/base/BaseProvider.ts +22 -0
- package/packages/context-engine/src/index.ts +32 -0
- package/packages/context-engine/src/pipeline.ts +219 -0
- package/packages/context-engine/src/processors/HistoryTruncate.ts +76 -0
- package/packages/context-engine/src/processors/InputTemplate.ts +83 -0
- package/packages/context-engine/src/processors/MessageCleanup.ts +87 -0
- package/packages/context-engine/src/processors/MessageContent.ts +298 -0
- package/packages/context-engine/src/processors/PlaceholderVariables.ts +196 -0
- package/packages/context-engine/src/processors/ToolCall.ts +186 -0
- package/packages/context-engine/src/processors/ToolMessageReorder.ts +113 -0
- package/packages/context-engine/src/processors/__tests__/HistoryTruncate.test.ts +175 -0
- package/packages/context-engine/src/processors/__tests__/InputTemplate.test.ts +243 -0
- package/packages/context-engine/src/processors/__tests__/MessageContent.test.ts +394 -0
- package/packages/context-engine/src/processors/__tests__/PlaceholderVariables.test.ts +334 -0
- package/packages/context-engine/src/processors/__tests__/ToolMessageReorder.test.ts +186 -0
- package/packages/context-engine/src/processors/index.ts +15 -0
- package/packages/context-engine/src/providers/HistorySummary.ts +102 -0
- package/packages/context-engine/src/providers/InboxGuide.ts +102 -0
- package/packages/context-engine/src/providers/SystemRoleInjector.ts +64 -0
- package/packages/context-engine/src/providers/ToolSystemRole.ts +118 -0
- package/packages/context-engine/src/providers/__tests__/HistorySummaryProvider.test.ts +112 -0
- package/packages/context-engine/src/providers/__tests__/InboxGuideProvider.test.ts +121 -0
- package/packages/context-engine/src/providers/__tests__/SystemRoleInjector.test.ts +200 -0
- package/packages/context-engine/src/providers/__tests__/ToolSystemRoleProvider.test.ts +140 -0
- package/packages/context-engine/src/providers/index.ts +11 -0
- package/packages/context-engine/src/types.ts +201 -0
- package/packages/context-engine/vitest.config.mts +10 -0
- package/packages/database/package.json +1 -1
- package/packages/prompts/src/prompts/systemRole/index.ts +1 -1
- package/packages/utils/src/index.ts +2 -0
- package/packages/utils/src/uriParser.test.ts +29 -0
- package/packages/utils/src/uriParser.ts +24 -0
- package/src/services/{__tests__ → chat}/chat.test.ts +22 -1032
- package/src/services/chat/clientModelRuntime.test.ts +385 -0
- package/src/services/chat/clientModelRuntime.ts +34 -0
- package/src/services/chat/contextEngineering.test.ts +848 -0
- package/src/services/chat/contextEngineering.ts +123 -0
- package/src/services/chat/helper.ts +61 -0
- package/src/services/{chat.ts → chat/index.ts} +24 -366
- package/src/services/chat/types.ts +9 -0
- package/src/services/models.ts +1 -1
- package/src/store/aiInfra/slices/aiModel/selectors.ts +2 -2
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +1 -40
- /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
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
-
});
|