@lobehub/chat 1.91.2 → 1.92.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 (41) hide show
  1. package/.eslintrc.js +2 -0
  2. package/CHANGELOG.md +74 -0
  3. package/changelog/v1.json +27 -0
  4. package/locales/ar/setting.json +1 -1
  5. package/locales/bg-BG/setting.json +1 -1
  6. package/locales/de-DE/setting.json +1 -1
  7. package/locales/en-US/setting.json +1 -1
  8. package/locales/es-ES/setting.json +1 -1
  9. package/locales/fa-IR/setting.json +1 -1
  10. package/locales/fr-FR/setting.json +1 -1
  11. package/locales/it-IT/setting.json +1 -1
  12. package/locales/ja-JP/setting.json +1 -1
  13. package/locales/ko-KR/setting.json +1 -1
  14. package/locales/nl-NL/setting.json +1 -1
  15. package/locales/pl-PL/setting.json +1 -1
  16. package/locales/pt-BR/setting.json +1 -1
  17. package/locales/ru-RU/setting.json +1 -1
  18. package/locales/tr-TR/setting.json +1 -1
  19. package/locales/vi-VN/setting.json +1 -1
  20. package/locales/zh-CN/setting.json +1 -1
  21. package/locales/zh-TW/setting.json +1 -1
  22. package/package.json +1 -1
  23. package/src/app/[variants]/(main)/profile/features/ClerkProfile.tsx +1 -4
  24. package/src/config/aiModels/modelscope.ts +4 -1
  25. package/src/config/aiModels/novita.ts +2 -0
  26. package/src/config/aiModels/openrouter.ts +2 -0
  27. package/src/config/aiModels/siliconcloud.ts +1 -0
  28. package/src/config/modelProviders/anthropic.ts +30 -11
  29. package/src/config/modelProviders/openai.ts +14 -0
  30. package/src/features/AgentSetting/AgentModal/index.tsx +3 -2
  31. package/src/features/ChatInput/ActionBar/Search/Controls.tsx +6 -2
  32. package/src/layout/AuthProvider/Clerk/useAppearance.ts +1 -4
  33. package/src/libs/model-runtime/utils/streams/vertex-ai.ts +12 -0
  34. package/src/locales/default/setting.ts +1 -1
  35. package/src/services/chat.ts +17 -9
  36. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +23 -31
  37. package/src/store/user/slices/auth/selectors.test.ts +18 -0
  38. package/src/store/user/slices/auth/selectors.ts +1 -0
  39. package/src/utils/client/parserPlaceholder.test.ts +326 -0
  40. package/src/utils/client/parserPlaceholder.ts +190 -0
  41. package/src/app/[variants]/(main)/settings/provider/(detail)/ollama/OllamaModelDownloader/index.tsx +0 -0
@@ -41,6 +41,7 @@ import { createErrorResponse } from '@/utils/errorResponse';
41
41
  import { FetchSSEOptions, fetchSSE, getMessageError } from '@/utils/fetch';
42
42
  import { genToolCallingName } from '@/utils/toolCall';
43
43
  import { createTraceHeader, getTraceId } from '@/utils/trace';
44
+ import { parsePlaceholderVariablesMessages } from '@/utils/client/parserPlaceholder';
44
45
 
45
46
  import { createHeaderWithAuth, createPayloadWithKeyVaults } from './_auth';
46
47
  import { API_ENDPOINTS } from './_url';
@@ -172,14 +173,18 @@ class ChatService {
172
173
 
173
174
  // =================== 0. process search =================== //
174
175
  const chatConfig = agentChatConfigSelectors.currentChatConfig(getAgentStoreState());
175
-
176
+ const aiInfraStoreState = getAiInfraStoreState();
176
177
  const enabledSearch = chatConfig.searchMode !== 'off';
178
+ const isProviderHasBuiltinSearch = aiProviderSelectors.isProviderHasBuiltinSearch(
179
+ payload.provider!,
180
+ )(aiInfraStoreState);
177
181
  const isModelHasBuiltinSearch = aiModelSelectors.isModelHasBuiltinSearch(
178
182
  payload.model,
179
183
  payload.provider!,
180
- )(getAiInfraStoreState());
184
+ )(aiInfraStoreState);
181
185
 
