@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
@@ -97,12 +97,29 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
97
97
  }
98
98
 
99
99
  private async buildAnthropicPayload(payload: ChatStreamPayload) {
100
- const { messages, model, max_tokens = 4096, temperature, top_p, tools } = payload;
100
+ const { messages, model, max_tokens, temperature, top_p, tools, thinking } = payload;
101
101
  const system_message = messages.find((m) => m.role === 'system');
102
102
  const user_messages = messages.filter((m) => m.role !== 'system');
103
103
 
104
+ if (!!thinking) {
105
+ const maxTokens =
106
+ max_tokens ?? (thinking?.budget_tokens ? thinking?.budget_tokens + 4096 : 4096);
107
+
108
+ // `temperature` may only be set to 1 when thinking is enabled.
109
+ // `top_p` must be unset when thinking is enabled.
110
+ return {
111
+ max_tokens: maxTokens,
112
+ messages: await buildAnthropicMessages(user_messages),
113
+ model,
114
+ system: system_message?.content as string,
115
+
116
+ thinking,
117
+ tools: buildAnthropicTools(tools),
118
+ } satisfies Anthropic.MessageCreateParams;
119
+ }
120
+
104
121
  return {
105
- max_tokens,
122
+ max_tokens: max_tokens ?? 4096,
106
123
  messages: await buildAnthropicMessages(user_messages),
107
124
  model,
108
125
  system: system_message?.content as string,
@@ -124,29 +141,30 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
124
141
  method: 'GET',
125
142
  });
126
143
  const json = await response.json();
127
-
144
+
128
145
  const modelList: AnthropicModelCard[] = json['data'];
129
-
146
+
130
147
  return modelList
131
148
  .map((model) => {
132
- const knownModel = LOBE_DEFAULT_MODEL_LIST.find((m) => model.id.toLowerCase() === m.id.toLowerCase());
149
+ const knownModel = LOBE_DEFAULT_MODEL_LIST.find(
150
+ (m) => model.id.toLowerCase() === m.id.toLowerCase(),
151
+ );
133
152
 
134
153
  return {
135
154
  contextWindowTokens: knownModel?.contextWindowTokens ?? undefined,
136
155
  displayName: model.display_name,
137
156
  enabled: knownModel?.enabled || false,
138
157
  functionCall:
139
- model.id.toLowerCase().includes('claude-3')
140
- || knownModel?.abilities?.functionCall
141
- || false,
158
+ model.id.toLowerCase().includes('claude-3') ||
159
+ knownModel?.abilities?.functionCall ||
160
+ false,
142
161
  id: model.id,
143
- reasoning:
144
- knownModel?.abilities?.reasoning
145
- || false,
162
+ reasoning: knownModel?.abilities?.reasoning || false,
146
163
  vision:
147
- model.id.toLowerCase().includes('claude-3') && !model.id.toLowerCase().includes('claude-3-5-haiku')
148
- || knownModel?.abilities?.vision
149
- || false,
164
+ (model.id.toLowerCase().includes('claude-3') &&
165
+ !model.id.toLowerCase().includes('claude-3-5-haiku')) ||
166
+ knownModel?.abilities?.vision ||
167
+ false,
150
168
  };
151
169
  })
152
170
  .filter(Boolean) as ChatModelCard[];
