@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
@@ -23,6 +23,12 @@ export type RequestHandler = (
23
23
 
24
24
  export const checkAuth =
25
25
  (handler: RequestHandler) => async (req: Request, options: RequestOptions) => {
26
+ // we have a special header to debug the api endpoint in development mode
27
+ const isDebugApi = req.headers.get('lobe-auth-dev-backend-api') === '1';
28
+ if (process.env.NODE_ENV === 'development' && isDebugApi) {
29
+ return handler(req, { ...options, jwtPayload: { userId: 'DEV_USER' } });
30
+ }
31
+
26
32
  let jwtPayload: JWTPayload;
27
33
 
28
34
  try {
@@ -9,8 +9,8 @@ const anthropicChatModels: AIChatModelCard[] = [
9
9
  },
10
10
  contextWindowTokens: 200_000,
11
11
  description:
12
- 'Claude 3.7 sonnet 是 Anthropic 最快的下一代模型。与 Claude 3 Haiku 相比,Claude 3.7 Sonnet 在各项技能上都有所提升,并在许多智力基准测试中超越了上一代最大的模型 Claude 3 Opus。',
13
- displayName: 'Claude 3.7 Sonnet',
12
+ 'Claude 3.7 Sonnet 是 Anthropic 迄今为止最智能的模型,也是市场上首个混合推理模型。Claude 3.7 Sonnet 可以产生近乎即时的响应或延长的逐步思考,用户可以清晰地看到这些过程。Sonnet 特别擅长编程、数据科学、视觉处理、代理任务。',
13
+ displayName: 'Claude 3.7 Sonnet 0219',
14
14
  enabled: true,
15
15
  id: 'claude-3-7-sonnet-20250219',
16
16
  maxOutput: 8192,
@@ -21,6 +21,9 @@ const anthropicChatModels: AIChatModelCard[] = [
21
21
  writeCacheInput: 3.75,
22
22
  },
23
23
  releasedAt: '2025-02-24',
24
+ settings: {
25
+ extendParams: ['enableReasoning', 'reasoningBudgetToken'],
26
+ },
24
27
  type: 'chat',
25
28
  },
26
29
  {
@@ -4,6 +4,7 @@ const googleChatModels: AIChatModelCard[] = [
4
4
  {
5
5
  abilities: {
6
6
  functionCall: true,
7
+ search: true,
7
8
  vision: true,
8
9
  },
9
10
  contextWindowTokens: 2_097_152 + 8192,
@@ -19,6 +20,10 @@ const googleChatModels: AIChatModelCard[] = [
19
20
  output: 0,
20
21
  },
21
22
  releasedAt: '2025-02-05',
23
+ settings: {
24
+ searchImpl: 'params',
25
+ searchProvider: 'google',
26
+ },
22
27
  type: 'chat',
23
28
  },
24
29
  {
@@ -49,6 +54,7 @@ const googleChatModels: AIChatModelCard[] = [
49
54
  {
50
55
  abilities: {
51
56
  functionCall: true,
57
+ search: true,
52
58
  vision: true,
53
59
  },
54
60
  contextWindowTokens: 1_048_576 + 8192,
@@ -65,6 +71,7 @@ const googleChatModels: AIChatModelCard[] = [
65
71
  releasedAt: '2025-02-05',
66
72
  settings: {
67
73
  searchImpl: 'params',
74
+ searchProvider: 'google',
68
75
  },
69
76
  type: 'chat',
70
77
  },
@@ -7,3 +7,6 @@ export const MESSAGE_THREAD_DIVIDER_ID = '__THREAD_DIVIDER__';
7
7
  export const MESSAGE_WELCOME_GUIDE_ID = 'welcome';
8
8
 
9
9
  export const THREAD_DRAFT_ID = '__THREAD_DRAFT_ID__';
10
+
11
+
12
+ export const MESSAGE_FLAGGED_THINKING='FLAGGED_THINKING'
@@ -19,7 +19,9 @@ export const DEFAULT_AGENT_CHAT_CONFIG: LobeAgentChatConfig = {
19
19
  enableAutoCreateTopic: true,
20
20
  enableCompressHistory: true,
21
21
  enableHistoryCount: true,
22
+ enableReasoning: true,
22
23
  historyCount: 8,
24
+ reasoningBudgetToken: 1024,
23
25
  searchMode: 'off',
24
26
  };
25
27
 
@@ -1,32 +1,57 @@
1
1
  import { Form } from '@lobehub/ui';
2
+ import type { FormItemProps } from '@lobehub/ui';
2
3
  import { Switch } from 'antd';
4
+ import isEqual from 'fast-deep-equal';
3
5
  import { memo } from 'react';
6
+ import { useTranslation } from 'react-i18next';
4
7
 
5
8
  import { useAgentStore } from '@/store/agent';
6
9
  import { agentSelectors } from '@/store/agent/slices/chat';
7
10
  import { aiModelSelectors, useAiInfraStore } from '@/store/aiInfra';
8
11
 
12
+ import ReasoningTokenSlider from './ReasoningTokenSlider';
13
+
9
14
  const ControlsForm = memo(() => {
10
- const [model, provider] = useAgentStore((s) => [
15
+ const { t } = useTranslation('chat');
16
+ const [model, provider, updateAgentChatConfig] = useAgentStore((s) => [
11
17
  agentSelectors.currentAgentModel(s),
12
18
  agentSelectors.currentAgentModelProvider(s),
19
+ s.updateAgentChatConfig,
13
20
  ]);
14
- const modelExtendControls = useAiInfraStore(
15
- aiModelSelectors.modelExtendControls(model, provider),
16
- );
21
+ const config = useAgentStore(agentSelectors.currentAgentChatConfig, isEqual);
22
+
23
+ const modelExtendParams = useAiInfraStore(aiModelSelectors.modelExtendParams(model, provider));
24
+
25
+ const items: FormItemProps[] = [
26
+ {
27
+ children: <Switch />,
28
+ label: t('extendParams.enableReasoning.title'),
29
+ minWidth: undefined,
30
+ name: 'enableReasoning',
31
+ },
32
+ {
33
+ children: <ReasoningTokenSlider />,
34
+ label: t('extendParams.reasoningBudgetToken.title'),
35
+ layout: 'vertical',
36
+ minWidth: undefined,
37
+ name: 'reasoningBudgetToken',
38
+ style: {
39
+ paddingBottom: 0,
40
+ },
41
+ },
42
+ ];
17
43
 
18
44
  return (
19
45
  <Form
20
- itemMinWidth={200}
21
- items={modelExtendControls!.map((item: any) => ({
22
- children: <Switch />,
23
- label: item.key,
24
- minWidth: undefined,
25
- name: item.key,
26
- }))}
46
+ initialValues={config}
47
+ items={
48
+ (modelExtendParams || [])
49
+ .map((item: any) => items.find((i) => i.name === item))
50
+ .filter(Boolean) as FormItemProps[]
51
+ }
27
52
  itemsType={'flat'}
28
- onValuesChange={(_, values) => {
29
- console.log(values);
53
+ onValuesChange={async (_, values) => {
54
+ await updateAgentChatConfig(values);
30
55
  }}
31
56
  size={'small'}
32
57
  style={{ fontSize: 12 }}
@@ -0,0 +1,92 @@
1
+ import { InputNumber, Slider } from 'antd';
2
+ import { memo, useMemo } from 'react';
3
+ import { Flexbox } from 'react-layout-kit';
4
+ import useMergeState from 'use-merge-value';
5
+
6
+ const Kibi = 1024;
7
+
8
+ const exponent = (num: number) => Math.log2(num);
9
+ const getRealValue = (num: number) => Math.round(Math.pow(2, num));
10
+ const powerKibi = (num: number) => Math.round(Math.pow(2, num) * Kibi);
11
+
12
+ interface MaxTokenSliderProps {
13
+ defaultValue?: number;
14
+ onChange?: (value: number) => void;
15
+ value?: number;
16
+ }
17
+
18
+ const MaxTokenSlider = memo<MaxTokenSliderProps>(({ value, onChange, defaultValue }) => {
19
+ const [token, setTokens] = useMergeState(0, {
20
+ defaultValue,
21
+ onChange,
22
+ value: value,
23
+ });
24
+
25
+ const [powValue, setPowValue] = useMergeState(0, {
26
+ defaultValue: exponent(typeof defaultValue === 'undefined' ? 0 : defaultValue / 1024),
27
+ value: exponent(typeof value === 'undefined' ? 0 : value / Kibi),
28
+ });
29
+
30
+ const updateWithPowValue = (value: number) => {
31
+ setPowValue(value);
32
+
33
+ setTokens(powerKibi(value));
34
+ };
35
+
36
+ const updateWithRealValue = (value: number) => {
37
+ setTokens(Math.round(value));
38
+
39
+ setPowValue(exponent(value / Kibi));
40
+ };
41
+
42
+ const marks = useMemo(() => {
43
+ return {
44
+ [exponent(1)]: '1k',
45
+ [exponent(2)]: '2k',
46
+ [exponent(4)]: '4k', // 4 kibi = 4096
47
+ [exponent(8)]: '8k',
48
+ [exponent(16)]: '16k',
49
+ [exponent(32)]: '32k',
50
+ [exponent(64)]: '64k',
51
+ };
52
+ }, []);
53
+
54
+ return (
55
+ <Flexbox align={'center'} gap={12} horizontal>
56
+ <Flexbox flex={1}>
57
+ <Slider
58
+ marks={marks}
59
+ max={exponent(64)}
60
+ min={exponent(1)}
61
+ onChange={updateWithPowValue}
62
+ step={null}
63
+ tooltip={{
64
+ formatter: (x) => {
65
+ if (typeof x === 'undefined') return;
66
+
67
+ let value = getRealValue(x);
68
+
69
+ if (value < Kibi) return ((value * Kibi) / 1000).toFixed(0) + 'k';
70
+ },
71
+ }}
72
+ value={powValue}
73
+ />
74
+ </Flexbox>
75
+ <div>
76
+ <InputNumber
77
+ changeOnWheel
78
+ min={0}
79
+ onChange={(e) => {
80
+ if (!e && e !== 0) return;
81
+
82
+ updateWithRealValue(e);
83
+ }}
84
+ step={4 * Kibi}
85
+ style={{ width: 60 }}
86
+ value={token}
87
+ />
88
+ </div>
89
+ </Flexbox>
90
+ );
91
+ });
92
+ export default MaxTokenSlider;
@@ -1,5 +1,5 @@
1
1
  import { ModelIcon } from '@lobehub/icons';
2
- import { ActionIcon, Tooltip } from '@lobehub/ui';
2
+ import { ActionIcon } from '@lobehub/ui';
3
3
  import { Popover } from 'antd';
4
4
  import { createStyles } from 'antd-style';
5
5
  import { Settings2Icon } from 'lucide-react';
@@ -63,8 +63,8 @@ const ModelSwitch = memo(() => {
63
63
  agentSelectors.currentAgentModelProvider(s),
64
64
  ]);
65
65
 
66
- const isModelHasExtendControls = useAiInfraStore(
67
- aiModelSelectors.isModelHasExtendControls(model, provider),
66
+ const isModelHasExtendParams = useAiInfraStore(
67
+ aiModelSelectors.isModelHasExtendParams(model, provider),
68
68
  );
69
69
 
70
70
  const isMobile = useIsMobile();
@@ -82,34 +82,29 @@ const ModelSwitch = memo(() => {
82
82
  // );
83
83
 
84
84
  return (
85
- <Flexbox
86
- align={'center'}
87
- className={isModelHasExtendControls ? styles.container : ''}
88
- horizontal
89
- >
85
+ <Flexbox align={'center'} className={isModelHasExtendParams ? styles.container : ''} horizontal>
90
86
  <ModelSwitchPanel>
91
87
  <Center
92
- className={cx(styles.model, isModelHasExtendControls && styles.modelWithControl)}
88
+ className={cx(styles.model, isModelHasExtendParams && styles.modelWithControl)}
93
89
  height={36}
94
90
  width={36}
95
91
  >
96
- <Tooltip placement={'bottom'} title={[provider, model].join(' / ')}>
97
- <div className={styles.icon}>
98
- <ModelIcon model={model} size={22} />
99
- </div>
100
- </Tooltip>
92
+ <div className={styles.icon}>
93
+ <ModelIcon model={model} size={22} />
94
+ </div>
101
95
  </Center>
102
96
  </ModelSwitchPanel>
103
97
 
104
- {isModelHasExtendControls && (
98
+ {isModelHasExtendParams && (
105
99
  <Flexbox style={{ marginInlineStart: -4 }}>
106
100
  <Popover
107
101
  arrow={false}
108
102
  content={<ControlsForm />}
109
- open
103
+ placement={'topLeft'}
110
104
  styles={{
111
105
  body: {
112
- minWidth: isMobile ? undefined : 200,
106
+ minWidth: isMobile ? undefined : 350,
107
+ paddingBlock: 4,
113
108
  width: isMobile ? '100vw' : undefined,
114
109
  },
115
110
  }}
@@ -118,7 +113,7 @@ const ModelSwitch = memo(() => {
118
113
  icon={Settings2Icon}
119
114
  placement={'bottom'}
120
115
  style={{ borderRadius: 20 }}
121
- title={t('extendControls.title')}
116
+ title={t('extendParams.title')}
122
117
  />
123
118
  </Popover>
124
119
  </Flexbox>
@@ -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[];
@@ -449,6 +449,14 @@ describe('LobeGoogleAI', () => {
449
449
  });
450
450
  expect(result).toEqual({ text: 'Hello' });
451
451
  });
452
+ it('should handle thinking type messages', async () => {
453
+ const result = await instance['convertContentToGooglePart']({
454
+ type: 'thinking',
455
+ thinking: 'Hello',
456
+ signature: 'abc',
457
+ });
458
+ expect(result).toEqual(undefined);
459
+ });
452
460
 
453
461
  it('should handle base64 type images', async () => {
454
462
  const base64Image =
@@ -208,11 +208,18 @@ export class LobeGoogleAI implements LobeRuntimeAI {
208
208
  system: system_message?.content,
209
209
  };
210
210
  }
211
- private convertContentToGooglePart = async (content: UserMessageContentPart): Promise<Part> => {
211
+ private convertContentToGooglePart = async (
212
+ content: UserMessageContentPart,
213
+ ): Promise<Part | undefined> => {
212
214
  switch (content.type) {
215
+ default: {
216
+ return undefined;
217
+ }
218
+
213
219
  case 'text': {
214
220
  return { text: content.text };
215
221
  }
222
+
216
223
  case 'image_url': {
217
224
  const { mimeType, base64, type } = parseDataUri(content.image_url.url);
218
225
 
@@ -261,11 +268,17 @@ export class LobeGoogleAI implements LobeRuntimeAI {
261
268
  };
262
269
  }
263
270
 
271
+ const getParts = async () => {
272
+ if (typeof content === 'string') return [{ text: content }];
273
+
274
+ const parts = await Promise.all(
275
+ content.map(async (c) => await this.convertContentToGooglePart(c)),
276
+ );
277
+ return parts.filter(Boolean) as Part[];
278
+ };
279
+
264
280
  return {
265
- parts:
266
- typeof content === 'string'
267
- ? [{ text: content }]
268
- : await Promise.all(content.map(async (c) => await this.convertContentToGooglePart(c))),
281
+ parts: await getParts(),
269
282
  role: message.role === 'assistant' ? 'model' : 'user',
270
283
  };
271
284
  };
@@ -2,6 +2,11 @@ import { MessageToolCall } from '@/types/message';
2
2
 
3
3
  export type LLMRoleType = 'user' | 'system' | 'assistant' | 'function' | 'tool';
4
4
 
5
+ interface UserMessageContentPartThinking {
6
+ signature: string;
7
+ thinking: string;
8
+ type: 'thinking';
9
+ }
5
10
  interface UserMessageContentPartText {
6
11
  text: string;
7
12
  type: 'text';
@@ -15,7 +20,10 @@ interface UserMessageContentPartImage {
15
20
  type: 'image_url';
16
21
  }
17
22
 
18
- export type UserMessageContentPart = UserMessageContentPartText | UserMessageContentPartImage;
23
+ export type UserMessageContentPart =
24
+ | UserMessageContentPartText
25
+ | UserMessageContentPartImage
26
+ | UserMessageContentPartThinking;
19
27
 
20
28
  export interface OpenAIChatMessage {
21
29
  /**
@@ -88,8 +96,14 @@ export interface ChatStreamPayload {
88
96
  * @default 1
89
97
  */
90
98
  temperature: number;
99
+ /**
100
+ * use for Claude
101
+ */
102
+ thinking?: {
103
+ budget_tokens: number;
104
+ type: 'enabled' | 'disabled';
105
+ };
91
106
  tool_choice?: string;
92
-
93
107
  tools?: ChatCompletionTool[];
94
108
  /**
95
109
  * @title 控制生成文本中最高概率的单个令牌
@@ -383,6 +383,119 @@ describe('anthropicHelpers', () => {
383
383
  { content: '继续', role: 'user' },
384
384
  ]);
385
385
  });
386
+
387
+ it('should correctly handle thinking content part', async () => {
388
+ const messages: OpenAIChatMessage[] = [
389
+ {
390
+ content: '告诉我杭州和北京的天气,先回答我好的',
391
+ role: 'user',
392
+ },
393
+ {
394
+ content: [
395
+ { thinking: '经过一番思考', type: 'thinking', signature: '123' },
396
+ {
397
+ type: 'text',
398
+ text: '好的,我会为您查询杭州和北京的天气信息。我现在就开始查询这两个城市的当前天气情况。',
399
+ },
400
+ ],
401
+ role: 'assistant',
402
+ tool_calls: [
403
+ {
404
+ function: {
405
+ arguments: '{"city": "\\u676d\\u5dde"}',
406
+ name: 'realtime-weather____fetchCurrentWeather',
407
+ },
408
+ id: 'toolu_018PNQkH8ChbjoJz4QBiFVod',
409
+ type: 'function',
410
+ },
411
+ {
412
+ function: {
413
+ arguments: '{"city": "\\u5317\\u4eac"}',
414
+ name: 'realtime-weather____fetchCurrentWeather',
415
+ },
416
+ id: 'toolu_018VQTQ6fwAEC3eppuEfMxPp',
417
+ type: 'function',
418
+ },
419
+ ],
420
+ },
421
+ {
422
+ content:
423
+ '[{"city":"杭州市","adcode":"330100","province":"浙江","reporttime":"2024-06-24 17:02:14","casts":[{"date":"2024-06-24","week":"1","dayweather":"小雨","nightweather":"中雨","daytemp":"26","nighttemp":"20","daywind":"西","nightwind":"西","daypower":"1-3","nightpower":"1-3","daytemp_float":"26.0","nighttemp_float":"20.0"},{"date":"2024-06-25","week":"2","dayweather":"大雨","nightweather":"中雨","daytemp":"23","nighttemp":"19","daywind":"东","nightwind":"东","daypower":"1-3","nightpower":"1-3","daytemp_float":"23.0","nighttemp_float":"19.0"},{"date":"2024-06-26","week":"3","dayweather":"中雨","nightweather":"中雨","daytemp":"24","nighttemp":"21","daywind":"东南","nightwind":"东南","daypower":"1-3","nightpower":"1-3","daytemp_float":"24.0","nighttemp_float":"21.0"},{"date":"2024-06-27","week":"4","dayweather":"中雨-大雨","nightweather":"中雨","daytemp":"24","nighttemp":"22","daywind":"南","nightwind":"南","daypower":"1-3","nightpower":"1-3","daytemp_float":"24.0","nighttemp_float":"22.0"}]}]',
424
+ name: 'realtime-weather____fetchCurrentWeather',
425
+ role: 'tool',
426
+ tool_call_id: 'toolu_018PNQkH8ChbjoJz4QBiFVod',
427
+ },
428
+ {
429
+ content:
430
+ '[{"city":"北京市","adcode":"110000","province":"北京","reporttime":"2024-06-24 17:03:11","casts":[{"date":"2024-06-24","week":"1","dayweather":"晴","nightweather":"晴","daytemp":"33","nighttemp":"20","daywind":"北","nightwind":"北","daypower":"1-3","nightpower":"1-3","daytemp_float":"33.0","nighttemp_float":"20.0"},{"date":"2024-06-25","week":"2","dayweather":"晴","nightweather":"晴","daytemp":"35","nighttemp":"21","daywind":"东南","nightwind":"东南","daypower":"1-3","nightpower":"1-3","daytemp_float":"35.0","nighttemp_float":"21.0"},{"date":"2024-06-26","week":"3","dayweather":"晴","nightweather":"晴","daytemp":"35","nighttemp":"23","daywind":"西南","nightwind":"西南","daypower":"1-3","nightpower":"1-3","daytemp_float":"35.0","nighttemp_float":"23.0"},{"date":"2024-06-27","week":"4","dayweather":"多云","nightweather":"多云","daytemp":"35","nighttemp":"23","daywind":"西南","nightwind":"西南","daypower":"1-3","nightpower":"1-3","daytemp_float":"35.0","nighttemp_float":"23.0"}]}]',
431
+ name: 'realtime-weather____fetchCurrentWeather',
432
+ role: 'tool',
433
+ tool_call_id: 'toolu_018VQTQ6fwAEC3eppuEfMxPp',
434
+ },
435
+ {
436
+ content: '继续',
437
+ role: 'user',
438
+ },
439
+ ];
440
+
441
+ const contents = await buildAnthropicMessages(messages);
442
+
443
+ expect(contents).toEqual([
444
+ { content: '告诉我杭州和北京的天气,先回答我好的', role: 'user' },
445
+ {
446
+ content: [
447
+ {
448
+ signature: '123',
449
+ thinking: '经过一番思考',
450
+ type: 'thinking',
451
+ },
452
+ {
453
+ text: '好的,我会为您查询杭州和北京的天气信息。我现在就开始查询这两个城市的当前天气情况。',
454
+ type: 'text',
455
+ },
456
+ {
457
+ id: 'toolu_018PNQkH8ChbjoJz4QBiFVod',
458
+ input: { city: '杭州' },
459
+ name: 'realtime-weather____fetchCurrentWeather',
460
+ type: 'tool_use',
461
+ },
462
+ {
463
+ id: 'toolu_018VQTQ6fwAEC3eppuEfMxPp',
464
+ input: { city: '北京' },
465
+ name: 'realtime-weather____fetchCurrentWeather',
466
+ type: 'tool_use',
467
+ },
468
+ ],
469
+ role: 'assistant',
470
+ },
471
+ {
472
+ content: [
473
+ {
474
+ content: [
475
+ {
476
+ text: '[{"city":"杭州市","adcode":"330100","province":"浙江","reporttime":"2024-06-24 17:02:14","casts":[{"date":"2024-06-24","week":"1","dayweather":"小雨","nightweather":"中雨","daytemp":"26","nighttemp":"20","daywind":"西","nightwind":"西","daypower":"1-3","nightpower":"1-3","daytemp_float":"26.0","nighttemp_float":"20.0"},{"date":"2024-06-25","week":"2","dayweather":"大雨","nightweather":"中雨","daytemp":"23","nighttemp":"19","daywind":"东","nightwind":"东","daypower":"1-3","nightpower":"1-3","daytemp_float":"23.0","nighttemp_float":"19.0"},{"date":"2024-06-26","week":"3","dayweather":"中雨","nightweather":"中雨","daytemp":"24","nighttemp":"21","daywind":"东南","nightwind":"东南","daypower":"1-3","nightpower":"1-3","daytemp_float":"24.0","nighttemp_float":"21.0"},{"date":"2024-06-27","week":"4","dayweather":"中雨-大雨","nightweather":"中雨","daytemp":"24","nighttemp":"22","daywind":"南","nightwind":"南","daypower":"1-3","nightpower":"1-3","daytemp_float":"24.0","nighttemp_float":"22.0"}]}]',
477
+ type: 'text',
478
+ },
479
+ ],
480
+ tool_use_id: 'toolu_018PNQkH8ChbjoJz4QBiFVod',
481
+ type: 'tool_result',
482
+ },
483
+ {
484
+ content: [
485
+ {
486
+ text: '[{"city":"北京市","adcode":"110000","province":"北京","reporttime":"2024-06-24 17:03:11","casts":[{"date":"2024-06-24","week":"1","dayweather":"晴","nightweather":"晴","daytemp":"33","nighttemp":"20","daywind":"北","nightwind":"北","daypower":"1-3","nightpower":"1-3","daytemp_float":"33.0","nighttemp_float":"20.0"},{"date":"2024-06-25","week":"2","dayweather":"晴","nightweather":"晴","daytemp":"35","nighttemp":"21","daywind":"东南","nightwind":"东南","daypower":"1-3","nightpower":"1-3","daytemp_float":"35.0","nighttemp_float":"21.0"},{"date":"2024-06-26","week":"3","dayweather":"晴","nightweather":"晴","daytemp":"35","nighttemp":"23","daywind":"西南","nightwind":"西南","daypower":"1-3","nightpower":"1-3","daytemp_float":"35.0","nighttemp_float":"23.0"},{"date":"2024-06-27","week":"4","dayweather":"多云","nightweather":"多云","daytemp":"35","nighttemp":"23","daywind":"西南","nightwind":"西南","daypower":"1-3","nightpower":"1-3","daytemp_float":"35.0","nighttemp_float":"23.0"}]}]',
487
+ type: 'text',
488
+ },
489
+ ],
490
+ tool_use_id: 'toolu_018VQTQ6fwAEC3eppuEfMxPp',
491
+ type: 'tool_result',
492
+ },
493
+ ],
494
+ role: 'user',
495
+ },
496
+ { content: '继续', role: 'user' },
497
+ ]);
498
+ });
386
499
  });
387
500
 
388
501
  describe('buildAnthropicTools', () => {
@@ -10,6 +10,7 @@ export const buildAnthropicBlock = async (
10
10
  content: UserMessageContentPart,
11
11
  ): Promise<Anthropic.ContentBlock | Anthropic.ImageBlockParam> => {
12
12
  switch (content.type) {
13
+ case 'thinking':
13
14
  case 'text': {
14
15
  // just pass-through the content
15
16
  return content as any;
@@ -83,13 +84,15 @@ export const buildAnthropicMessage = async (
83
84
  // if there is tool_calls , we need to covert the tool_calls to tool_use content block
84
85
  // refs: https://docs.anthropic.com/claude/docs/tool-use#tool-use-and-tool-result-content-blocks
85
86
  if (message.tool_calls) {
87
+ const messageContent =
88
+ typeof content === 'string'
89
+ ? [{ text: message.content, type: 'text' }]
90
+ : await Promise.all(content.map(async (c) => await buildAnthropicBlock(c)));
91
+
86
92
  return {
87
93
  content: [
88
94
  // avoid empty text content block
89
- !!message.content && {
90
- text: message.content as string,
91
- type: 'text',
92
- },
95
+ ...messageContent,
93
96
  ...(message.tool_calls.map((tool) => ({
94
97
  id: tool.id,
95
98
  input: JSON.parse(tool.function.arguments),