182
- const useModelSearch = isModelHasBuiltinSearch && chatConfig.useModelBuiltinSearch;
186
+ const useModelSearch =
187
+ (isProviderHasBuiltinSearch || isModelHasBuiltinSearch) && chatConfig.useModelBuiltinSearch;
183
188
 
184
189
  const useApplicationBuiltinSearchTool = enabledSearch && !useModelSearch;
185
190
 
@@ -189,11 +194,14 @@ class ChatService {
189
194
  pluginIds.push(WebBrowsingManifest.identifier);
190
195
  }
191
196
 
192
- // ============ 1. preprocess messages ============ //
197
+ // ============ 1. preprocess placeholder variables ============ //
198
+ const parsedMessages = parsePlaceholderVariablesMessages(messages);
199
+
200
+ // ============ 2. preprocess messages ============ //
193
201
 
194
202
  const oaiMessages = this.processMessages(
195
203
  {
196
- messages,
204
+ messages: parsedMessages,
197
205
  model: payload.model,
198
206
  provider: payload.provider!,
199
207
  tools: pluginIds,
@@ -201,28 +209,28 @@ class ChatService {
201
209
  options,
202
210
  );
203
211
 
204
- // ============ 2. preprocess tools ============ //
212
+ // ============ 3. preprocess tools ============ //
205
213
 
206
214
  const tools = this.prepareTools(pluginIds, {
207
215
  model: payload.model,
208
216
  provider: payload.provider!,
209
217
  });
210
218
 
211
- // ============ 3. process extend params ============ //
219
+ // ============ 4. process extend params ============ //
212
220
 
213
221
  let extendParams: Record<string, any> = {};
214
222
 
215
223
  const isModelHasExtendParams = aiModelSelectors.isModelHasExtendParams(
216
224
  payload.model,
217
225
  payload.provider!,
218
- )(getAiInfraStoreState());
226
+ )(aiInfraStoreState);
219
227
 
220
228
  // model
221
229
  if (isModelHasExtendParams) {
222
230
  const modelExtendParams = aiModelSelectors.modelExtendParams(
223
231
  payload.model,
224
232
  payload.provider!,
225
- )(getAiInfraStoreState());
233
+ )(aiInfraStoreState);
226
234
  // if model has extended params, then we need to check if the model can use reasoning
227
235
 
228
236
  if (modelExtendParams!.includes('enableReasoning')) {
@@ -1,7 +1,6 @@
1
1
  /* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
2
2
  // Disable the auto sort key eslint rule to make the code more logic and readable
3
3
  import { produce } from 'immer';
4
- import { template } from 'lodash-es';
5
4
  import { StateCreator } from 'zustand/vanilla';
6
5
 
7
6
  import { LOADING_FLAT, MESSAGE_CANCEL_FLAT } from '@/const/message';
@@ -13,7 +12,7 @@ import { messageService } from '@/services/message';
13
12
  import { useAgentStore } from '@/store/agent';
14
13
  import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
15
14
  import { getAgentStoreState } from '@/store/agent/store';
16
- import { aiModelSelectors } from '@/store/aiInfra';
15
+ import { aiModelSelectors, aiProviderSelectors } from '@/store/aiInfra';
17
16
  import { getAiInfraStoreState } from '@/store/aiInfra/store';
18
17
  import { chatHelpers } from '@/store/chat/helpers';
19
18
  import { ChatStore } from '@/store/chat/store';
@@ -299,7 +298,8 @@ export const generateAIChat: StateCreator<
299
298
  // create a new array to avoid the original messages array change
300
299
  const messages = [...originalMessages];
301
300
 
302
- const { model, provider, chatConfig } = agentSelectors.currentAgentConfig(getAgentStoreState());
301
+ const agentStoreState = getAgentStoreState();
302
+ const { model, provider, chatConfig } = agentSelectors.currentAgentConfig(agentStoreState);
303
303
 
304
304
  let fileChunks: MessageSemanticSearchChunk[] | undefined;
305
305
  let ragQueryId;
@@ -323,7 +323,7 @@ export const generateAIChat: StateCreator<
323
323
  chunks,
324
324
  userQuery: lastMsg.content,
325
325
  rewriteQuery,
326
- knowledge: agentSelectors.currentEnabledKnowledge(getAgentStoreState()),
326
+ knowledge: agentSelectors.currentEnabledKnowledge(agentStoreState),
327
327
  });
328
328
 
329
329
  // 3. add the retrieve context messages to the messages history
@@ -355,14 +355,25 @@ export const generateAIChat: StateCreator<
355
355
  if (!assistantId) return;
356
356
 
357
357
  // 3. place a search with the search working model if this model is not support tool use
358
+ const aiInfraStoreState = getAiInfraStoreState();
358
359
  const isModelSupportToolUse = aiModelSelectors.isModelSupportToolUse(
359
360
  model,
360
361
  provider!,
361
- )(getAiInfraStoreState());
362
- const isAgentEnableSearch = agentChatConfigSelectors.isAgentEnableSearch(getAgentStoreState());
362
+ )(aiInfraStoreState);
363
+ const isProviderHasBuiltinSearch = aiProviderSelectors.isProviderHasBuiltinSearch(provider!)(
364
+ aiInfraStoreState,
365
+ );
366
+ const isModelHasBuiltinSearch = aiModelSelectors.isModelHasBuiltinSearch(
367
+ model,
368
+ provider!,
369
+ )(aiInfraStoreState);
370
+ const useModelBuiltinSearch = agentChatConfigSelectors.useModelBuiltinSearch(agentStoreState);
371
+ const useModelSearch =
372
+ (isProviderHasBuiltinSearch || isModelHasBuiltinSearch) && useModelBuiltinSearch;
373
+ const isAgentEnableSearch = agentChatConfigSelectors.isAgentEnableSearch(agentStoreState);
363
374
 
364
- if (isAgentEnableSearch && !isModelSupportToolUse) {
365
- const { model, provider } = agentChatConfigSelectors.searchFCModel(getAgentStoreState());
375
+ if (isAgentEnableSearch && !useModelSearch && !isModelSupportToolUse) {
376
+ const { model, provider } = agentChatConfigSelectors.searchFCModel(agentStoreState);
366
377
 
367
378
  let isToolsCalling = false;
368
379
  let isError = false;
@@ -460,10 +471,10 @@ export const generateAIChat: StateCreator<
460
471
  }
461
472
 
462
473
  // 6. summary history if context messages is larger than historyCount
463
- const historyCount = agentChatConfigSelectors.historyCount(getAgentStoreState());
474
+ const historyCount = agentChatConfigSelectors.historyCount(agentStoreState);
464
475
 
465
476
  if (
466
- agentChatConfigSelectors.enableHistoryCount(getAgentStoreState()) &&
477
+ agentChatConfigSelectors.enableHistoryCount(agentStoreState) &&
467
478
  chatConfig.enableCompressHistory &&
468
479
  originalMessages.length > historyCount
469
480
  ) {
@@ -495,8 +506,6 @@ export const generateAIChat: StateCreator<
495
506
  const agentConfig = agentSelectors.currentAgentConfig(getAgentStoreState());
496
507
  const chatConfig = agentChatConfigSelectors.currentChatConfig(getAgentStoreState());
497
508
 
498
- const compiler = template(chatConfig.inputTemplate, { interpolate: /{{([\S\s]+?)}}/g });
499
-
500
509
  // ================================== //
501
510
  // messages uniformly preprocess //
502
511
  // ================================== //
@@ -511,29 +520,12 @@ export const generateAIChat: StateCreator<
511
520
  historyCount,
512
521
  });
513
522
 
514
- // 2. replace inputMessage template
515
- preprocessMsgs = !chatConfig.inputTemplate
516
- ? preprocessMsgs
517
- : preprocessMsgs.map((m) => {
518
- if (m.role === 'user') {
519
- try {
520
- return { ...m, content: compiler({ text: m.content }) };
521
- } catch (error) {
522
- console.error(error);
523
-
524
- return m;
525
- }
526
- }
527
-
528
- return m;
529
- });
530
-
531
- // 3. add systemRole
523
+ // 2. add systemRole
532
524
  if (agentConfig.systemRole) {
533
525
  preprocessMsgs.unshift({ content: agentConfig.systemRole, role: 'system' } as ChatMessage);
534
526
  }
535
527
 
536
- // 4. handle max_tokens
528
+ // 3. handle max_tokens
537
529
  agentConfig.params.max_tokens = chatConfig.enableMaxTokens
538
530
  ? agentConfig.params.max_tokens
539
531
  : undefined;
@@ -33,6 +33,24 @@ afterEach(() => {
33
33
  });
34
34
 
35
35
  describe('userProfileSelectors', () => {
36
+ describe('fullName', () => {
37
+ it('should return user fullName if exist', () => {
38
+ const store: UserStore = {
39
+ user: { fullName: 'John Doe' },
40
+ } as UserStore;
41
+
42
+ expect(userProfileSelectors.fullName(store)).toBe('John Doe');
43
+ });
44
+
45
+ it('should return empty string if not exist', () => {
46
+ const store: UserStore = {
47
+ user: { fullName: undefined },
48
+ } as UserStore;
49
+
50
+ expect(userProfileSelectors.fullName(store)).toBe('');
51
+ });
52
+ });
53
+
36
54
  describe('nickName', () => {
37
55
  it('should return default nickname when auth is disabled and not desktop', () => {
38
56
  enableAuth = false;
@@ -36,6 +36,7 @@ const username = (s: UserStore) => {
36
36
  };
37
37
 
38
38
  export const userProfileSelectors = {
39
+ fullName: (s: UserStore): string => s.user?.fullName || '',
39
40
  nickName,
40
41
  userAvatar: (s: UserStore): string => s.user?.avatar || '',
41
42
  userId: (s: UserStore) => s.user?.id,
@@ -0,0 +1,326 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { parsePlaceholderVariablesMessages, VARIABLE_GENERATORS } from './parserPlaceholder';
3
+
4
+ // Mock dependencies
5
+ vi.mock('@/utils/uuid', () => ({
6
+ uuid: () => 'mocked-uuid-12345'
7
+ }));
8
+
9
+ vi.mock('@/store/user', () => ({
10
+ useUserStore: {
11
+ getState: () => ({})
12
+ }
13
+ }));
14
+
15
+ vi.mock('@/store/user/selectors', () => ({
16
+ userProfileSelectors: {
17
+ nickName: () => 'Test User',
18
+ username: () => 'testuser',
19
+ fullName: () => 'Test Full Name'
20
+ }
21
+ }));
22
+
23
+ vi.mock('@/store/agent/store', () => ({
24
+ getAgentStoreState: () => ({})
25
+ }));
26
+
27
+ vi.mock('@/store/agent/selectors', () => ({
28
+ agentChatConfigSelectors: {
29
+ currentChatConfig: () => ({
30
+ inputTemplate: 'Hello {{username}}!'
31
+ })
32
+ }
33
+ }));
34
+
35
+ describe('parsePlaceholderVariablesMessages', () => {
36
+ beforeEach(() => {
37
+ // Mock Date for consistent testing
38
+ vi.useFakeTimers();
39
+ vi.setSystemTime(new Date('2025-06-06T06:06:06.666Z'));
40
+
41
+ // Mock Math.random for consistent random values
42
+ vi.spyOn(Math, 'random').mockReturnValue(0.5);
43
+ });
44
+
45
+ afterEach(() => {
46
+ vi.useRealTimers();
47
+ vi.restoreAllMocks();
48
+ });
49
+
50
+ describe('string content messages', () => {
51
+ it('should replace template variables in string content', () => {
52
+ const messages = [
53
+ {
54
+ id: '1',
55
+ content: 'Hello {{username}}, today is {{date}}'
56
+ }
57
+ ];
58
+
59
+ const result = parsePlaceholderVariablesMessages(messages);
60
+
61
+ expect(result[0].content).toContain('testuser');
62
+ expect(result[0].content).toContain(new Date().toLocaleDateString());
63
+ });
64
+
65
+ it('should handle multiple variables in one message', () => {
66
+ const messages = [
67
+ {
68
+ id: '1',
69
+ content: 'Time: {{time}}, Date: {{date}}, User: {{nickname}}'
70
+ }
71
+ ];
72
+
73
+ const result = parsePlaceholderVariablesMessages(messages);
74
+
75
+ expect(result[0].content).toContain('Test User');
76
+ expect(result[0].content).toMatch(/Time: .+, Date: .+, User: Test User/);
77
+ });
78
+
79
+ it('should preserve message structure when replacing variables', () => {
80
+ const messages = [
81
+ {
82
+ id: '1',
83
+ role: 'user',
84
+ content: 'Hello {{username}}'
85
+ }
86
+ ];
87
+
88
+ const result = parsePlaceholderVariablesMessages(messages);
89
+
90
+ expect(result[0]).toEqual({
91
+ id: '1',
92
+ role: 'user',
93
+ content: 'Hello testuser'
94
+ });
95
+ });
96
+ });
97
+
98
+ describe('array content messages', () => {
99
+ it('should replace variables in text type array elements', () => {
100
+ const messages = [
101
+ {
102
+ id: '1',
103
+ content: [
104
+ {
105
+ type: 'text',
106
+ text: 'Hello {{username}}'
107
+ },
108
+ {
109
+ type: 'image_url',
110
+ image_url: 'image.jpg'
111
+ }
112
+ ]
113
+ }
114
+ ];
115
+
116
+ const result = parsePlaceholderVariablesMessages(messages);
117
+
118
+ expect(result[0].content[0].text).toBe('Hello testuser');
119
+ expect(result[0].content[1]).toEqual({
120
+ type: 'image_url',
121
+ image_url: 'image.jpg'
122
+ });
123
+ });
124
+
125
+ it('should handle multiple text elements with variables', () => {
126
+ const messages = [
127
+ {
128
+ id: '1',
129
+ content: [
130
+ {
131
+ type: 'text',
132
+ text: 'Date: {{date}}'
133
+ },
134
+ {
135
+ type: 'text',
136
+ text: 'Time: {{time}}'
137
+ },
138
+ {
139
+ type: 'image_url',
140
+ image_url: 'test.jpg'
141
+ }
142
+ ]
143
+ }
144
+ ];
145
+
146
+ const result = parsePlaceholderVariablesMessages(messages);
147
+
148
+ expect(result[0].content[0].text).toContain(new Date().toLocaleDateString());
149
+ expect(result[0].content[1].text).toContain(new Date().toLocaleTimeString());
150
+ expect(result[0].content[2]).toEqual({
151
+ type: 'image_url',
152
+ image_url: 'test.jpg'
153
+ });
154
+ });
155
+
156
+ it('should preserve non-text array elements unchanged', () => {
157
+ const messages = [
158
+ {
159
+ id: '1',
160
+ content: [
161
+ {
162
+ type: 'image_url',
163
+ image_url: 'image.jpg',
164
+ },
165
+ {
166
+ type: 'image_url',
167
+ name: 'image2.jpg'
168
+ }
169
+ ]
170
+ }
171
+ ];
172
+
173
+ const result = parsePlaceholderVariablesMessages(messages);
174
+
175
+ expect(result[0].content).toEqual([
176
+ {
177
+ type: 'image_url',
178
+ image_url: 'image.jpg'
179
+ },
180
+ {
181
+ type: 'image_url',
182
+ name: 'image2.jpg'
183
+ }
184
+ ]);
185
+ });
186
+ });
187
+
188
+ describe('edge cases', () => {
189
+ it('should handle empty messages array', () => {
190
+ const result = parsePlaceholderVariablesMessages([]);
191
+ expect(result).toEqual([]);
192
+ });
193
+
194
+ it('should handle messages without content', () => {
195
+ const messages = [
196
+ { id: '1' },
197
+ { id: '2', content: null },
198
+ { id: '3', content: undefined }
199
+ ];
200
+
201
+ const result = parsePlaceholderVariablesMessages(messages);
202
+
203
+ expect(result).toEqual([
204
+ { id: '1' },
205
+ { id: '2', content: null },
206
+ { id: '3', content: undefined }
207
+ ]);
208
+ });
209
+
210
+ it('should handle empty string content', () => {
211
+ const messages = [
212
+ { id: '1', content: '' }
213
+ ];
214
+
215
+ const result = parsePlaceholderVariablesMessages(messages);
216
+
217
+ expect(result[0].content).toBe('');
218
+ });
219
+
220
+ it('should handle content without variables', () => {
221
+ const messages = [
222
+ { id: '1', content: 'Hello world!' },
223
+ {
224
+ id: '2',
225
+ content: [
226
+ { type: 'text', text: 'No variables here' },
227
+ { type: 'image_url', image_url: 'test.jpg' }
228
+ ]
229
+ }
230
+ ];
231
+
232
+ const result = parsePlaceholderVariablesMessages(messages);
233
+
234
+ expect(result[0].content).toBe('Hello world!');
235
+ expect(result[1].content[0].text).toBe('No variables here');
236
+ });
237
+
238
+ it('should handle unknown variable types', () => {
239
+ const messages = [
240
+ { id: '1', content: 'Hello {{unknown_variable}}!' }
241
+ ];
242
+
243
+ const result = parsePlaceholderVariablesMessages(messages);
244
+
245
+ // Unknown variables should remain unchanged
246
+ expect(result[0].content).toBe('Hello {{unknown_variable}}!');
247
+ });
248
+
249
+ it('should handle nested variables (input_template)', () => {
250
+ const messages = [
251
+ { id: '1', content: 'Template: {{input_template}}' }
252
+ ];
253
+
254
+ const result = parsePlaceholderVariablesMessages(messages);
255
+
256
+ // Should resolve nested variables in input_template
257
+ expect(result[0].content).toBe('Template: Hello testuser!');
258
+ });
259
+ });
260
+
261
+ describe('specific variable types', () => {
262
+ it('should handle time variables', () => {
263
+ const messages = [
264
+ {
265
+ id: '1',
266
+ content: 'Year: {{year}}, Month: {{month}}, Day: {{day}}'
267
+ }
268
+ ];
269
+
270
+ const result = parsePlaceholderVariablesMessages(messages);
271
+
272
+ expect(result[0].content).toContain('Year: 2025');
273
+ expect(result[0].content).toContain('Month: 06');
274
+ expect(result[0].content).toContain('Day: 06');
275
+ });
276
+
277
+ it('should handle random variables', () => {
278
+ const messages = [
279
+ {
280
+ id: '1',
281
+ content: 'Random: {{random}}, Bool: {{random_bool}}, UUID: {{uuid}}'
282
+ }
283
+ ];
284
+
285
+ const result = parsePlaceholderVariablesMessages(messages);
286
+
287
+ expect(result[0].content).toContain('Random: 500001'); // Math.random() * 1000000 + 1 with 0.5
288
+ expect(result[0].content).toContain('Bool: false'); // Math.random() > 0.5 with 0.5
289
+ expect(result[0].content).toContain('UUID: mocked-uuid-12345');
290
+ });
291
+
292
+ it('should handle user variables', () => {
293
+ const messages = [
294
+ {
295
+ id: '1',
296
+ content: 'User: {{username}}, Nickname: {{nickname}}'
297
+ }
298
+ ];
299
+
300
+ const result = parsePlaceholderVariablesMessages(messages);
301
+
302
+ expect(result[0].content).toBe('User: testuser, Nickname: Test User');
303
+ });
304
+ });
305
+
306
+ describe('multiple messages', () => {
307
+ it('should process multiple messages correctly', () => {
308
+ const messages = [
309
+ { id: '1', content: 'Hello {{username}}' },
310
+ {
311
+ id: '2',
312
+ content: [
313
+ { type: 'text', text: 'Today is {{date}}' }
314
+ ]
315
+ },
316
+ { id: '3', content: 'Time: {{time}}' }
317
+ ];
318
+
319
+ const result = parsePlaceholderVariablesMessages(messages);
320
+
321
+ expect(result[0].content).toBe('Hello testuser');
322
+ expect(result[1].content[0].text).toContain(new Date().toLocaleDateString());
323
+ expect(result[2].content).toContain(new Date().toLocaleTimeString());
324
+ });
325
+ });
326
+ });