@lobehub/chat 1.64.2 → 1.65.0

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 (62) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/changelog/v1.json +21 -0
  3. package/locales/ar/chat.json +7 -1
  4. package/locales/ar/models.json +6 -0
  5. package/locales/bg-BG/chat.json +7 -1
  6. package/locales/bg-BG/models.json +6 -0
  7. package/locales/de-DE/chat.json +7 -1
  8. package/locales/de-DE/models.json +6 -0
  9. package/locales/en-US/chat.json +7 -1
  10. package/locales/en-US/models.json +6 -0
  11. package/locales/es-ES/chat.json +8 -2
  12. package/locales/es-ES/models.json +6 -0
  13. package/locales/fa-IR/chat.json +7 -1
  14. package/locales/fa-IR/models.json +6 -0
  15. package/locales/fr-FR/chat.json +7 -1
  16. package/locales/fr-FR/models.json +6 -0
  17. package/locales/it-IT/chat.json +7 -1
  18. package/locales/it-IT/models.json +6 -0
  19. package/locales/ja-JP/chat.json +7 -1
  20. package/locales/ja-JP/models.json +6 -0
  21. package/locales/ko-KR/chat.json +7 -1
  22. package/locales/ko-KR/models.json +6 -0
  23. package/locales/nl-NL/chat.json +8 -2
  24. package/locales/nl-NL/models.json +6 -0
  25. package/locales/pl-PL/chat.json +7 -1
  26. package/locales/pl-PL/models.json +6 -0
  27. package/locales/pt-BR/chat.json +7 -1
  28. package/locales/pt-BR/models.json +6 -0
  29. package/locales/ru-RU/chat.json +8 -2
  30. package/locales/ru-RU/models.json +6 -0
  31. package/locales/tr-TR/chat.json +7 -1
  32. package/locales/tr-TR/models.json +6 -0
  33. package/locales/vi-VN/chat.json +7 -1
  34. package/locales/vi-VN/models.json +6 -0
  35. package/locales/zh-CN/chat.json +7 -1
  36. package/locales/zh-CN/models.json +6 -0
  37. package/locales/zh-TW/chat.json +7 -1
  38. package/locales/zh-TW/models.json +6 -0
  39. package/package.json +2 -2
  40. package/src/config/aiModels/anthropic.ts +26 -0
  41. package/src/config/aiModels/bedrock.ts +23 -2
  42. package/src/config/aiModels/google.ts +7 -0
  43. package/src/config/modelProviders/anthropic.ts +34 -0
  44. package/src/config/modelProviders/bedrock.ts +51 -0
  45. package/src/const/settings/agent.ts +2 -0
  46. package/src/features/ChatInput/ActionBar/Model/ControlsForm.tsx +38 -13
  47. package/src/features/ChatInput/ActionBar/Model/ReasoningTokenSlider.tsx +92 -0
  48. package/src/features/ChatInput/ActionBar/Model/index.tsx +13 -18
  49. package/src/libs/agent-runtime/anthropic/index.ts +32 -14
  50. package/src/libs/agent-runtime/types/chat.ts +7 -1
  51. package/src/libs/agent-runtime/utils/streams/anthropic.test.ts +126 -0
  52. package/src/libs/agent-runtime/utils/streams/anthropic.ts +46 -16
  53. package/src/libs/agent-runtime/utils/streams/protocol.ts +4 -0
  54. package/src/locales/default/chat.ts +7 -1
  55. package/src/services/chat.ts +26 -0
  56. package/src/store/agent/slices/chat/__snapshots__/selectors.test.ts.snap +2 -0
  57. package/src/store/aiInfra/slices/aiModel/selectors.ts +6 -6
  58. package/src/store/chat/slices/builtinTool/actions/searXNG.test.ts +288 -0
  59. package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +2 -0
  60. package/src/types/agent/index.ts +23 -9
  61. package/src/types/aiModel.ts +3 -8
  62. package/src/features/ChatInput/ActionBar/Model/ExtendControls.tsx +0 -40