@@ -88,8 +88,14 @@ export interface ChatStreamPayload {
88
88
  * @default 1
89
89
  */
90
90
  temperature: number;
91
+ /**
92
+ * use for Claude
93
+ */
94
+ thinking?: {
95
+ budget_tokens: number;
96
+ type: 'enabled' | 'disabled';
97
+ };
91
98
  tool_choice?: string;
92
-
93
99
  tools?: ChatCompletionTool[];
94
100
  /**
95
101
  * @title 控制生成文本中最高概率的单个令牌
@@ -384,6 +384,132 @@ describe('AnthropicStream', () => {
384
384
  expect(onToolCallMock).toHaveBeenCalledTimes(6);
385
385
  });
386
386
 
387
+ it('should handle thinking ', async () => {
388
+ const streams = [
389
+ {
390
+ type: 'message_start',
391
+ message: {
392
+ id: 'msg_01MNsLe7n1uVLtu6W8rCFujD',
393
+ type: 'message',
394
+ role: 'assistant',
395
+ model: 'claude-3-7-sonnet-20250219',
396
+ content: [],
397
+ stop_reason: null,
398
+ stop_sequence: null,
399
+ usage: {
400
+ input_tokens: 46,
401
+ cache_creation_input_tokens: 0,
402
+ cache_read_input_tokens: 0,
403
+ output_tokens: 11,
404
+ },
405
+ },
406
+ },
407
+ {
408
+ type: 'content_block_start',
409
+ index: 0,
410
+ content_block: { type: 'thinking', thinking: '', signature: '' },
411
+ },
412
+ {
413
+ type: 'content_block_delta',
414
+ index: 0,
415
+ delta: { type: 'thinking_delta', thinking: '我需要比较两个数字的' },
416
+ },
417
+ {
418
+ type: 'content_block_delta',
419
+ index: 0,
420
+ delta: { type: 'thinking_delta', thinking: '大小:9.8和9' },
421
+ },
422
+ {
423
+ type: 'content_block_delta',
424
+ index: 0,
425
+ delta: { type: 'thinking_delta', thinking: '11\n\n所以9.8比9.11大。' },
426
+ },
427
+ {
428
+ type: 'content_block_delta',
429
+ index: 0,
430
+ delta: {
431
+ type: 'signature_delta',
432
+ signature:
433
+ 'EuYBCkQYAiJAHnHRJG4nPBrdTlo6CmXoyE8WYoQeoPiLnXaeuaM8ExdiIEkVvxK1DYXOz5sCubs2s/G1NsST8A003Zb8XmuhYBIMwDGMZSZ3+gxOEBpVGgzdpOlDNBTxke31SngiMKUk6WcSiA11OSVBuInNukoAhnRd5jPAEg7e5mIoz/qJwnQHV8I+heKUreP77eJdFipQaM3FHn+avEHuLa/Z/fu0O9BftDi+caB1UWDwJakNeWX1yYTvK+N1v4gRpKbj4AhctfYHMjq8qX9XTnXme5AGzCYC6HgYw2/RfalWzwNxI6k=',
434
+ },
435
+ },
436
+ { type: 'content_block_stop', index: 0 },
437
+ { type: 'content_block_start', index: 1, content_block: { type: 'text', text: '' } },
438
+ {
439
+ type: 'content_block_delta',
440
+ index: 1,
441
+ delta: { type: 'text_delta', text: '9.8比9.11大。' },
442
+ },
443
+ { type: 'content_block_stop', index: 1 },
444
+ {
445
+ type: 'message_delta',
446
+ delta: { stop_reason: 'end_turn', stop_sequence: null },
447
+ usage: { output_tokens: 354 },
448
+ },
449
+ { type: 'message_stop' },
450
+ ];
451
+
452
+ const mockReadableStream = new ReadableStream({
453
+ start(controller) {
454
+ streams.forEach((chunk) => {
455
+ controller.enqueue(chunk);
456
+ });
457
+ controller.close();
458
+ },
459
+ });
460
+
461
+ const protocolStream = AnthropicStream(mockReadableStream);
462
+
463
+ const decoder = new TextDecoder();
464
+ const chunks = [];
465
+
466
+ // @ts-ignore
467
+ for await (const chunk of protocolStream) {
468
+ chunks.push(decoder.decode(chunk, { stream: true }));
469
+ }
470
+
471
+ expect(chunks).toEqual(
472
+ [
473
+ 'id: msg_01MNsLe7n1uVLtu6W8rCFujD',
474
+ 'event: data',
475
+ 'data: {"id":"msg_01MNsLe7n1uVLtu6W8rCFujD","type":"message","role":"assistant","model":"claude-3-7-sonnet-20250219","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":46,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":11}}\n',
476
+ 'id: msg_01MNsLe7n1uVLtu6W8rCFujD',
477
+ 'event: reasoning',
478
+ 'data: ""\n',
479
+ 'id: msg_01MNsLe7n1uVLtu6W8rCFujD',
480
+ 'event: reasoning',
481
+ 'data: "我需要比较两个数字的"\n',
482
+ 'id: msg_01MNsLe7n1uVLtu6W8rCFujD',
483
+ 'event: reasoning',
484
+ 'data: "大小:9.8和9"\n',
485
+ 'id: msg_01MNsLe7n1uVLtu6W8rCFujD',
486
+ 'event: reasoning',
487
+ 'data: "11\\n\\n所以9.8比9.11大。"\n',
488
+ // Tool calls
489
+ 'id: msg_01MNsLe7n1uVLtu6W8rCFujD',
490
+ 'event: reasoning_signature',
491
+ `data: "EuYBCkQYAiJAHnHRJG4nPBrdTlo6CmXoyE8WYoQeoPiLnXaeuaM8ExdiIEkVvxK1DYXOz5sCubs2s/G1NsST8A003Zb8XmuhYBIMwDGMZSZ3+gxOEBpVGgzdpOlDNBTxke31SngiMKUk6WcSiA11OSVBuInNukoAhnRd5jPAEg7e5mIoz/qJwnQHV8I+heKUreP77eJdFipQaM3FHn+avEHuLa/Z/fu0O9BftDi+caB1UWDwJakNeWX1yYTvK+N1v4gRpKbj4AhctfYHMjq8qX9XTnXme5AGzCYC6HgYw2/RfalWzwNxI6k="\n`,
492
+ 'id: msg_01MNsLe7n1uVLtu6W8rCFujD',
493
+ 'event: data',
494
+ `data: {"type":"content_block_stop","index":0}\n`,
495
+ 'id: msg_01MNsLe7n1uVLtu6W8rCFujD',
496
+ 'event: data',
497
+ `data: ""\n`,
498
+ 'id: msg_01MNsLe7n1uVLtu6W8rCFujD',
499
+ 'event: text',
500
+ `data: "9.8比9.11大。"\n`,
501
+ 'id: msg_01MNsLe7n1uVLtu6W8rCFujD',
502
+ 'event: data',
503
+ `data: {"type":"content_block_stop","index":1}\n`,
504
+ 'id: msg_01MNsLe7n1uVLtu6W8rCFujD',
505
+ 'event: stop',
506
+ 'data: "end_turn"\n',
507
+ 'id: msg_01MNsLe7n1uVLtu6W8rCFujD',
508
+ 'event: stop',
509
+ 'data: "message_stop"\n',
510
+ ].map((item) => `${item}\n`),
511
+ );
512
+ });
387
513
  it('should handle ReadableStream input', async () => {
388
514
  const mockReadableStream = new ReadableStream({
389
515
  start(controller) {
@@ -14,12 +14,12 @@ import {
14
14
 
15
15
  export const transformAnthropicStream = (
16
16
  chunk: Anthropic.MessageStreamEvent,
17
- stack: StreamContext,
17
+ context: StreamContext,
18
18
  ): StreamProtocolChunk => {
19
19
  // maybe need another structure to add support for multiple choices
20
20
  switch (chunk.type) {
21
21
  case 'message_start': {
22
- stack.id = chunk.message.id;
22
+ context.id = chunk.message.id;
23
23
  return { data: chunk.message, id: chunk.message.id, type: 'data' };
24
24
  }
25
25
  case 'content_block_start': {
@@ -27,12 +27,12 @@ export const transformAnthropicStream = (
27
27
  const toolChunk = chunk.content_block;
28
28
 
29
29
  // if toolIndex is not defined, set it to 0
30
- if (typeof stack.toolIndex === 'undefined') {
31
- stack.toolIndex = 0;
30
+ if (typeof context.toolIndex === 'undefined') {
31
+ context.toolIndex = 0;
32
32
  }
33
33
  // if toolIndex is defined, increment it
34
34
  else {
35
- stack.toolIndex += 1;
35
+ context.toolIndex += 1;
36
36
  }
37
37
 
38
38
  const toolCall: StreamToolCallChunkData = {
@@ -41,22 +41,36 @@ export const transformAnthropicStream = (
41
41
  name: toolChunk.name,
42
42
  },
43
43
  id: toolChunk.id,
44
- index: stack.toolIndex,
44
+ index: context.toolIndex,
45
45
  type: 'function',
46
46
  };
47
47
 
48
- stack.tool = { id: toolChunk.id, index: stack.toolIndex, name: toolChunk.name };
48
+ context.tool = { id: toolChunk.id, index: context.toolIndex, name: toolChunk.name };
49
49
 
50
- return { data: [toolCall], id: stack.id, type: 'tool_calls' };
50
+ return { data: [toolCall], id: context.id, type: 'tool_calls' };
51
51
  }
52
52
 
53
- return { data: chunk.content_block.text, id: stack.id, type: 'data' };
53
+ if (chunk.content_block.type === 'thinking') {
54
+ const thinkingChunk = chunk.content_block;
55
+
56
+ return { data: thinkingChunk.thinking, id: context.id, type: 'reasoning' };
57
+ }
58
+
59
+ if (chunk.content_block.type === 'redacted_thinking') {
60
+ return {
61
+ data: chunk.content_block.data,
62
+ id: context.id,
63
+ type: 'reasoning',
64
+ };
65
+ }
66
+
67
+ return { data: chunk.content_block.text, id: context.id, type: 'data' };
54
68
  }
55
69
 
56
70
  case 'content_block_delta': {
57
71
  switch (chunk.delta.type) {
58
72
  case 'text_delta': {
59
- return { data: chunk.delta.text, id: stack.id, type: 'text' };
73
+ return { data: chunk.delta.text, id: context.id, type: 'text' };
60
74
  }
61
75
 
62
76
  case 'input_json_delta': {
@@ -64,34 +78,50 @@ export const transformAnthropicStream = (
64
78
 
65
79
  const toolCall: StreamToolCallChunkData = {
66
80
  function: { arguments: delta },
67
- index: stack.toolIndex || 0,
81
+ index: context.toolIndex || 0,
68
82
  type: 'function',
69
83
  };
70
84
 
71
85
  return {
72
86
  data: [toolCall],
73
- id: stack.id,
87
+ id: context.id,
74
88
  type: 'tool_calls',
75
89
  } as StreamProtocolToolCallChunk;
76
90
  }
77
91
 
92
+ case 'signature_delta': {
93
+ return {
94
+ data: chunk.delta.signature,
95
+ id: context.id,
96
+ type: 'reasoning_signature' as any,
97
+ };
98
+ }
99
+
100
+ case 'thinking_delta': {
101
+ return {
102
+ data: chunk.delta.thinking,
103
+ id: context.id,
104
+ type: 'reasoning',
105
+ };
106
+ }
107
+
78
108
  default: {
79
109
  break;
80
110
  }
81
111
  }
82
- return { data: chunk, id: stack.id, type: 'data' };
112
+ return { data: chunk, id: context.id, type: 'data' };
83
113
  }
84
114
 
85
115
  case 'message_delta': {
86
- return { data: chunk.delta.stop_reason, id: stack.id, type: 'stop' };
116
+ return { data: chunk.delta.stop_reason, id: context.id, type: 'stop' };
87
117
  }
88
118
 
89
119
  case 'message_stop': {
90
- return { data: 'message_stop', id: stack.id, type: 'stop' };
120
+ return { data: 'message_stop', id: context.id, type: 'stop' };
91
121
  }
92
122
 
93
123
  default: {
94
- return { data: chunk, id: stack.id, type: 'data' };
124
+ return { data: chunk, id: context.id, type: 'data' };
95
125
  }
96
126
  }
97
127
  };
@@ -12,6 +12,10 @@ export interface StreamContext {
12
12
  * this flag is used to check if the pplx citation is returned,and then not return it again
13
13
  */
14
14
  returnedPplxCitation?: boolean;
15
+ thinking?: {
16
+ id: string;
17
+ name: string;
18
+ };
15
19
  tool?: {
16
20
  id: string;
17
21
  index: number;
@@ -32,7 +32,13 @@ export default {
32
32
  },
33
33
  duplicateTitle: '{{title}} 副本',
34
34
  emptyAgent: '暂无助手',
35
- extendControls: {
35
+ extendParams: {
36
+ enableReasoning: {
37
+ title: '开启深度思考',
38
+ },
39
+ reasoningBudgetToken: {
40
+ title: '思考消耗 Token',
41
+ },
36
42
  title: '模型扩展功能',
37
43
  },
38
44
  historyRange: '历史范围',
@@ -214,9 +214,35 @@ class ChatService {
214
214
 
215
215
  const tools = shouldUseTools ? filterTools : undefined;
216
216
 
217
+ // ============ 3. process extend params ============ //
218
+
219
+ let extendParams: Record<string, any> = {};
220
+
221
+ const isModelHasExtendParams = aiModelSelectors.isModelHasExtendParams(
222
+ payload.model,
223
+ payload.provider!,
224
+ )(useAiInfraStore.getState());
225
+
226
+ // model
227
+ if (isModelHasExtendParams) {
228
+ const modelExtendParams = aiModelSelectors.modelExtendParams(
229
+ payload.model,
230
+ payload.provider!,
231
+ )(useAiInfraStore.getState());
232
+ // if model has extended params, then we need to check if the model can use reasoning
233
+
234
+ if (modelExtendParams!.includes('enableReasoning') && chatConfig.enableReasoning) {
235
+ extendParams.thinking = {
236
+ budget_tokens: chatConfig.reasoningBudgetToken || 1024,
237
+ type: 'enabled',
238
+ };
239
+ }
240
+ }
241
+
217
242
  return this.getChatCompletion(
218
243
  {
219
244
  ...params,
245
+ ...extendParams,
220
246
  enabledSearch: enabledSearch && isModelHasBuiltinSearch ? true : undefined,
221
247
  messages: oaiMessages,
222
248
  tools,
@@ -8,7 +8,9 @@ exports[`agentSelectors > defaultAgentConfig > should merge DEFAULT_AGENT_CONFIG
8
8
  "enableAutoCreateTopic": true,
9
9
  "enableCompressHistory": true,
10
10
  "enableHistoryCount": true,
11
+ "enableReasoning": true,
11
12
  "historyCount": 8,
13
+ "reasoningBudgetToken": 1024,
12
14
  "searchMode": "off",
13
15
  },
14
16
  "model": "gpt-3.5-turbo",
@@ -70,14 +70,14 @@ const modelContextWindowTokens = (id: string, provider: string) => (s: AIProvide
70
70
  return model?.contextWindowTokens;
71
71
  };
72
72
 
73
- const modelExtendControls = (id: string, provider: string) => (s: AIProviderStoreState) => {
73
+ const modelExtendParams = (id: string, provider: string) => (s: AIProviderStoreState) => {
74
74
  const model = getEnabledModelById(id, provider)(s);
75
75
 
76
- return model?.settings?.extendControls;
76
+ return model?.settings?.extendParams;
77
77
  };
78
78
 
79
- const isModelHasExtendControls = (id: string, provider: string) => (s: AIProviderStoreState) => {
80
- const controls = modelExtendControls(id, provider)(s);
79
+ const isModelHasExtendParams = (id: string, provider: string) => (s: AIProviderStoreState) => {
80
+ const controls = modelExtendParams(id, provider)(s);
81
81
 
82
82
  return !!controls && controls.length > 0;
83
83
  };
@@ -119,13 +119,13 @@ export const aiModelSelectors = {
119
119
  isModelHasBuiltinSearch,
120
120
  isModelHasBuiltinSearchConfig,
121
121
  isModelHasContextWindowToken,
122
- isModelHasExtendControls,
122
+ isModelHasExtendParams,
123
123
  isModelLoading,
124
124
  isModelSupportReasoning,
125
125
  isModelSupportToolUse,
126
126
  isModelSupportVision,
127
127
  modelBuiltinSearchImpl,
128
128
  modelContextWindowTokens,
129
- modelExtendControls,
129
+ modelExtendParams,
130
130
  totalAiProviderModelList,
131
131
  };