@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.
- package/CHANGELOG.md +58 -0
- package/README.md +1 -1
- package/changelog/v1.json +21 -0
- package/locales/ar/chat.json +7 -1
- package/locales/ar/models.json +6 -9
- package/locales/bg-BG/chat.json +7 -1
- package/locales/bg-BG/models.json +6 -9
- package/locales/de-DE/chat.json +7 -1
- package/locales/de-DE/models.json +6 -9
- package/locales/en-US/chat.json +7 -1
- package/locales/en-US/models.json +6 -9
- package/locales/es-ES/chat.json +8 -2
- package/locales/es-ES/models.json +6 -9
- package/locales/fa-IR/chat.json +7 -1
- package/locales/fa-IR/models.json +6 -3
- package/locales/fr-FR/chat.json +7 -1
- package/locales/fr-FR/models.json +6 -9
- package/locales/it-IT/chat.json +7 -1
- package/locales/it-IT/models.json +6 -9
- package/locales/ja-JP/chat.json +7 -1
- package/locales/ja-JP/models.json +6 -9
- package/locales/ko-KR/chat.json +7 -1
- package/locales/ko-KR/models.json +6 -9
- package/locales/nl-NL/chat.json +8 -2
- package/locales/nl-NL/models.json +6 -9
- package/locales/pl-PL/chat.json +7 -1
- package/locales/pl-PL/models.json +6 -9
- package/locales/pt-BR/chat.json +7 -1
- package/locales/pt-BR/models.json +6 -9
- package/locales/ru-RU/chat.json +8 -2
- package/locales/ru-RU/models.json +6 -9
- package/locales/tr-TR/chat.json +7 -1
- package/locales/tr-TR/models.json +6 -9
- package/locales/vi-VN/chat.json +7 -1
- package/locales/vi-VN/models.json +6 -9
- package/locales/zh-CN/chat.json +7 -1
- package/locales/zh-CN/models.json +6 -9
- package/locales/zh-TW/chat.json +7 -1
- package/locales/zh-TW/models.json +6 -9
- package/package.json +2 -2
- package/src/app/(backend)/middleware/auth/index.ts +6 -0
- package/src/config/aiModels/anthropic.ts +5 -2
- package/src/config/aiModels/google.ts +7 -0
- package/src/const/message.ts +3 -0
- package/src/const/settings/agent.ts +2 -0
- package/src/features/ChatInput/ActionBar/Model/ControlsForm.tsx +38 -13
- package/src/features/ChatInput/ActionBar/Model/ReasoningTokenSlider.tsx +92 -0
- package/src/features/ChatInput/ActionBar/Model/index.tsx +13 -18
- package/src/libs/agent-runtime/anthropic/index.ts +32 -14
- package/src/libs/agent-runtime/google/index.test.ts +8 -0
- package/src/libs/agent-runtime/google/index.ts +18 -5
- package/src/libs/agent-runtime/types/chat.ts +16 -2
- package/src/libs/agent-runtime/utils/anthropicHelpers.test.ts +113 -0
- package/src/libs/agent-runtime/utils/anthropicHelpers.ts +7 -4
- package/src/libs/agent-runtime/utils/streams/anthropic.test.ts +371 -0
- package/src/libs/agent-runtime/utils/streams/anthropic.ts +80 -30
- package/src/libs/agent-runtime/utils/streams/openai.test.ts +181 -0
- package/src/libs/agent-runtime/utils/streams/openai.ts +40 -30
- package/src/libs/agent-runtime/utils/streams/protocol.ts +8 -0
- package/src/locales/default/chat.ts +7 -1
- package/src/services/__tests__/chat.test.ts +89 -50
- package/src/services/chat.ts +39 -1
- package/src/store/agent/slices/chat/__snapshots__/selectors.test.ts.snap +2 -0
- package/src/store/aiInfra/slices/aiModel/selectors.ts +6 -6
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +1 -1
- package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +2 -0
- package/src/types/agent/index.ts +23 -9
- package/src/types/aiModel.ts +3 -8
- package/src/types/message/base.ts +1 -0
- package/src/utils/fetch/__tests__/fetchSSE.test.ts +113 -10
- package/src/utils/fetch/fetchSSE.ts +12 -3
- 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
|
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
|
},
|
package/src/const/message.ts
CHANGED
@@ -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
|
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
|
15
|
-
|
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
|
-
|
21
|
-
items={
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
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
|
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
|
67
|
-
aiModelSelectors.
|
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,
|
88
|
+
className={cx(styles.model, isModelHasExtendParams && styles.modelWithControl)}
|
93
89
|
height={36}
|
94
90
|
width={36}
|
95
91
|
>
|
96
|
-
<
|
97
|
-
<
|
98
|
-
|
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
|
-
{
|
98
|
+
{isModelHasExtendParams && (
|
105
99
|
<Flexbox style={{ marginInlineStart: -4 }}>
|
106
100
|
<Popover
|
107
101
|
arrow={false}
|
108
102
|
content={<ControlsForm />}
|
109
|
-
|
103
|
+
placement={'topLeft'}
|
110
104
|
styles={{
|
111
105
|
body: {
|
112
|
-
minWidth: isMobile ? undefined :
|
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('
|
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
|
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(
|
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
|
-
|
141
|
-
|
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') &&
|
148
|
-
|
149
|
-
||
|
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 (
|
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 =
|
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
|
-
|
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),
|