@lobehub/chat 1.68.11 → 1.69.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 (90) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/chat.json +8 -0
  4. package/locales/bg-BG/chat.json +8 -0
  5. package/locales/de-DE/chat.json +8 -0
  6. package/locales/en-US/chat.json +8 -0
  7. package/locales/es-ES/chat.json +8 -0
  8. package/locales/fa-IR/chat.json +8 -0
  9. package/locales/fr-FR/chat.json +8 -0
  10. package/locales/it-IT/chat.json +8 -0
  11. package/locales/ja-JP/chat.json +8 -0
  12. package/locales/ko-KR/chat.json +8 -0
  13. package/locales/nl-NL/chat.json +8 -0
  14. package/locales/pl-PL/chat.json +8 -0
  15. package/locales/pt-BR/chat.json +8 -0
  16. package/locales/ru-RU/chat.json +8 -0
  17. package/locales/tr-TR/chat.json +8 -0
  18. package/locales/vi-VN/chat.json +8 -0
  19. package/locales/zh-CN/chat.json +8 -0
  20. package/locales/zh-TW/chat.json +8 -0
  21. package/next.config.ts +6 -0
  22. package/package.json +1 -1
  23. package/packages/web-crawler/src/crawImpl/naive.ts +19 -12
  24. package/packages/web-crawler/src/urlRules.ts +9 -1
  25. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/ChatItem/index.tsx +9 -18
  26. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/WelcomeMessage.tsx +2 -5
  27. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/HeaderAction.tsx +3 -2
  28. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/Main.tsx +56 -30
  29. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/Tags/HistoryLimitTags.tsx +26 -0
  30. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/{SearchTags.tsx → Tags/SearchTags.tsx} +7 -4
  31. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/{Tags.tsx → Tags/index.tsx} +4 -1
  32. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/index.tsx +1 -1
  33. package/src/config/aiModels/anthropic.ts +16 -1
  34. package/src/config/aiModels/google.ts +0 -1
  35. package/src/config/aiModels/groq.ts +14 -0
  36. package/src/config/aiModels/novita.ts +36 -0
  37. package/src/config/aiModels/siliconcloud.ts +18 -2
  38. package/src/config/modelProviders/anthropic.ts +0 -2
  39. package/src/const/layoutTokens.test.ts +1 -1
  40. package/src/const/layoutTokens.ts +1 -1
  41. package/src/const/models.ts +27 -0
  42. package/src/features/ChatInput/ActionBar/History.tsx +6 -3
  43. package/src/features/ChatInput/ActionBar/Model/ContextCachingSwitch.tsx +20 -0
  44. package/src/features/ChatInput/ActionBar/Model/ControlsForm.tsx +49 -7
  45. package/src/features/ChatInput/ActionBar/Model/ReasoningTokenSlider.tsx +6 -14
  46. package/src/features/ChatInput/ActionBar/Search/ModelBuiltinSearch.tsx +2 -2
  47. package/src/features/ChatInput/ActionBar/Search/SwitchPanel.tsx +2 -2
  48. package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +3 -5
  49. package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +2 -0
  50. package/src/features/Conversation/Messages/Assistant/Tool/Render/index.tsx +5 -1
  51. package/src/features/Conversation/Messages/Assistant/Tool/index.tsx +2 -0
  52. package/src/features/Conversation/components/ChatItem/index.tsx +3 -6
  53. package/src/features/Portal/Thread/Chat/ChatItem.tsx +4 -9
  54. package/src/hooks/useAgentEnableSearch.ts +2 -2
  55. package/src/libs/agent-runtime/anthropic/index.test.ts +36 -7
  56. package/src/libs/agent-runtime/anthropic/index.ts +30 -8
  57. package/src/libs/agent-runtime/azureOpenai/index.ts +4 -9
  58. package/src/libs/agent-runtime/azureai/index.ts +4 -9
  59. package/src/libs/agent-runtime/openai/index.ts +21 -38
  60. package/src/libs/agent-runtime/types/chat.ts +4 -0
  61. package/src/libs/agent-runtime/utils/anthropicHelpers.test.ts +55 -0
  62. package/src/libs/agent-runtime/utils/anthropicHelpers.ts +37 -3
  63. package/src/libs/langchain/loaders/code/__tests__/long.json +2 -2
  64. package/src/libs/langchain/loaders/code/__tests__/long.txt +1 -1
  65. package/src/locales/default/chat.ts +8 -0
  66. package/src/store/agent/initialState.ts +2 -2
  67. package/src/store/agent/selectors.ts +1 -1
  68. package/src/store/agent/slices/chat/{selectors.test.ts → selectors/agent.test.ts} +2 -2
  69. package/src/store/agent/slices/chat/{selectors.ts → selectors/agent.ts} +24 -33
  70. package/src/store/agent/slices/chat/selectors/chatConfig.test.ts +184 -0
  71. package/src/store/agent/slices/chat/selectors/chatConfig.ts +65 -0
  72. package/src/store/agent/slices/chat/selectors/index.ts +2 -0
  73. package/src/store/agent/store.ts +2 -2
  74. package/src/store/chat/helpers.test.ts +7 -7
  75. package/src/store/chat/helpers.ts +11 -7
  76. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts +3 -3
  77. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +11 -2
  78. package/src/store/chat/slices/aiChat/actions/helpers.ts +6 -2
  79. package/src/store/chat/slices/builtinTool/actions/searXNG.ts +28 -20
  80. package/src/store/chat/slices/message/selectors.ts +7 -3
  81. package/src/store/chat/slices/thread/selectors/index.ts +7 -3
  82. package/src/tools/web-browsing/Render/PageContent/Result.tsx +4 -2
  83. package/src/tools/web-browsing/Render/index.tsx +2 -0
  84. package/src/types/agent/index.ts +4 -0
  85. package/src/types/aiModel.ts +1 -1
  86. package/src/types/aiProvider.ts +60 -31
  87. /package/packages/web-crawler/src/{__test__ → __tests__}/crawler.test.ts +0 -0
  88. /package/packages/web-crawler/src/crawImpl/{__test__ → __tests__}/jina.test.ts +0 -0
  89. /package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/{KnowledgeTag.tsx → Tags/KnowledgeTag.tsx} +0 -0
  90. /package/src/store/agent/slices/chat/{__snapshots__/selectors.test.ts.snap → selectors/__snapshots__/agent.test.ts.snap} +0 -0
