@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/ar/components.json +2 -0
  4. package/locales/ar/models.json +3 -0
  5. package/locales/bg-BG/components.json +2 -0
  6. package/locales/bg-BG/models.json +3 -0
  7. package/locales/de-DE/components.json +2 -0
  8. package/locales/de-DE/models.json +3 -0
  9. package/locales/en-US/components.json +2 -0
  10. package/locales/en-US/models.json +3 -0
  11. package/locales/es-ES/components.json +2 -0
  12. package/locales/es-ES/models.json +3 -0
  13. package/locales/fa-IR/components.json +2 -0
  14. package/locales/fa-IR/models.json +3 -0
  15. package/locales/fr-FR/components.json +2 -0
  16. package/locales/fr-FR/models.json +3 -0
  17. package/locales/it-IT/components.json +2 -0
  18. package/locales/it-IT/models.json +3 -0
  19. package/locales/ja-JP/components.json +2 -0
  20. package/locales/ja-JP/models.json +3 -0
  21. package/locales/ko-KR/components.json +2 -0
  22. package/locales/ko-KR/models.json +3 -0
  23. package/locales/nl-NL/components.json +2 -0
  24. package/locales/nl-NL/models.json +3 -0
  25. package/locales/pl-PL/components.json +2 -0
  26. package/locales/pl-PL/models.json +3 -0
  27. package/locales/pt-BR/components.json +2 -0
  28. package/locales/pt-BR/models.json +3 -0
  29. package/locales/ru-RU/components.json +2 -0
  30. package/locales/ru-RU/models.json +3 -0
  31. package/locales/tr-TR/components.json +2 -0
  32. package/locales/tr-TR/models.json +3 -0
  33. package/locales/vi-VN/components.json +2 -0
  34. package/locales/vi-VN/models.json +3 -0
  35. package/locales/zh-CN/components.json +3 -1
  36. package/locales/zh-CN/models.json +3 -0
  37. package/locales/zh-TW/components.json +2 -0
  38. package/locales/zh-TW/models.json +3 -0
  39. package/package.json +3 -3
  40. package/packages/web-crawler/package.json +1 -1
  41. package/packages/web-crawler/src/crawImpl/__tests__/browserless.test.ts +1 -1
  42. package/packages/web-crawler/src/crawImpl/browserless.ts +1 -1
  43. package/packages/web-crawler/src/crawImpl/naive.ts +1 -1
  44. package/packages/web-crawler/src/utils/__snapshots__/htmlToMarkdown.test.ts.snap +2 -382
  45. package/packages/web-crawler/src/utils/htmlToMarkdown.ts +32 -2
  46. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/AgentsSuggest.tsx +5 -1
  47. package/src/config/modelProviders/openai.ts +3 -0
  48. package/src/database/client/db.ts +3 -3
  49. package/src/features/Conversation/components/MarkdownElements/LobeArtifact/Render/index.tsx +51 -52
  50. package/src/features/ModelSwitchPanel/index.tsx +37 -8
  51. package/src/libs/agent-runtime/anthropic/index.ts +5 -2
  52. package/src/locales/default/components.ts +3 -1
  53. package/src/services/__tests__/chat.test.ts +123 -0
  54. package/src/services/chat.ts +19 -19
  55. package/src/utils/fetch/__tests__/fetchSSE.test.ts +3 -2
  56. 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
- <ProviderItemRender
95
- logo={provider.logo}
96
- name={provider.name}
97
- provider={provider.id}
98
- source={provider.source}
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
- max_tokens ?? (thinking?.budget_tokens ? thinking?.budget_tokens + 4096 : 4096);
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
- max_tokens: max_tokens ?? 4096,
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,
@@ -87,7 +87,9 @@ export default {
87
87
  },
88
88
  ModelSwitchPanel: {
89
89
  emptyModel: '没有启用的模型,请前往设置开启',
90
- provider: '提供商',
90
+ emptyProvider: '没有启用的服务商,请前往设置开启',
91
+ goToSettings: '前往设置',
92
+ provider: '服务商',
91
93
  },
92
94
  OllamaSetupGuide: {
93
95
  cors: {
@@ -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', () => {
@@ -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 = useAiInfraStore
78
- .getState()
79
- .enabledAiModels?.find((i) => i.id === model && i.providerId === provider);
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)(useAiInfraStore.getState());
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
- )(useAiInfraStore.getState());
179
+ )(getAiInfraStoreState());
185
180
 
186
- const useApplicationBuiltinSearchTool =
187
- enabledSearch && !(isModelHasBuiltinSearch && chatConfig.useModelBuiltinSearch);
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
- )(useAiInfraStore.getState());
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
- )(useAiInfraStore.getState());
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 && isModelHasBuiltinSearch ? true : undefined,
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 = aiProviderSelectors.providerConfigById(provider)(
355
- useAiInfraStore.getState(),
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;