@lobehub/chat 1.64.3 → 1.65.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +1 -1
  3. package/changelog/v1.json +21 -0
  4. package/locales/ar/chat.json +7 -1
  5. package/locales/ar/models.json +6 -9
  6. package/locales/bg-BG/chat.json +7 -1
  7. package/locales/bg-BG/models.json +6 -9
  8. package/locales/de-DE/chat.json +7 -1
  9. package/locales/de-DE/models.json +6 -9
  10. package/locales/en-US/chat.json +7 -1
  11. package/locales/en-US/models.json +6 -9
  12. package/locales/es-ES/chat.json +8 -2
  13. package/locales/es-ES/models.json +6 -9
  14. package/locales/fa-IR/chat.json +7 -1
  15. package/locales/fa-IR/models.json +6 -3
  16. package/locales/fr-FR/chat.json +7 -1
  17. package/locales/fr-FR/models.json +6 -9
  18. package/locales/it-IT/chat.json +7 -1
  19. package/locales/it-IT/models.json +6 -9
  20. package/locales/ja-JP/chat.json +7 -1
  21. package/locales/ja-JP/models.json +6 -9
  22. package/locales/ko-KR/chat.json +7 -1
  23. package/locales/ko-KR/models.json +6 -9
  24. package/locales/nl-NL/chat.json +8 -2
  25. package/locales/nl-NL/models.json +6 -9
  26. package/locales/pl-PL/chat.json +7 -1
  27. package/locales/pl-PL/models.json +6 -9
  28. package/locales/pt-BR/chat.json +7 -1
  29. package/locales/pt-BR/models.json +6 -9
  30. package/locales/ru-RU/chat.json +8 -2
  31. package/locales/ru-RU/models.json +6 -9
  32. package/locales/tr-TR/chat.json +7 -1
  33. package/locales/tr-TR/models.json +6 -9
  34. package/locales/vi-VN/chat.json +7 -1
  35. package/locales/vi-VN/models.json +6 -9
  36. package/locales/zh-CN/chat.json +7 -1
  37. package/locales/zh-CN/models.json +6 -9
  38. package/locales/zh-TW/chat.json +7 -1
  39. package/locales/zh-TW/models.json +6 -9
  40. package/package.json +2 -2
  41. package/src/app/(backend)/middleware/auth/index.ts +6 -0
  42. package/src/config/aiModels/anthropic.ts +5 -2
  43. package/src/config/aiModels/google.ts +7 -0
  44. package/src/const/message.ts +3 -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/google/index.test.ts +8 -0
  51. package/src/libs/agent-runtime/google/index.ts +18 -5
  52. package/src/libs/agent-runtime/types/chat.ts +16 -2
  53. package/src/libs/agent-runtime/utils/anthropicHelpers.test.ts +113 -0
  54. package/src/libs/agent-runtime/utils/anthropicHelpers.ts +7 -4
  55. package/src/libs/agent-runtime/utils/streams/anthropic.test.ts +371 -0
  56. package/src/libs/agent-runtime/utils/streams/anthropic.ts +80 -30
  57. package/src/libs/agent-runtime/utils/streams/openai.test.ts +181 -0
  58. package/src/libs/agent-runtime/utils/streams/openai.ts +40 -30
  59. package/src/libs/agent-runtime/utils/streams/protocol.ts +8 -0
  60. package/src/locales/default/chat.ts +7 -1
  61. package/src/services/__tests__/chat.test.ts +89 -50
  62. package/src/services/chat.ts +39 -1
  63. package/src/store/agent/slices/chat/__snapshots__/selectors.test.ts.snap +2 -0
  64. package/src/store/aiInfra/slices/aiModel/selectors.ts +6 -6
  65. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +1 -1
  66. package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +2 -0
  67. package/src/types/agent/index.ts +23 -9
  68. package/src/types/aiModel.ts +3 -8
  69. package/src/types/message/base.ts +1 -0
  70. package/src/utils/fetch/__tests__/fetchSSE.test.ts +113 -10
  71. package/src/utils/fetch/fetchSSE.ts +12 -3
  72. package/src/features/ChatInput/ActionBar/Model/ExtendControls.tsx +0 -40
@@ -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
  */
@@ -10,6 +10,7 @@ export interface CitationItem {
10
10
  export interface ModelReasoning {
11
11
  content?: string;
12
12
  duration?: number;
13
+ signature?: string;
13
14
  }
14
15
 
15
16
  export type MessageRoleType = 'user' | 'system' | 'assistant' | 'tool';
@@ -154,16 +154,119 @@ describe('fetchSSE', () => {
154
154
  });
155
155
  });