@@ -6,7 +6,6 @@ import useMergeState from 'use-merge-value';
6
6
  const Kibi = 1024;
7
7
 
8
8
  const exponent = (num: number) => Math.log2(num);
9
- const getRealValue = (num: number) => Math.round(Math.pow(2, num));
10
9
  const powerKibi = (num: number) => Math.round(Math.pow(2, num) * Kibi);
11
10
 
12
11
  interface MaxTokenSliderProps {
@@ -15,7 +14,7 @@ interface MaxTokenSliderProps {
15
14
  value?: number;
16
15
  }
17
16
 
18
- const MaxTokenSlider = memo<MaxTokenSliderProps>(({ value, onChange, defaultValue }) => {
17
+ const ReasoningTokenSlider = memo<MaxTokenSliderProps>(({ value, onChange, defaultValue }) => {
19
18
  const [token, setTokens] = useMergeState(0, {
20
19
  defaultValue,
21
20
  onChange,
@@ -30,7 +29,7 @@ const MaxTokenSlider = memo<MaxTokenSliderProps>(({ value, onChange, defaultValu
30
29
  const updateWithPowValue = (value: number) => {
31
30
  setPowValue(value);
32
31
 
33
- setTokens(powerKibi(value));
32
+ setTokens(Math.min(powerKibi(value), 64_000));
34
33
  };
35
34
 
36
35
  const updateWithRealValue = (value: number) => {
@@ -52,7 +51,7 @@ const MaxTokenSlider = memo<MaxTokenSliderProps>(({ value, onChange, defaultValu
52
51
  }, []);
53
52
 
54
53
  return (
55
- <Flexbox align={'center'} gap={12} horizontal>
54
+ <Flexbox align={'center'} gap={12} horizontal paddingInline={'4px 0'}>
56
55
  <Flexbox flex={1}>
57
56
  <Slider
58
57
  marks={marks}
@@ -60,21 +59,14 @@ const MaxTokenSlider = memo<MaxTokenSliderProps>(({ value, onChange, defaultValu
60
59
  min={exponent(1)}
61
60
  onChange={updateWithPowValue}
62
61
  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
- }}
62
+ tooltip={{ open: false }}
72
63
  value={powValue}
73
64
  />
74
65
  </Flexbox>
75
66
  <div>
76
67
  <InputNumber
77
68
  changeOnWheel
69
+ max={64_000}
78
70
  min={0}
79
71
  onChange={(e) => {
80
72
  if (!e && e !== 0) return;
@@ -89,4 +81,4 @@ const MaxTokenSlider = memo<MaxTokenSliderProps>(({ value, onChange, defaultValu
89
81
  </Flexbox>
90
82
  );
91
83
  });
92
- export default MaxTokenSlider;
84
+ export default ReasoningTokenSlider;
@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
7
7
  import { Flexbox } from 'react-layout-kit';
8
8
 
9
9
  import { useAgentStore } from '@/store/agent';
10
- import { agentSelectors } from '@/store/agent/selectors';
10
+ import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
11
11
  import { aiModelSelectors, useAiInfraStore } from '@/store/aiInfra';
12
12
 
13
13
  import ExaIcon from './ExaIcon';
@@ -37,7 +37,7 @@ const ModelBuiltinSearch = memo(() => {
37
37
  const [model, provider, checked, updateAgentChatConfig] = useAgentStore((s) => [
38
38
  agentSelectors.currentAgentModel(s),
39
39
  agentSelectors.currentAgentModelProvider(s),
40
- agentSelectors.currentAgentChatConfig(s).useModelBuiltinSearch,
40
+ agentChatConfigSelectors.useModelBuiltinSearch(s),
41
41
  s.updateAgentChatConfig,
42
42
  ]);
43
43
 
@@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next';
8
8
  import { Flexbox } from 'react-layout-kit';
9
9
 
10
10
  import { useAgentStore } from '@/store/agent';
11
- import { agentSelectors } from '@/store/agent/slices/chat';
11
+ import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/slices/chat';
12
12
  import { aiModelSelectors, useAiInfraStore } from '@/store/aiInfra';
13
13
  import { SearchMode } from '@/types/search';
14
14
 
@@ -84,7 +84,7 @@ const Item = memo<NetworkOption>(({ value, description, icon, label, disable })
84
84
  const { t } = useTranslation('chat');
85
85
  const { styles } = useStyles();
86
86
  const [mode, updateAgentChatConfig] = useAgentStore((s) => [
87
- agentSelectors.agentSearchMode(s),
87
+ agentChatConfigSelectors.agentSearchMode(s),
88
88
  s.updateAgentChatConfig,
89
89
  ]);
90
90
 
@@ -10,7 +10,7 @@ import { useModelContextWindowTokens } from '@/hooks/useModelContextWindowTokens
10
10
  import { useModelSupportToolUse } from '@/hooks/useModelSupportToolUse';
11
11
  import { useTokenCount } from '@/hooks/useTokenCount';
12
12
  import { useAgentStore } from '@/store/agent';
13
- import { agentSelectors } from '@/store/agent/selectors';
13
+ import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
14
14
  import { useChatStore } from '@/store/chat';
15
15
  import { topicSelectors } from '@/store/chat/selectors';
16
16
  import { useToolStore } from '@/store/tool';
@@ -31,15 +31,13 @@ const Token = memo<TokenTagProps>(({ total: messageString }) => {
31
31
  ]);
32
32
 
33
33
  const [systemRole, model, provider] = useAgentStore((s) => {
34
- const config = agentSelectors.currentAgentChatConfig(s);
35
-
36
34
  return [
37
35
  agentSelectors.currentAgentSystemRole(s),
38
36
  agentSelectors.currentAgentModel(s) as string,
39
37
  agentSelectors.currentAgentModelProvider(s) as string,
40
38
  // add these two params to enable the component to re-render
41
- config.historyCount,
42
- config.enableHistoryCount,
39
+ agentChatConfigSelectors.historyCount(s),
40
+ agentChatConfigSelectors.enableHistoryCount(s),
43
41
  ];
44
42
  });
45
43
 
@@ -57,4 +57,6 @@ const CustomRender = memo<CustomRenderProps>(
57
57
  },
58
58
  );
59
59
 
60
+ CustomRender.displayName = 'CustomRender';
61
+
60
62
  export default CustomRender;
@@ -1,3 +1,4 @@
1
+ import isEqual from 'fast-deep-equal';
1
2
  import { Suspense, memo } from 'react';
2
3
 
3
4
  import { LOADING_FLAT } from '@/const/message';
@@ -16,10 +17,11 @@ interface RenderProps {
16
17
  toolCallId: string;
17
18
  toolIndex: number;
18
19
  }
20
+
19
21
  const Render = memo<RenderProps>(
20
22
  ({ toolCallId, toolIndex, messageId, requestArgs, showPluginRender, setShowPluginRender }) => {
21
23
  const loading = useChatStore(chatSelectors.isToolCallStreaming(messageId, toolIndex));
22
- const toolMessage = useChatStore(chatSelectors.getMessageByToolCallId(toolCallId));
24
+ const toolMessage = useChatStore(chatSelectors.getMessageByToolCallId(toolCallId), isEqual);
23
25
 
24
26
  // 如果处于 loading 或者找不到 toolMessage 则展示 Arguments
25
27
  if (loading || !toolMessage) return <Arguments arguments={requestArgs} />;
@@ -48,4 +50,6 @@ const Render = memo<RenderProps>(
48
50
  },
49
51
  );
50
52
 
53
+ Render.displayName = 'ToolRender';
54
+
51
55
  export default Render;
@@ -52,4 +52,6 @@ const Tool = memo<InspectorProps>(
52
52
  },
53
53
  );
54
54
 
55
+ Tool.displayName = 'AssistantTool';
56
+
55
57
  export default Tool;
@@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next';
8
8
  import { Flexbox } from 'react-layout-kit';
9
9
 
10
10
  import { useAgentStore } from '@/store/agent';
11
- import { agentSelectors } from '@/store/agent/selectors';
11
+ import { agentChatConfigSelectors } from '@/store/agent/selectors';
12
12
  import { useChatStore } from '@/store/chat';
13
13
  import { chatSelectors } from '@/store/chat/selectors';
14
14
  import { useUserStore } from '@/store/user';
@@ -65,15 +65,12 @@ const Item = memo<ChatListItemProps>(
65
65
  disableEditing,
66
66
  inPortalThread = false,
67
67
  }) => {
68
- const fontSize = useUserStore(userGeneralSettingsSelectors.fontSize);
69
68
  const { t } = useTranslation('common');
70
69
  const { styles, cx } = useStyles();
71
- const [type = 'chat'] = useAgentStore((s) => {
72
- const config = agentSelectors.currentAgentChatConfig(s);
73
- return [config.displayMode];
74
- });
75
70
 
71
+ const type = useAgentStore(agentChatConfigSelectors.displayMode);
76
72
  const item = useChatStore(chatSelectors.getMessageById(id), isEqual);
73
+ const fontSize = useUserStore(userGeneralSettingsSelectors.fontSize);
77
74
 
78
75
  const [
79
76
  isMessageLoading,
@@ -3,7 +3,7 @@ import React, { memo, useMemo } from 'react';
3
3
  import { ChatItem } from '@/features/Conversation';
4
4
  import ActionsBar from '@/features/Conversation/components/ChatItem/ActionsBar';
5
5
  import { useAgentStore } from '@/store/agent';
6
- import { agentSelectors } from '@/store/agent/selectors';
6
+ import { agentChatConfigSelectors } from '@/store/agent/selectors';
7
7
  import { useChatStore } from '@/store/chat';
8
8
  import { threadSelectors } from '@/store/chat/selectors';
9
9
 
@@ -35,14 +35,9 @@ const ThreadChatItem = memo<ThreadChatItemProps>(({ id, index }) => {
35
35
  [id, isParentMessage],
36
36
  );
37
37
 
38
- const enableHistoryDivider = useAgentStore((s) => {
39
- const config = agentSelectors.currentAgentChatConfig(s);
40
- return (
41
- config.enableHistoryCount &&
42
- historyLength > (config.historyCount ?? 0) &&
43
- config.historyCount === historyLength - index
44
- );
45
- });
38
+ const enableHistoryDivider = useAgentStore(
39
+ agentChatConfigSelectors.enableHistoryDivider(historyLength, index),
40
+ );
46
41
 
47
42
  return (
48
43
  <ChatItem
@@ -1,12 +1,12 @@
1
1
  import { useAgentStore } from '@/store/agent';
2
- import { agentSelectors } from '@/store/agent/slices/chat';
2
+ import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
3
3
  import { aiModelSelectors, useAiInfraStore } from '@/store/aiInfra';
4
4
 
5
5
  export const useAgentEnableSearch = () => {
6
6
  const [model, provider, agentSearchMode] = useAgentStore((s) => [
7
7
  agentSelectors.currentAgentModel(s),
8
8
  agentSelectors.currentAgentModelProvider(s),
9
- agentSelectors.agentSearchMode(s),
9
+ agentChatConfigSelectors.agentSearchMode(s),
10
10
  ]);
11
11
 
12
12
  const isModelSupportToolUse = useAiInfraStore(
@@ -81,7 +81,12 @@ describe('LobeAnthropicAI', () => {
81
81
  expect(instance['client'].messages.create).toHaveBeenCalledWith(
82
82
  {
83
83
  max_tokens: 4096,
84
- messages: [{ content: 'Hello', role: 'user' }],
84
+ messages: [
85
+ {
86
+ content: [{ cache_control: { type: 'ephemeral' }, text: 'Hello', type: 'text' }],
87
+ role: 'user',
88
+ },
89
+ ],
85
90
  model: 'claude-3-haiku-20240307',
86
91
  stream: true,
87
92
  temperature: 0,
@@ -117,10 +122,21 @@ describe('LobeAnthropicAI', () => {
117
122
  expect(instance['client'].messages.create).toHaveBeenCalledWith(
118
123
  {
119
124
  max_tokens: 4096,
120
- messages: [{ content: 'Hello', role: 'user' }],
125
+ messages: [
126
+ {
127
+ content: [{ cache_control: { type: 'ephemeral' }, text: 'Hello', type: 'text' }],
128
+ role: 'user',
129
+ },
130
+ ],
121
131
  model: 'claude-3-haiku-20240307',
122
132
  stream: true,
123
- system: 'You are an awesome greeter',
133
+ system: [
134
+ {
135
+ cache_control: { type: 'ephemeral' },
136
+ type: 'text',
137
+ text: 'You are an awesome greeter',
138
+ },
139
+ ],
124
140
  temperature: 0,
125
141
  },
126
142
  {},
@@ -152,7 +168,12 @@ describe('LobeAnthropicAI', () => {
152
168
  expect(instance['client'].messages.create).toHaveBeenCalledWith(
153
169
  {
154
170
  max_tokens: 2048,
155
- messages: [{ content: 'Hello', role: 'user' }],
171
+ messages: [
172
+ {
173
+ content: [{ cache_control: { type: 'ephemeral' }, text: 'Hello', type: 'text' }],
174
+ role: 'user',
175
+ },
176
+ ],
156
177
  model: 'claude-3-haiku-20240307',
157
178
  stream: true,
158
179
  temperature: 0.25,
@@ -189,7 +210,12 @@ describe('LobeAnthropicAI', () => {
189
210
  expect(instance['client'].messages.create).toHaveBeenCalledWith(
190
211
  {
191
212
  max_tokens: 2048,
192
- messages: [{ content: 'Hello', role: 'user' }],
213
+ messages: [
214
+ {
215
+ content: [{ cache_control: { type: 'ephemeral' }, text: 'Hello', type: 'text' }],
216
+ role: 'user',
217
+ },
218
+ ],
193
219
  model: 'claude-3-haiku-20240307',
194
220
  stream: true,
195
221
  temperature: 0.25,
@@ -240,7 +266,7 @@ describe('LobeAnthropicAI', () => {
240
266
  });
241
267
 
242
268
  describe('chat with tools', () => {
243
- it('should call client.beta.tools.messages.create when tools are provided', async () => {
269
+ it('should call tools when tools are provided', async () => {
244
270
  // Arrange
245
271
  const tools: ChatCompletionTool[] = [
246
272
  { function: { name: 'tool1', description: 'desc1' }, type: 'function' },
@@ -257,7 +283,10 @@ describe('LobeAnthropicAI', () => {
257
283
 
258
284
  // Assert
259
285
  expect(instance['client'].messages.create).toHaveBeenCalled();
260
- expect(spyOn).toHaveBeenCalledWith(tools);
286
+ expect(spyOn).toHaveBeenCalledWith(
287
+ [{ function: { name: 'tool1', description: 'desc1' }, type: 'function' }],
288
+ { enabledContextCaching: true },
289
+ );
261
290
  });
262
291
  });
263
292
 
@@ -97,10 +97,33 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
97
97
  }
98
98
 
99
99
  private async buildAnthropicPayload(payload: ChatStreamPayload) {
100
- const { messages, model, max_tokens, temperature, top_p, tools, thinking } = payload;
100
+ const {
101
+ messages,
102
+ model,
103
+ max_tokens,
104
+ temperature,
105
+ top_p,
106
+ tools,
107
+ thinking,
108
+ enabledContextCaching = true,
109
+ } = payload;
101
110
  const system_message = messages.find((m) => m.role === 'system');
102
111
  const user_messages = messages.filter((m) => m.role !== 'system');
103
112
 
113
+ const systemPrompts = !!system_message?.content
114
+ ? ([
115
+ {
116
+ cache_control: enabledContextCaching ? { type: 'ephemeral' } : undefined,
117
+ text: system_message?.content as string,
118
+ type: 'text',
119
+ },
120
+ ] as Anthropic.TextBlockParam[])
121
+ : undefined;
122
+
123
+ const postMessages = await buildAnthropicMessages(user_messages, { enabledContextCaching });
124
+
125
+ const postTools = buildAnthropicTools(tools, { enabledContextCaching });
126
+
104
127
  if (!!thinking) {
105
128
  const maxTokens =
106
129
  max_tokens ?? (thinking?.budget_tokens ? thinking?.budget_tokens + 4096 : 4096);
@@ -109,22 +132,21 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
109
132
  // `top_p` must be unset when thinking is enabled.
110
133
  return {
111
134
  max_tokens: maxTokens,
112
- messages: await buildAnthropicMessages(user_messages),
135
+ messages: postMessages,
113
136
  model,
114
- system: system_message?.content as string,
115
-
137
+ system: systemPrompts,
116
138
  thinking,
117
- tools: buildAnthropicTools(tools),
139
+ tools: postTools,
118
140
  } satisfies Anthropic.MessageCreateParams;
119
141
  }
120
142
 
121
143
  return {
122
144
  max_tokens: max_tokens ?? 4096,
123
- messages: await buildAnthropicMessages(user_messages),
145
+ messages: postMessages,
124
146
  model,
125
- system: system_message?.content as string,
147
+ system: systemPrompts,
126
148
  temperature: payload.temperature !== undefined ? temperature / 2 : undefined,
127
- tools: buildAnthropicTools(tools),
149
+ tools: postTools,
128
150
  top_p,
129
151
  } satisfies Anthropic.MessageCreateParams;
130
152
  }
@@ -1,6 +1,8 @@
1
1
  import OpenAI, { AzureOpenAI } from 'openai';
2
2
  import type { Stream } from 'openai/streaming';
3
3
 
4
+ import { systemToUserModels } from '@/const/models';
5
+
4
6
  import { LobeRuntimeAI } from '../BaseAI';
5
7
  import { AgentRuntimeErrorType } from '../error';
6
8
  import { ChatCompetitionOptions, ChatStreamPayload, ModelProvider } from '../types';
@@ -13,7 +15,7 @@ import { OpenAIStream } from '../utils/streams';
13
15
  export class LobeAzureOpenAI implements LobeRuntimeAI {
14
16
  client: AzureOpenAI;
15
17
 
16
- constructor(params: { apiKey?: string; apiVersion?: string, baseURL?: string; } = {}) {
18
+ constructor(params: { apiKey?: string; apiVersion?: string; baseURL?: string } = {}) {
17
19
  if (!params.apiKey || !params.baseURL)
18
20
  throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidProviderAPIKey);
19
21
 
@@ -34,17 +36,10 @@ export class LobeAzureOpenAI implements LobeRuntimeAI {
34
36
  // o1 series models on Azure OpenAI does not support streaming currently
35
37
  const enableStreaming = model.includes('o1') ? false : (params.stream ?? true);
36
38
 
37
- // Convert 'system' role to 'user' or 'developer' based on the model
38
- const systemToUserModels = new Set([
39
- 'o1-preview',
40
- 'o1-preview-2024-09-12',
41
- 'o1-mini',
42
- 'o1-mini-2024-09-12',
43
- ]);
44
-
45
39
  const updatedMessages = messages.map((message) => ({
46
40
  ...message,
47
41
  role:
42
+ // Convert 'system' role to 'user' or 'developer' based on the model
48
43
  (model.includes('o1') || model.includes('o3')) && message.role === 'system'
49
44
  ? [...systemToUserModels].some((sub) => model.includes(sub))
50
45
  ? 'user'
@@ -2,6 +2,8 @@ import createClient, { ModelClient } from '@azure-rest/ai-inference';
2
2
  import { AzureKeyCredential } from '@azure/core-auth';
3
3
  import OpenAI from 'openai';
4
4
 
5
+ import { systemToUserModels } from '@/const/models';
6
+
5
7
  import { LobeRuntimeAI } from '../BaseAI';
6
8
  import { AgentRuntimeErrorType } from '../error';
7
9
  import { ChatCompetitionOptions, ChatStreamPayload, ModelProvider } from '../types';
@@ -30,24 +32,17 @@ export class LobeAzureAI implements LobeRuntimeAI {
30
32
  // o1 series models on Azure OpenAI does not support streaming currently
31
33
  const enableStreaming = model.includes('o1') ? false : (params.stream ?? true);
32
34
 
33
- // Convert 'system' role to 'user' or 'developer' based on the model
34
- const systemToUserModels = new Set([
35
- 'o1-preview',
36
- 'o1-preview-2024-09-12',
37
- 'o1-mini',
38
- 'o1-mini-2024-09-12',
39
- ]);
40
-
41
35
  const updatedMessages = messages.map((message) => ({
42
36
  ...message,
43
37
  role:
38
+ // Convert 'system' role to 'user' or 'developer' based on the model
44
39
  (model.includes('o1') || model.includes('o3')) && message.role === 'system'
45
40
  ? [...systemToUserModels].some((sub) => model.includes(sub))
46
41
  ? 'user'
47
42
  : 'developer'
48
43
  : message.role,
49
44
  }));
50
-
45
+
51
46
  try {
52
47
  const response = this.client.path('/chat/completions').post({
53
48
  body: {
@@ -1,25 +1,14 @@
1
+ import { disableStreamModels, systemToUserModels } from '@/const/models';
2
+ import type { ChatModelCard } from '@/types/llm';
3
+
1
4
  import { ChatStreamPayload, ModelProvider, OpenAIChatMessage } from '../types';
2
5
  import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory';
3
6
 
4
- import type { ChatModelCard } from '@/types/llm';
5
-
6
7
  export interface OpenAIModelCard {
7
8
  id: string;
8
9
  }
9
10
 
10
11
  export const pruneReasoningPayload = (payload: ChatStreamPayload) => {
11
- // TODO: 临时写法,后续要重构成 model card 展示配置
12
- const disableStreamModels = new Set([
13
- 'o1',
14
- 'o1-2024-12-17'
15
- ]);
16
- const systemToUserModels = new Set([
17
- 'o1-preview',
18
- 'o1-preview-2024-09-12',
19
- 'o1-mini',
20
- 'o1-mini-2024-09-12',
21
- ]);
22
-
23
12
  return {
24
13
  ...payload,
25
14
  frequency_penalty: 0,
@@ -58,46 +47,40 @@ export const LobeOpenAI = LobeOpenAICompatibleFactory({
58
47
  models: async ({ client }) => {
59
48
  const { LOBE_DEFAULT_MODEL_LIST } = await import('@/config/aiModels');
60
49
 
61
- const functionCallKeywords = [
62
- 'gpt-4',
63
- 'gpt-3.5',
64
- 'o3-mini',
65
- ];
50
+ const functionCallKeywords = ['gpt-4', 'gpt-3.5', 'o3-mini'];
66
51
 
67
- const visionKeywords = [
68
- 'gpt-4o',
69
- 'vision',
70
- ];
52
+ const visionKeywords = ['gpt-4o', 'vision'];
71
53
 
72
- const reasoningKeywords = [
73
- 'o1',
74
- 'o3',
75
- ];
54
+ const reasoningKeywords = ['o1', 'o3'];
76
55
 
77
- const modelsPage = await client.models.list() as any;
56
+ const modelsPage = (await client.models.list()) as any;
78
57
  const modelList: OpenAIModelCard[] = modelsPage.data;
79
58
 
80
59
  return modelList
81
60
  .map((model) => {
82
- const knownModel = LOBE_DEFAULT_MODEL_LIST.find((m) => model.id.toLowerCase() === m.id.toLowerCase());
61
+ const knownModel = LOBE_DEFAULT_MODEL_LIST.find(
62
+ (m) => model.id.toLowerCase() === m.id.toLowerCase(),
63
+ );
83
64
 
84
65
  return {
85
66
  contextWindowTokens: knownModel?.contextWindowTokens ?? undefined,
86
67
  displayName: knownModel?.displayName ?? undefined,
87
68
  enabled: knownModel?.enabled || false,
88
69
  functionCall:
89
- functionCallKeywords.some(keyword => model.id.toLowerCase().includes(keyword)) && !model.id.toLowerCase().includes('audio')
90
- || knownModel?.abilities?.functionCall
91
- || false,
70
+ (functionCallKeywords.some((keyword) => model.id.toLowerCase().includes(keyword)) &&
71
+ !model.id.toLowerCase().includes('audio')) ||
72
+ knownModel?.abilities?.functionCall ||
73
+ false,
92
74
  id: model.id,
93
75
  reasoning:
94
- reasoningKeywords.some(keyword => model.id.toLowerCase().includes(keyword))
95
- || knownModel?.abilities?.reasoning
96
- || false,
76
+ reasoningKeywords.some((keyword) => model.id.toLowerCase().includes(keyword)) ||
77
+ knownModel?.abilities?.reasoning ||
78
+ false,
97
79
  vision:
98
- visionKeywords.some(keyword => model.id.toLowerCase().includes(keyword)) && !model.id.toLowerCase().includes('audio')
99
- || knownModel?.abilities?.vision
100
- || false,
80
+ (visionKeywords.some((keyword) => model.id.toLowerCase().includes(keyword)) &&
81
+ !model.id.toLowerCase().includes('audio')) ||
82
+ knownModel?.abilities?.vision ||
83
+ false,
101
84
  };
102
85
  })
103
86
  .filter(Boolean) as ChatModelCard[];
@@ -46,6 +46,10 @@ export interface OpenAIChatMessage {
46
46
  * @title Chat Stream Payload
47
47
  */
48
48
  export interface ChatStreamPayload {
49
+ /**
50
+ * 开启上下文缓存
51
+ */
52
+ enabledContextCaching?: boolean;
49
53
  /**
50
54
  * 是否开启搜索
51
55
  */
@@ -619,6 +619,26 @@ describe('anthropicHelpers', () => {
619
619
  { content: '继续', role: 'user' },
620
620
  ]);
621
621
  });
622
+
623
+ it('should enable cache control', async () => {
624
+ const messages: OpenAIChatMessage[] = [
625
+ { content: 'Hello', role: 'user' },
626
+ { content: 'Hello', role: 'user' },
627
+ { content: 'Hi', role: 'assistant' },
628
+ ];
629
+
630
+ const contents = await buildAnthropicMessages(messages, { enabledContextCaching: true });
631
+
632
+ expect(contents).toHaveLength(3);
633
+ expect(contents).toEqual([
634
+ { content: 'Hello', role: 'user' },
635
+ { content: 'Hello', role: 'user' },
636
+ {
637
+ content: [{ cache_control: { type: 'ephemeral' }, text: 'Hi', type: 'text' }],
638
+ role: 'assistant',
639
+ },
640
+ ]);
641
+ });
622
642
  });
623
643
 
624
644
  describe('buildAnthropicTools', () => {
@@ -656,5 +676,40 @@ describe('anthropicHelpers', () => {
656
676
  },
657
677
  ]);
658
678
  });
679
+ it('should enable cache control', () => {
680
+ const tools: OpenAI.ChatCompletionTool[] = [
681
+ {
682
+ type: 'function',
683
+ function: {
684
+ name: 'search',
685
+ description: 'Searches the web',
686
+ parameters: {
687
+ type: 'object',
688
+ properties: {
689
+ query: { type: 'string' },
690
+ },
691
+ required: ['query'],
692
+ },
693
+ },
694
+ },
695
+ ];
696
+
697
+ const result = buildAnthropicTools(tools, { enabledContextCaching: true });
698
+
699
+ expect(result).toEqual([
700
+ {
701
+ name: 'search',
702
+ description: 'Searches the web',
703
+ input_schema: {
704
+ type: 'object',
705
+ properties: {
706
+ query: { type: 'string' },
707
+ },
708
+ required: ['query'],
709
+ },
710
+ cache_control: { type: 'ephemeral' },
711
+ },
712
+ ]);
713
+ });
659
714
  });
660
715
  });