@@ -0,0 +1,288 @@
1
+ import { act, renderHook } from '@testing-library/react';
2
+ import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { searchService } from '@/services/search';
5
+ import { useChatStore } from '@/store/chat';
6
+ import { chatSelectors } from '@/store/chat/selectors';
7
+ import { ChatMessage } from '@/types/message';
8
+ import { SearchContent, SearchQuery, SearchResponse } from '@/types/tool/search';
9
+
10
+ // Mock services
11
+ vi.mock('@/services/search', () => ({
12
+ searchService: {
13
+ search: vi.fn(),
14
+ },
15
+ }));
16
+
17
+ vi.mock('@/store/chat/selectors', () => ({
18
+ chatSelectors: {
19
+ getMessageById: vi.fn(),
20
+ },
21
+ }));
22
+
23
+ describe('searXNG actions', () => {
24
+ beforeEach(() => {
25
+ vi.clearAllMocks();
26
+ useChatStore.setState({
27
+ activeId: 'session-id',
28
+ activeTopicId: 'topic-id',
29
+ searchLoading: {},
30
+ internal_updateMessageContent: vi.fn(),
31
+ internal_updateMessagePluginError: vi.fn(),
32
+ updatePluginArguments: vi.fn(),
33
+ updatePluginState: vi.fn(),
34
+ internal_createMessage: vi.fn(),
35
+ internal_addToolToAssistantMessage: vi.fn(),
36
+ openToolUI: vi.fn(),
37
+ });
38
+ });
39
+
40
+ describe('searchWithSearXNG', () => {
41
+ it('should handle successful search', async () => {
42
+ const mockResponse: SearchResponse = {
43
+ results: [
44
+ {
45
+ title: 'Test Result',
46
+ content: 'Test Content',
47
+ url: 'https://test.com',
48
+ category: 'general',
49
+ engine: 'google',
50
+ engines: ['google'],
51
+ parsed_url: ['test.com'],
52
+ positions: [1],
53
+ score: 1,
54
+ template: 'default',
55
+ },
56
+ ],
57
+ answers: [],
58
+ corrections: [],
59
+ infoboxes: [],
60
+ number_of_results: 1,
61
+ query: 'test',
62
+ suggestions: [],
63
+ unresponsive_engines: [],
64
+ };
65
+
66
+ (searchService.search as Mock).mockResolvedValue(mockResponse);
67
+
68
+ const { result } = renderHook(() => useChatStore());
69
+ const { searchWithSearXNG } = result.current;
70
+
71
+ const messageId = 'test-message-id';
72
+ const query: SearchQuery = {
73
+ query: 'test query',
74
+ searchEngines: ['google'],
75
+ };
76
+
77
+ await act(async () => {
78
+ await searchWithSearXNG(messageId, query);
79
+ });
80
+
81
+ const expectedContent: SearchContent[] = [
82
+ {
83
+ content: 'Test Content',
84
+ title: 'Test Result',
85
+ url: 'https://test.com',
86
+ },
87
+ ];
88
+
89
+ expect(searchService.search).toHaveBeenCalledWith('test query', ['google']);
90
+ expect(result.current.searchLoading[messageId]).toBe(false);
91
+ expect(result.current.internal_updateMessageContent).toHaveBeenCalledWith(
92
+ messageId,
93
+ JSON.stringify(expectedContent),
94
+ );
95
+ });
96
+
97
+ it('should handle empty search results and retry with default engine', async () => {
98
+ const emptyResponse: SearchResponse = {
99
+ results: [],
100
+ answers: [],
101
+ corrections: [],
102
+ infoboxes: [],
103
+ number_of_results: 0,
104
+ query: 'test',
105
+ suggestions: [],
106
+ unresponsive_engines: [],
107
+ };
108
+
109
+ const retryResponse: SearchResponse = {
110
+ results: [
111
+ {
112
+ title: 'Retry Result',
113
+ content: 'Retry Content',
114
+ url: 'https://retry.com',
115
+ category: 'general',
116
+ engine: 'google',
117
+ engines: ['google'],
118
+ parsed_url: ['retry.com'],
119
+ positions: [1],
120
+ score: 1,
121
+ template: 'default',
122
+ },
123
+ ],
124
+ answers: [],
125
+ corrections: [],
126
+ infoboxes: [],
127
+ number_of_results: 1,
128
+ query: 'test',
129
+ suggestions: [],
130
+ unresponsive_engines: [],
131
+ };
132
+
133
+ (searchService.search as Mock)
134
+ .mockResolvedValueOnce(emptyResponse)
135
+ .mockResolvedValueOnce(retryResponse);
136
+
137
+ const { result } = renderHook(() => useChatStore());
138
+ const { searchWithSearXNG } = result.current;
139
+
140
+ const messageId = 'test-message-id';
141
+ const query: SearchQuery = {
142
+ query: 'test query',
143
+ searchEngines: ['custom-engine'],
144
+ };
145
+
146
+ await act(async () => {
147
+ await searchWithSearXNG(messageId, query);
148
+ });
149
+
150
+ expect(searchService.search).toHaveBeenCalledTimes(2);
151
+ expect(searchService.search).toHaveBeenNthCalledWith(1, 'test query', ['custom-engine']);
152
+ expect(searchService.search).toHaveBeenNthCalledWith(2, 'test query');
153
+ expect(result.current.updatePluginArguments).toHaveBeenCalledWith(messageId, {
154
+ query: 'test query',
155
+ searchEngines: undefined,
156
+ });
157
+ });
158
+
159
+ it('should handle search error', async () => {
160
+ const error = new Error('Search failed');
161
+ (searchService.search as Mock).mockRejectedValue(error);
162
+
163
+ const { result } = renderHook(() => useChatStore());
164
+ const { searchWithSearXNG } = result.current;
165
+
166
+ const messageId = 'test-message-id';
167
+ const query: SearchQuery = {
168
+ query: 'test query',
169
+ };
170
+
171
+ await act(async () => {
172
+ await searchWithSearXNG(messageId, query);
173
+ });
174
+
175
+ expect(result.current.internal_updateMessagePluginError).toHaveBeenCalledWith(messageId, {
176
+ body: error,
177
+ message: 'Search failed',
178
+ type: 'PluginServerError',
179
+ });
180
+ expect(result.current.searchLoading[messageId]).toBe(false);
181
+ });
182
+ });
183
+
184
+ describe('reSearchWithSearXNG', () => {
185
+ it('should update arguments and perform search', async () => {
186
+ const { result } = renderHook(() => useChatStore());
187
+ const spy = vi.spyOn(result.current, 'searchWithSearXNG');
188
+ const { reSearchWithSearXNG } = result.current;
189
+
190
+ const messageId = 'test-message-id';
191
+ const query: SearchQuery = {
192
+ query: 'test query',
193
+ };
194
+
195
+ await act(async () => {
196
+ await reSearchWithSearXNG(messageId, query, { aiSummary: true });
197
+ });
198
+
199
+ expect(result.current.updatePluginArguments).toHaveBeenCalledWith(messageId, query);
200
+ expect(spy).toHaveBeenCalledWith(messageId, query, true);
201
+ });
202
+ });
203
+
204
+ describe('saveSearXNGSearchResult', () => {
205
+ it('should save search result as tool message', async () => {
206
+ const messageId = 'test-message-id';
207
+ const parentId = 'parent-message-id';
208
+ const mockMessage: Partial<ChatMessage> = {
209
+ id: messageId,
210
+ parentId,
211
+ content: 'test content',
212
+ plugin: {
213
+ identifier: 'search',
214
+ arguments: '{}',
215
+ apiName: 'search',
216
+ type: 'default',
217
+ },
218
+ pluginState: {},
219
+ role: 'assistant',
220
+ createdAt: Date.now(),
221
+ updatedAt: Date.now(),
222
+ meta: {},
223
+ };
224
+
225
+ vi.spyOn(chatSelectors, 'getMessageById').mockImplementation(
226
+ () => () => mockMessage as ChatMessage,
227
+ );
228
+
229
+ const { result } = renderHook(() => useChatStore());
230
+ const { saveSearXNGSearchResult } = result.current;
231
+
232
+ await act(async () => {
233
+ await saveSearXNGSearchResult(messageId);
234
+ });
235
+
236
+ expect(result.current.internal_createMessage).toHaveBeenCalledWith(
237
+ expect.objectContaining({
238
+ content: 'test content',
239
+ parentId,
240
+ plugin: mockMessage.plugin,
241
+ pluginState: mockMessage.pluginState,
242
+ role: 'tool',
243
+ }),
244
+ );
245
+
246
+ expect(result.current.internal_addToolToAssistantMessage).toHaveBeenCalledWith(
247
+ parentId,
248
+ expect.objectContaining({
249
+ identifier: 'search',
250
+ type: 'default',
251
+ }),
252
+ );
253
+ });
254
+
255
+ it('should not save if message not found', async () => {
256
+ vi.spyOn(chatSelectors, 'getMessageById').mockImplementation(() => () => undefined);
257
+
258
+ const { result } = renderHook(() => useChatStore());
259
+ const { saveSearXNGSearchResult } = result.current;
260
+
261
+ await act(async () => {
262
+ await saveSearXNGSearchResult('non-existent-id');
263
+ });
264
+
265
+ expect(result.current.internal_createMessage).not.toHaveBeenCalled();
266
+ expect(result.current.internal_addToolToAssistantMessage).not.toHaveBeenCalled();
267
+ });
268
+ });
269
+
270
+ describe('toggleSearchLoading', () => {
271
+ it('should toggle search loading state', () => {
272
+ const { result } = renderHook(() => useChatStore());
273
+ const messageId = 'test-message-id';
274
+
275
+ act(() => {
276
+ result.current.toggleSearchLoading(messageId, true);
277
+ });
278
+
279
+ expect(result.current.searchLoading[messageId]).toBe(true);
280
+
281
+ act(() => {
282
+ result.current.toggleSearchLoading(messageId, false);
283
+ });
284
+
285
+ expect(result.current.searchLoading[messageId]).toBe(false);
286
+ });
287
+ });
288
+ });
@@ -75,7 +75,9 @@ exports[`settingsSelectors > defaultAgent > should merge DEFAULT_AGENT and s.set
75
75
  "enableAutoCreateTopic": true,
76
76
  "enableCompressHistory": true,
77
77
  "enableHistoryCount": true,
78
+ "enableReasoning": true,
78
79
  "historyCount": 8,
80
+ "reasoningBudgetToken": 1024,
79
81
  "searchMode": "off",
80
82
  },
81
83
  "model": "gpt-3.5-turbo",
@@ -55,33 +55,45 @@ export interface LobeAgentConfig {
55
55
  tts: LobeAgentTTSConfig;
56
56
  }
57
57
 
58
+ /* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
59
+
58
60
  export interface LobeAgentChatConfig {
59
- autoCreateTopicThreshold: number;
60
61
  displayMode?: 'chat' | 'docs';
62
+
61
63
  enableAutoCreateTopic?: boolean;
62
- /**
63
- * 历史消息长度压缩阈值
64
- */
65
- enableCompressHistory?: boolean;
66
- /**
67
- * 开启历史记录条数
68
- */
69
- enableHistoryCount?: boolean;
64
+ autoCreateTopicThreshold: number;
65
+
70
66
  enableMaxTokens?: boolean;
71
67
 
68
+ /**
69
+ * 是否开启推理
70
+ */
71
+ enableReasoning?: boolean;
72
72
  /**
73
73
  * 自定义推理强度
74
74
  */
75
75
  enableReasoningEffort?: boolean;
76
+ reasoningBudgetToken?: number;
76
77
 
77
78
  /**
78
79
  * 历史消息条数
79
80
  */
80
81
  historyCount?: number;
82
+ /**
83
+ * 开启历史记录条数
84
+ */
85
+ enableHistoryCount?: boolean;
86
+ /**
87
+ * 历史消息长度压缩阈值
88
+ */
89
+ enableCompressHistory?: boolean;
90
+
81
91
  inputTemplate?: string;
92
+
82
93
  searchMode?: SearchMode;
83
94
  useModelBuiltinSearch?: boolean;
84
95
  }
96
+ /* eslint-enable */
85
97
 
86
98
  export const AgentChatConfigSchema = z.object({
87
99
  autoCreateTopicThreshold: z.number().default(2),
@@ -90,8 +102,10 @@ export const AgentChatConfigSchema = z.object({
90
102
  enableCompressHistory: z.boolean().optional(),
91
103
  enableHistoryCount: z.boolean().optional(),
92
104
  enableMaxTokens: z.boolean().optional(),
105
+ enableReasoning: z.boolean().optional(),
93
106
  enableReasoningEffort: z.boolean().optional(),
94
107
  historyCount: z.number().optional(),
108
+ reasoningBudgetToken: z.number().optional(),
95
109
  searchMode: z.enum(['off', 'on', 'auto']).optional(),
96
110
  });
97
111
 
@@ -138,17 +138,12 @@ export interface AiModelConfig {
138
138
  enabledSearch?: boolean;
139
139
  }
140
140
 
141
- export interface ExtendedControl {
142
- key: string;
143
- requestParams: string | string[];
144
- type: 'params' | 'tool';
145
- valueType: 'boolean';
146
- }
147
-
148
141
  export type ModelSearchImplementType = 'tool' | 'params' | 'internal';
149
142
 
143
+ export type ExtendParamsType = 'reasoningBudgetToken' | 'enableReasoning';
144
+
150
145
  export interface AiModelSettings {
151
- extendControls?: ExtendedControl[];
146
+ extendParams?: ExtendParamsType[];
152
147
  /**
153
148
  * 模型层实现搜索的方式
154
149
  */
@@ -1,40 +0,0 @@
1
- import { ActionIcon } from '@lobehub/ui';
2
- import { Popover } from 'antd';
3
- import { Settings2Icon } from 'lucide-react';
4
- import { memo } from 'react';
5
- import { useTranslation } from 'react-i18next';
6
- import { Flexbox } from 'react-layout-kit';
7
-
8
- import { useIsMobile } from '@/hooks/useIsMobile';
9
-
10
- import ControlsForm from './ControlsForm';
11
-
12
- const ExtendControls = memo(() => {
13
- const { t } = useTranslation('chat');
14
-
15
- const isMobile = useIsMobile();
16
- return (
17
- <Flexbox style={{ marginInlineStart: -4 }}>
18
- <Popover
19
- arrow={false}
20
- content={<ControlsForm />}
21
- open
22
- styles={{
23
- body: {
24
- minWidth: isMobile ? undefined : 250,
25
- width: isMobile ? '100vw' : undefined,
26
- },
27
- }}
28
- >
29
- <ActionIcon
30
- icon={Settings2Icon}
31
- placement={'bottom'}
32
- style={{ borderRadius: 20 }}
33
- title={t('extendControls.title')}
34
- />
35
- </Popover>
36
- </Flexbox>
37
- );
38
- });
39
-
40
- export default ExtendControls;