156
156
 
157
- it('should handle reasoning event with smoothing correctly', async () => {
157
+ describe('reasoning', () => {
158
+ it('should handle reasoning event without smoothing', async () => {
159
+ const mockOnMessageHandle = vi.fn();
160
+ const mockOnFinish = vi.fn();
161
+
162
+ (fetchEventSource as any).mockImplementationOnce(
163
+ async (url: string, options: FetchEventSourceInit) => {
164
+ options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
165
+ options.onmessage!({ event: 'reasoning', data: JSON.stringify('Hello') } as any);
166
+ await sleep(100);
167
+ options.onmessage!({ event: 'reasoning', data: JSON.stringify(' World') } as any);
168
+ await sleep(100);
169
+ options.onmessage!({ event: 'text', data: JSON.stringify('hi') } as any);
170
+ },
171
+ );
172
+
173
+ await fetchSSE('/', {
174
+ onMessageHandle: mockOnMessageHandle,
175
+ onFinish: mockOnFinish,
176
+ });
177
+
178
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hello', type: 'reasoning' });
179
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: ' World', type: 'reasoning' });
180
+
181
+ expect(mockOnFinish).toHaveBeenCalledWith('hi', {
182
+ observationId: null,
183
+ toolCalls: undefined,
184
+ reasoning: { content: 'Hello World' },
185
+ traceId: null,
186
+ type: 'done',
187
+ });
188
+ });
189
+
190
+ it('should handle reasoning event with smoothing correctly', async () => {
191
+ const mockOnMessageHandle = vi.fn();
192
+ const mockOnFinish = vi.fn();
193
+
194
+ (fetchEventSource as any).mockImplementationOnce(
195
+ async (url: string, options: FetchEventSourceInit) => {
196
+ options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
197
+ options.onmessage!({ event: 'reasoning', data: JSON.stringify('Hello') } as any);
198
+ await sleep(100);
199
+ options.onmessage!({ event: 'reasoning', data: JSON.stringify(' World') } as any);
200
+ await sleep(100);
201
+ options.onmessage!({ event: 'text', data: JSON.stringify('hi') } as any);
202
+ },
203
+ );
204
+
205
+ await fetchSSE('/', {
206
+ onMessageHandle: mockOnMessageHandle,
207
+ onFinish: mockOnFinish,
208
+ smoothing: true,
209
+ });
210
+
211
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hell', type: 'reasoning' });
212
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: 'o', type: 'reasoning' });
213
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(3, { text: ' Wor', type: 'reasoning' });
214
+ // more assertions for each character...
215
+ expect(mockOnFinish).toHaveBeenCalledWith('hi', {
216
+ observationId: null,
217
+ toolCalls: undefined,
218
+ reasoning: { content: 'Hello World' },
219
+ traceId: null,
220
+ type: 'done',
221
+ });
222
+ });
223
+ it('should handle reasoning with signature', async () => {
224
+ const mockOnMessageHandle = vi.fn();
225
+ const mockOnFinish = vi.fn();
226
+
227
+ (fetchEventSource as any).mockImplementationOnce(
228
+ async (url: string, options: FetchEventSourceInit) => {
229
+ options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
230
+ options.onmessage!({ event: 'reasoning', data: JSON.stringify('Hello') } as any);
231
+ await sleep(100);
232
+ options.onmessage!({ event: 'reasoning', data: JSON.stringify(' World') } as any);
233
+ options.onmessage!({
234
+ event: 'reasoning_signature',
235
+ data: JSON.stringify('abcbcd'),
236
+ } as any);
237
+ await sleep(100);
238
+ options.onmessage!({ event: 'text', data: JSON.stringify('hi') } as any);
239
+ },
240
+ );
241
+
242
+ await fetchSSE('/', {
243
+ onMessageHandle: mockOnMessageHandle,
244
+ onFinish: mockOnFinish,
245
+ smoothing: true,
246
+ });
247
+
248
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hell', type: 'reasoning' });
249
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: 'o', type: 'reasoning' });
250
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(3, { text: ' Wor', type: 'reasoning' });
251
+ // more assertions for each character...
252
+ expect(mockOnFinish).toHaveBeenCalledWith('hi', {
253
+ observationId: null,
254
+ toolCalls: undefined,
255
+ reasoning: { content: 'Hello World', signature: 'abcbcd' },
256
+ traceId: null,
257
+ type: 'done',
258
+ });
259
+ });
260
+ });
261
+
262
+ it('should handle grounding event', async () => {
158
263
  const mockOnMessageHandle = vi.fn();
159
264
  const mockOnFinish = vi.fn();
160
265
 
161
266
  (fetchEventSource as any).mockImplementationOnce(
162
267
  async (url: string, options: FetchEventSourceInit) => {
163
268
  options.onopen!({ clone: () => ({ ok: true, headers: new Headers() }) } as any);
164
- options.onmessage!({ event: 'reasoning', data: JSON.stringify('Hello') } as any);
165
- await sleep(100);
166
- options.onmessage!({ event: 'reasoning', data: JSON.stringify(' World') } as any);
269
+ options.onmessage!({ event: 'grounding', data: JSON.stringify('Hello') } as any);
167
270
  await sleep(100);
168
271
  options.onmessage!({ event: 'text', data: JSON.stringify('hi') } as any);
169
272
  },
@@ -172,17 +275,17 @@ describe('fetchSSE', () => {
172
275
  await fetchSSE('/', {
173
276
  onMessageHandle: mockOnMessageHandle,
174
277
  onFinish: mockOnFinish,
175
- smoothing: true,
176
278
  });
177
279
 
178
- expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, { text: 'Hell', type: 'reasoning' });
179
- expect(mockOnMessageHandle).toHaveBeenNthCalledWith(2, { text: 'o', type: 'reasoning' });
180
- expect(mockOnMessageHandle).toHaveBeenNthCalledWith(3, { text: ' Wor', type: 'reasoning' });
181
- // more assertions for each character...
280
+ expect(mockOnMessageHandle).toHaveBeenNthCalledWith(1, {
281
+ grounding: 'Hello',
282
+ type: 'grounding',
283
+ });
284
+
182
285
  expect(mockOnFinish).toHaveBeenCalledWith('hi', {
183
286
  observationId: null,
184
287
  toolCalls: undefined,
185
- reasoning: 'Hello World',
288
+ grounding: 'Hello',
186
289
  traceId: null,
187
290
  type: 'done',
188
291
  });
@@ -9,6 +9,7 @@ import {
9
9
  MessageToolCall,
10
10
  MessageToolCallChunk,
11
11
  MessageToolCallSchema,
12
+ ModelReasoning,
12
13
  } from '@/types/message';
13
14
  import { GroundingSearch } from '@/types/search';
14
15
 
@@ -23,7 +24,7 @@ export type OnFinishHandler = (
23
24
  context: {
24
25
  grounding?: GroundingSearch;
25
26
  observationId?: string | null;
26
- reasoning?: string;
27
+ reasoning?: ModelReasoning;
27
28
  toolCalls?: MessageToolCall[];
28
29
  traceId?: string | null;
29
30
  type?: SSEFinishType;
@@ -36,7 +37,8 @@ export interface MessageTextChunk {
36
37
  }
37
38
 
38
39
  export interface MessageReasoningChunk {
39
- text: string;
40
+ signature?: string;
41
+ text?: string;
40
42
  type: 'reasoning';
41
43
  }
42
44
 
@@ -271,6 +273,8 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
271
273
  });
272
274
 
273
275
  let thinking = '';
276
+ let thinkingSignature: string | undefined;
277
+
274
278
  const thinkingController = createSmoothMessage({
275
279
  onTextUpdate: (delta, text) => {
276
280
  thinking = text;
@@ -365,6 +369,11 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
365
369
  break;
366
370
  }
367
371
 
372
+ case 'reasoning_signature': {
373
+ thinkingSignature = data;
374
+ break;
375
+ }
376
+
368
377
  case 'reasoning': {
369
378
  if (textSmoothing) {
370
379
  thinkingController.pushToQueue(data);
@@ -436,7 +445,7 @@ export const fetchSSE = async (url: string, options: RequestInit & FetchSSEOptio
436
445
  await options?.onFinish?.(output, {
437
446
  grounding,
438
447
  observationId,
439
- reasoning: !!thinking ? thinking : undefined,
448
+ reasoning: !!thinking ? { content: thinking, signature: thinkingSignature } : undefined,
440
449
  toolCalls,
441
450
  traceId,
442
451
  type: finishedType,
@@ -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;