@lobehub/chat 1.69.5 → 1.69.6
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/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/locales/ar/components.json +2 -0
- package/locales/ar/models.json +3 -0
- package/locales/bg-BG/components.json +2 -0
- package/locales/bg-BG/models.json +3 -0
- package/locales/de-DE/components.json +2 -0
- package/locales/de-DE/models.json +3 -0
- package/locales/en-US/components.json +2 -0
- package/locales/en-US/models.json +3 -0
- package/locales/es-ES/components.json +2 -0
- package/locales/es-ES/models.json +3 -0
- package/locales/fa-IR/components.json +2 -0
- package/locales/fa-IR/models.json +3 -0
- package/locales/fr-FR/components.json +2 -0
- package/locales/fr-FR/models.json +3 -0
- package/locales/it-IT/components.json +2 -0
- package/locales/it-IT/models.json +3 -0
- package/locales/ja-JP/components.json +2 -0
- package/locales/ja-JP/models.json +3 -0
- package/locales/ko-KR/components.json +2 -0
- package/locales/ko-KR/models.json +3 -0
- package/locales/nl-NL/components.json +2 -0
- package/locales/nl-NL/models.json +3 -0
- package/locales/pl-PL/components.json +2 -0
- package/locales/pl-PL/models.json +3 -0
- package/locales/pt-BR/components.json +2 -0
- package/locales/pt-BR/models.json +3 -0
- package/locales/ru-RU/components.json +2 -0
- package/locales/ru-RU/models.json +3 -0
- package/locales/tr-TR/components.json +2 -0
- package/locales/tr-TR/models.json +3 -0
- package/locales/vi-VN/components.json +2 -0
- package/locales/vi-VN/models.json +3 -0
- package/locales/zh-CN/components.json +3 -1
- package/locales/zh-CN/models.json +3 -0
- package/locales/zh-TW/components.json +2 -0
- package/locales/zh-TW/models.json +3 -0
- package/package.json +3 -3
- package/packages/web-crawler/package.json +1 -1
- package/packages/web-crawler/src/crawImpl/__tests__/browserless.test.ts +1 -1
- package/packages/web-crawler/src/crawImpl/browserless.ts +1 -1
- package/packages/web-crawler/src/crawImpl/naive.ts +1 -1
- package/packages/web-crawler/src/utils/__snapshots__/htmlToMarkdown.test.ts.snap +2 -382
- package/packages/web-crawler/src/utils/htmlToMarkdown.ts +32 -2
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/AgentsSuggest.tsx +5 -1
- package/src/config/modelProviders/openai.ts +3 -0
- package/src/database/client/db.ts +3 -3
- package/src/features/Conversation/components/MarkdownElements/LobeArtifact/Render/index.tsx +51 -52
- package/src/features/ModelSwitchPanel/index.tsx +37 -8
- package/src/libs/agent-runtime/anthropic/index.ts +5 -2
- package/src/locales/default/components.ts +3 -1
- package/src/services/__tests__/chat.test.ts +123 -0
- package/src/services/chat.ts +19 -19
- package/src/utils/fetch/__tests__/fetchSSE.test.ts +3 -2
- package/src/utils/fetch/fetchSSE.ts +1 -1
@@ -1,8 +1,9 @@
|
|
1
|
-
import { Icon } from '@lobehub/ui';
|
1
|
+
import { ActionIcon, Icon } from '@lobehub/ui';
|
2
2
|
import { Dropdown } from 'antd';
|
3
3
|
import { createStyles } from 'antd-style';
|
4
4
|
import type { ItemType } from 'antd/es/menu/interface';
|
5
|
-
import { LucideArrowRight } from 'lucide-react';
|
5
|
+
import { LucideArrowRight, LucideBolt } from 'lucide-react';
|
6
|
+
import Link from 'next/link';
|
6
7
|
import { useRouter } from 'next/navigation';
|
7
8
|
import { PropsWithChildren, memo, useMemo } from 'react';
|
8
9
|
import { useTranslation } from 'react-i18next';
|
@@ -86,17 +87,45 @@ const ModelSwitchPanel = memo<PropsWithChildren>(({ children }) => {
|
|
86
87
|
return items;
|
87
88
|
};
|
88
89
|
|
90
|
+
if (enabledList.length === 0)
|
91
|
+
return [
|
92
|
+
{
|
93
|
+
key: `no-provider`,
|
94
|
+
label: (
|
95
|
+
<Flexbox gap={8} horizontal style={{ color: theme.colorTextTertiary }}>
|
96
|
+
{t('ModelSwitchPanel.emptyProvider')}
|
97
|
+
<Icon icon={LucideArrowRight} />
|
98
|
+
</Flexbox>
|
99
|
+
),
|
100
|
+
onClick: () => {
|
101
|
+
router.push(isDeprecatedEdition ? '/settings/llm' : `/settings/provider`);
|
102
|
+
},
|
103
|
+
},
|
104
|
+
];
|
105
|
+
|
89
106
|
// otherwise show with provider group
|
90
107
|
return enabledList.map((provider) => ({
|
91
108
|
children: getModelItems(provider),
|
92
109
|
key: provider.id,
|
93
110
|
label: (
|
94
|
-
<
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
111
|
+
<Flexbox horizontal justify="space-between">
|
112
|
+
<ProviderItemRender
|
113
|
+
logo={provider.logo}
|
114
|
+
name={provider.name}
|
115
|
+
provider={provider.id}
|
116
|
+
source={provider.source}
|
117
|
+
/>
|
118
|
+
<Link
|
119
|
+
href={isDeprecatedEdition ? '/settings/llm' : `/settings/provider/${provider.id}`}
|
120
|
+
prefetch={false}
|
121
|
+
>
|
122
|
+
<ActionIcon
|
123
|
+
icon={LucideBolt}
|
124
|
+
size={'small'}
|
125
|
+
title={t('ModelSwitchPanel.goToSettings')}
|
126
|
+
/>
|
127
|
+
</Link>
|
128
|
+
</Flexbox>
|
100
129
|
),
|
101
130
|
type: 'group',
|
102
131
|
}));
|
@@ -102,7 +102,8 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
|
|
102
102
|
|
103
103
|
if (!!thinking) {
|
104
104
|
const maxTokens =
|
105
|
-
|
105
|
+
// claude 3.7 thinking has max output of 64000 tokens
|
106
|
+
max_tokens ?? (thinking?.budget_tokens ? thinking?.budget_tokens + 64_000 : 8192);
|
106
107
|
|
107
108
|
// `temperature` may only be set to 1 when thinking is enabled.
|
108
109
|
// `top_p` must be unset when thinking is enabled.
|
@@ -117,7 +118,9 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
|
|
117
118
|
}
|
118
119
|
|
119
120
|
return {
|
120
|
-
|
121
|
+
// claude 3 series model hax max output token of 4096, 3.x series has 8192
|
122
|
+
// https://docs.anthropic.com/en/docs/about-claude/models/all-models#:~:text=200K-,Max%20output,-Normal%3A
|
123
|
+
max_tokens: max_tokens ?? (model.startsWith('claude-3-') ? 4096 : 8192),
|
121
124
|
messages: postMessages,
|
122
125
|
model,
|
123
126
|
system: systemPrompts,
|
@@ -26,12 +26,16 @@ import {
|
|
26
26
|
ModelProvider,
|
27
27
|
} from '@/libs/agent-runtime';
|
28
28
|
import { AgentRuntime } from '@/libs/agent-runtime';
|
29
|
+
import { agentChatConfigSelectors } from '@/store/agent/selectors';
|
30
|
+
import { aiModelSelectors } from '@/store/aiInfra';
|
29
31
|
import { useToolStore } from '@/store/tool';
|
32
|
+
import { toolSelectors } from '@/store/tool/selectors';
|
30
33
|
import { UserStore } from '@/store/user';
|
31
34
|
import { useUserStore } from '@/store/user';
|
32
35
|
import { modelConfigSelectors } from '@/store/user/selectors';
|
33
36
|
import { UserSettingsState, initialSettingsState } from '@/store/user/slices/settings/initialState';
|
34
37
|
import { DalleManifest } from '@/tools/dalle';
|
38
|
+
import { WebBrowsingManifest } from '@/tools/web-browsing';
|
35
39
|
import { ChatMessage } from '@/types/message';
|
36
40
|
import { ChatStreamPayload, type OpenAIChatMessage } from '@/types/openai/chat';
|
37
41
|
import { LobeTool } from '@/types/tool';
|
@@ -480,6 +484,125 @@ describe('ChatService', () => {
|
|
480
484
|
expect(calls![1]).toBeUndefined();
|
481
485
|
});
|
482
486
|
});
|
487
|
+
|
488
|
+
describe('search functionality', () => {
|
489
|
+
it('should add WebBrowsingManifest when search is enabled and not using model built-in search', async () => {
|
490
|
+
const getChatCompletionSpy = vi.spyOn(chatService, 'getChatCompletion');
|
491
|
+
|
492
|
+
const messages = [{ content: 'Search for something', role: 'user' }] as ChatMessage[];
|
493
|
+
|
494
|
+
// Mock agent store state with search enabled
|
495
|
+
vi.spyOn(agentChatConfigSelectors, 'currentChatConfig').mockReturnValueOnce({
|
496
|
+
searchMode: 'auto', // not 'off'
|
497
|
+
useModelBuiltinSearch: false,
|
498
|
+
} as any);
|
499
|
+
|
500
|
+
// Mock AI infra store state
|
501
|
+
vi.spyOn(aiModelSelectors, 'isModelHasBuiltinSearch').mockReturnValueOnce(() => false);
|
502
|
+
vi.spyOn(aiModelSelectors, 'isModelHasExtendParams').mockReturnValueOnce(() => false);
|
503
|
+
|
504
|
+
// Mock tool selectors
|
505
|
+
vi.spyOn(toolSelectors, 'enabledSchema').mockReturnValueOnce(() => [
|
506
|
+
{
|
507
|
+
type: 'function',
|
508
|
+
function: {
|
509
|
+
name: WebBrowsingManifest.identifier + '____search',
|
510
|
+
description: 'Search the web',
|
511
|
+
},
|
512
|
+
},
|
513
|
+
]);
|
514
|
+
|
515
|
+
await chatService.createAssistantMessage({ messages, plugins: [] });
|
516
|
+
|
517
|
+
// Verify tools were passed to getChatCompletion
|
518
|
+
expect(getChatCompletionSpy).toHaveBeenCalledWith(
|
519
|
+
expect.objectContaining({
|
520
|
+
tools: expect.arrayContaining([
|
521
|
+
expect.objectContaining({
|
522
|
+
function: expect.objectContaining({
|
523
|
+
name: expect.stringContaining(WebBrowsingManifest.identifier),
|
524
|
+
}),
|
525
|
+
}),
|
526
|
+
]),
|
527
|
+
}),
|
528
|
+
undefined,
|
529
|
+
);
|
530
|
+
});
|
531
|
+
|
532
|
+
it('should enable built-in search when model supports it and useModelBuiltinSearch is true', async () => {
|
533
|
+
const getChatCompletionSpy = vi.spyOn(chatService, 'getChatCompletion');
|
534
|
+
|
535
|
+
const messages = [{ content: 'Search for something', role: 'user' }] as ChatMessage[];
|
536
|
+
|
537
|
+
// Mock agent store state with search enabled and useModelBuiltinSearch enabled
|
538
|
+
vi.spyOn(agentChatConfigSelectors, 'currentChatConfig').mockReturnValueOnce({
|
539
|
+
searchMode: 'auto', // not 'off'
|
540
|
+
useModelBuiltinSearch: true,
|
541
|
+
} as any);
|
542
|
+
|
543
|
+
// Mock AI infra store state - model has built-in search
|
544
|
+
vi.spyOn(aiModelSelectors, 'isModelHasBuiltinSearch').mockReturnValueOnce(() => true);
|
545
|
+
vi.spyOn(aiModelSelectors, 'isModelHasExtendParams').mockReturnValueOnce(() => false);
|
546
|
+
|
547
|
+
// Mock tool selectors
|
548
|
+
vi.spyOn(toolSelectors, 'enabledSchema').mockReturnValueOnce(() => [
|
549
|
+
{
|
550
|
+
type: 'function',
|
551
|
+
function: {
|
552
|
+
name: WebBrowsingManifest.identifier + '____search',
|
553
|
+
description: 'Search the web',
|
554
|
+
},
|
555
|
+
},
|
556
|
+
]);
|
557
|
+
|
558
|
+
await chatService.createAssistantMessage({ messages, plugins: [] });
|
559
|
+
|
560
|
+
// Verify enabledSearch was set to true
|
561
|
+
expect(getChatCompletionSpy).toHaveBeenCalledWith(
|
562
|
+
expect.objectContaining({
|
563
|
+
enabledSearch: true,
|
564
|
+
}),
|
565
|
+
undefined,
|
566
|
+
);
|
567
|
+
});
|
568
|
+
|
569
|
+
it('should not enable search when searchMode is off', async () => {
|
570
|
+
const getChatCompletionSpy = vi.spyOn(chatService, 'getChatCompletion');
|
571
|
+
|
572
|
+
const messages = [{ content: 'Search for something', role: 'user' }] as ChatMessage[];
|
573
|
+
|
574
|
+
// Mock agent store state with search disabled
|
575
|
+
vi.spyOn(agentChatConfigSelectors, 'currentChatConfig').mockReturnValueOnce({
|
576
|
+
searchMode: 'off',
|
577
|
+
useModelBuiltinSearch: true,
|
578
|
+
} as any);
|
579
|
+
|
580
|
+
// Mock AI infra store state
|
581
|
+
vi.spyOn(aiModelSelectors, 'isModelHasBuiltinSearch').mockReturnValueOnce(() => true);
|
582
|
+
vi.spyOn(aiModelSelectors, 'isModelHasExtendParams').mockReturnValueOnce(() => false);
|
583
|
+
|
584
|
+
// Mock tool selectors
|
585
|
+
vi.spyOn(toolSelectors, 'enabledSchema').mockReturnValueOnce(() => [
|
586
|
+
{
|
587
|
+
type: 'function',
|
588
|
+
function: {
|
589
|
+
name: WebBrowsingManifest.identifier + '____search',
|
590
|
+
description: 'Search the web',
|
591
|
+
},
|
592
|
+
},
|
593
|
+
]);
|
594
|
+
|
595
|
+
await chatService.createAssistantMessage({ messages, plugins: [] });
|
596
|
+
|
597
|
+
// Verify enabledSearch was not set
|
598
|
+
expect(getChatCompletionSpy).toHaveBeenCalledWith(
|
599
|
+
expect.objectContaining({
|
600
|
+
enabledSearch: undefined,
|
601
|
+
}),
|
602
|
+
undefined,
|
603
|
+
);
|
604
|
+
});
|
605
|
+
});
|
483
606
|
});
|
484
607
|
|
485
608
|
describe('getChatCompletion', () => {
|
package/src/services/chat.ts
CHANGED
@@ -19,12 +19,7 @@ import { filesPrompts } from '@/prompts/files';
|
|
19
19
|
import { BuiltinSystemRolePrompts } from '@/prompts/systemRole';
|
20
20
|
import { getAgentStoreState } from '@/store/agent';
|
21
21
|
import { agentChatConfigSelectors } from '@/store/agent/selectors';
|
22
|
-
import {
|
23
|
-
aiModelSelectors,
|
24
|
-
aiProviderSelectors,
|
25
|
-
getAiInfraStoreState,
|
26
|
-
useAiInfraStore,
|
27
|
-
} from '@/store/aiInfra';
|
22
|
+
import { aiModelSelectors, aiProviderSelectors, getAiInfraStoreState } from '@/store/aiInfra';
|
28
23
|
import { getSessionStoreState } from '@/store/session';
|
29
24
|
import { sessionMetaSelectors } from '@/store/session/selectors';
|
30
25
|
import { getToolStoreState } from '@/store/tool';
|
@@ -74,9 +69,9 @@ const findDeploymentName = (model: string, provider: string) => {
|
|
74
69
|
if (deploymentName) deploymentId = deploymentName;
|
75
70
|
} else {
|
76
71
|
// find the model by id
|
77
|
-
const modelItem =
|
78
|
-
|
79
|
-
|
72
|
+
const modelItem = getAiInfraStoreState().enabledAiModels?.find(
|
73
|
+
(i) => i.id === model && i.providerId === provider,
|
74
|
+
);
|
80
75
|
|
81
76
|
if (modelItem && modelItem.config?.deploymentName) {
|
82
77
|
deploymentId = modelItem.config?.deploymentName;
|
@@ -91,7 +86,7 @@ const isEnableFetchOnClient = (provider: string) => {
|
|
91
86
|
if (isDeprecatedEdition) {
|
92
87
|
return modelConfigSelectors.isProviderFetchOnClient(provider)(useUserStore.getState());
|
93
88
|
} else {
|
94
|
-
return aiProviderSelectors.isProviderFetchOnClient(provider)(
|
89
|
+
return aiProviderSelectors.isProviderFetchOnClient(provider)(getAiInfraStoreState());
|
95
90
|
}
|
96
91
|
};
|
97
92
|
|
@@ -181,10 +176,10 @@ class ChatService {
|
|
181
176
|
const isModelHasBuiltinSearch = aiModelSelectors.isModelHasBuiltinSearch(
|
182
177
|
payload.model,
|
183
178
|
payload.provider!,
|
184
|
-
)(
|
179
|
+
)(getAiInfraStoreState());
|
185
180
|
|
186
|
-
const
|
187
|
-
|
181
|
+
const useModelSearch = isModelHasBuiltinSearch && chatConfig.useModelBuiltinSearch;
|
182
|
+
const useApplicationBuiltinSearchTool = enabledSearch && !useModelSearch;
|
188
183
|
|
189
184
|
const pluginIds = [...(enabledPlugins || [])];
|
190
185
|
|
@@ -225,14 +220,14 @@ class ChatService {
|
|
225
220
|
const isModelHasExtendParams = aiModelSelectors.isModelHasExtendParams(
|
226
221
|
payload.model,
|
227
222
|
payload.provider!,
|
228
|
-
)(
|
223
|
+
)(getAiInfraStoreState());
|
229
224
|
|
230
225
|
// model
|
231
226
|
if (isModelHasExtendParams) {
|
232
227
|
const modelExtendParams = aiModelSelectors.modelExtendParams(
|
233
228
|
payload.model,
|
234
229
|
payload.provider!,
|
235
|
-
)(
|
230
|
+
)(getAiInfraStoreState());
|
236
231
|
// if model has extended params, then we need to check if the model can use reasoning
|
237
232
|
|
238
233
|
if (modelExtendParams!.includes('enableReasoning') && chatConfig.enableReasoning) {
|
@@ -241,13 +236,19 @@ class ChatService {
|
|
241
236
|
type: 'enabled',
|
242
237
|
};
|
243
238
|
}
|
239
|
+
if (
|
240
|
+
modelExtendParams!.includes('disableContextCaching') &&
|
241
|
+
chatConfig.disableContextCaching
|
242
|
+
) {
|
243
|
+
extendParams.enabledContextCaching = false;
|
244
|
+
}
|
244
245
|
}
|
245
246
|
|
246
247
|
return this.getChatCompletion(
|
247
248
|
{
|
248
249
|
...params,
|
249
250
|
...extendParams,
|
250
|
-
enabledSearch: enabledSearch &&
|
251
|
+
enabledSearch: enabledSearch && useModelSearch ? true : undefined,
|
251
252
|
messages: oaiMessages,
|
252
253
|
tools,
|
253
254
|
},
|
@@ -351,9 +352,8 @@ class ChatService {
|
|
351
352
|
|
352
353
|
// TODO: remove `!isDeprecatedEdition` condition in V2.0
|
353
354
|
if (!isDeprecatedEdition && !isBuiltin) {
|
354
|
-
const providerConfig =
|
355
|
-
|
356
|
-
);
|
355
|
+
const providerConfig =
|
356
|
+
aiProviderSelectors.providerConfigById(provider)(getAiInfraStoreState());
|
357
357
|
|
358
358
|
sdkType = providerConfig?.settings.sdkType || 'openai';
|
359
359
|
}
|
@@ -159,7 +159,7 @@ describe('fetchSSE', () => {
|
|
159
159
|
expectedMessages.forEach((message, index) => {
|
160
160
|
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(index + 1, message);
|
161
161
|
});
|
162
|
-
|
162
|
+
|
163
163
|
// more assertions for each character...
|
164
164
|
expect(mockOnFinish).toHaveBeenCalledWith('Hello World', {
|
165
165
|
observationId: null,
|
@@ -188,6 +188,7 @@ describe('fetchSSE', () => {
|
|
188
188
|
await fetchSSE('/', {
|
189
189
|
onMessageHandle: mockOnMessageHandle,
|
190
190
|
onFinish: mockOnFinish,
|
191
|
+
smoothing: false,
|
191
192
|
});
|
192
193
|
|
193
194
|
expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hello', type: 'reasoning' });
|
@@ -225,7 +226,7 @@ describe('fetchSSE', () => {
|
|
225
226
|
grounding: 'Hello',
|
226
227
|
type: 'grounding',
|
227
228
|
});
|
228
|
-
|
229
|
+
|
229
230
|
expect(mockOnFinish).toHaveBeenCalledWith('hi', {
|
230
231
|
observationId: null,
|
231
232
|
toolCalls: undefined,
|
@@ -296,7 +296,7 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
|
|
296
296
|
|
297
297
|
const { smoothing } = options;
|
298
298
|
|
299
|
-
const textSmoothing = typeof smoothing === 'boolean' ? smoothing : smoothing?.text;
|
299
|
+
const textSmoothing = typeof smoothing === 'boolean' ? smoothing : (smoothing?.text ?? true);
|
300
300
|
const toolsCallingSmoothing =
|
301
301
|
typeof smoothing === 'boolean' ? smoothing : (smoothing?.toolsCalling ?? true);
|
302
302
|
const smoothingSpeed = isObject(smoothing) ? smoothing.speed : undefined;
|