@lobehub/chat 0.156.2 → 0.157.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 (114) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/package.json +3 -2
  3. package/src/config/modelProviders/anthropic.ts +3 -0
  4. package/src/config/modelProviders/google.ts +3 -0
  5. package/src/config/modelProviders/groq.ts +5 -1
  6. package/src/config/modelProviders/minimax.ts +10 -7
  7. package/src/config/modelProviders/mistral.ts +1 -0
  8. package/src/config/modelProviders/moonshot.ts +3 -0
  9. package/src/config/modelProviders/zhipu.ts +2 -6
  10. package/src/config/server/provider.ts +1 -1
  11. package/src/database/client/core/db.ts +32 -0
  12. package/src/database/client/core/schemas.ts +9 -0
  13. package/src/database/client/models/__tests__/message.test.ts +2 -2
  14. package/src/database/client/schemas/message.ts +8 -1
  15. package/src/features/AgentSetting/store/action.ts +15 -6
  16. package/src/features/Conversation/Actions/Tool.tsx +16 -0
  17. package/src/features/Conversation/Actions/index.ts +2 -2
  18. package/src/features/Conversation/Messages/Assistant/ToolCalls/index.tsx +78 -0
  19. package/src/features/Conversation/Messages/Assistant/ToolCalls/style.ts +25 -0
  20. package/src/features/Conversation/Messages/Assistant/index.tsx +47 -0
  21. package/src/features/Conversation/Messages/Default.tsx +4 -1
  22. package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/index.tsx +34 -35
  23. package/src/features/Conversation/Messages/Tool/index.tsx +44 -0
  24. package/src/features/Conversation/Messages/index.ts +3 -2
  25. package/src/features/Conversation/Plugins/Render/StandaloneType/Iframe.tsx +1 -1
  26. package/src/features/Conversation/components/SkeletonList.tsx +2 -2
  27. package/src/features/Conversation/index.tsx +2 -3
  28. package/src/libs/agent-runtime/BaseAI.ts +2 -9
  29. package/src/libs/agent-runtime/anthropic/index.test.ts +195 -0
  30. package/src/libs/agent-runtime/anthropic/index.ts +71 -15
  31. package/src/libs/agent-runtime/azureOpenai/index.ts +6 -5
  32. package/src/libs/agent-runtime/bedrock/index.ts +24 -18
  33. package/src/libs/agent-runtime/google/index.test.ts +154 -0
  34. package/src/libs/agent-runtime/google/index.ts +91 -10
  35. package/src/libs/agent-runtime/groq/index.test.ts +41 -72
  36. package/src/libs/agent-runtime/groq/index.ts +7 -0
  37. package/src/libs/agent-runtime/minimax/index.test.ts +2 -2
  38. package/src/libs/agent-runtime/minimax/index.ts +14 -37
  39. package/src/libs/agent-runtime/mistral/index.test.ts +0 -53
  40. package/src/libs/agent-runtime/mistral/index.ts +1 -0
  41. package/src/libs/agent-runtime/moonshot/index.test.ts +1 -71
  42. package/src/libs/agent-runtime/ollama/index.test.ts +197 -0
  43. package/src/libs/agent-runtime/ollama/index.ts +3 -3
  44. package/src/libs/agent-runtime/openai/index.test.ts +0 -53
  45. package/src/libs/agent-runtime/openrouter/index.test.ts +1 -53
  46. package/src/libs/agent-runtime/perplexity/index.test.ts +0 -71
  47. package/src/libs/agent-runtime/perplexity/index.ts +2 -3
  48. package/src/libs/agent-runtime/togetherai/__snapshots__/index.test.ts.snap +886 -0
  49. package/src/libs/agent-runtime/togetherai/fixtures/models.json +8111 -0
  50. package/src/libs/agent-runtime/togetherai/index.test.ts +16 -54
  51. package/src/libs/agent-runtime/types/chat.ts +19 -3
  52. package/src/libs/agent-runtime/utils/anthropicHelpers.test.ts +120 -1
  53. package/src/libs/agent-runtime/utils/anthropicHelpers.ts +67 -4
  54. package/src/libs/agent-runtime/utils/debugStream.test.ts +70 -0
  55. package/src/libs/agent-runtime/utils/debugStream.ts +39 -9
  56. package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.test.ts +521 -0
  57. package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +76 -5
  58. package/src/libs/agent-runtime/utils/response.ts +12 -0
  59. package/src/libs/agent-runtime/utils/streams/anthropic.test.ts +197 -0
  60. package/src/libs/agent-runtime/utils/streams/anthropic.ts +91 -0
  61. package/src/libs/agent-runtime/utils/streams/bedrock/claude.ts +21 -0
  62. package/src/libs/agent-runtime/utils/streams/bedrock/common.ts +32 -0
  63. package/src/libs/agent-runtime/utils/streams/bedrock/index.ts +3 -0
  64. package/src/libs/agent-runtime/utils/streams/bedrock/llama.test.ts +196 -0
  65. package/src/libs/agent-runtime/utils/streams/bedrock/llama.ts +51 -0
  66. package/src/libs/agent-runtime/utils/streams/google-ai.test.ts +97 -0
  67. package/src/libs/agent-runtime/utils/streams/google-ai.ts +68 -0
  68. package/src/libs/agent-runtime/utils/streams/index.ts +7 -0
  69. package/src/libs/agent-runtime/utils/streams/minimax.ts +39 -0
  70. package/src/libs/agent-runtime/utils/streams/ollama.test.ts +77 -0
  71. package/src/libs/agent-runtime/utils/streams/ollama.ts +38 -0
  72. package/src/libs/agent-runtime/utils/streams/openai.test.ts +263 -0
  73. package/src/libs/agent-runtime/utils/streams/openai.ts +79 -0
  74. package/src/libs/agent-runtime/utils/streams/protocol.ts +100 -0
  75. package/src/libs/agent-runtime/zeroone/index.test.ts +1 -53
  76. package/src/libs/agent-runtime/zhipu/index.test.ts +1 -1
  77. package/src/libs/agent-runtime/zhipu/index.ts +3 -2
  78. package/src/locales/default/plugin.ts +3 -4
  79. package/src/migrations/FromV4ToV5/fixtures/from-v1-to-v5-output.json +245 -0
  80. package/src/migrations/FromV4ToV5/fixtures/function-input-v4.json +96 -0
  81. package/src/migrations/FromV4ToV5/fixtures/function-output-v5.json +120 -0
  82. package/src/migrations/FromV4ToV5/index.ts +58 -0
  83. package/src/migrations/FromV4ToV5/migrations.test.ts +49 -0
  84. package/src/migrations/FromV4ToV5/types/v4.ts +21 -0
  85. package/src/migrations/FromV4ToV5/types/v5.ts +27 -0
  86. package/src/migrations/index.ts +8 -1
  87. package/src/services/__tests__/chat.test.ts +10 -20
  88. package/src/services/chat.ts +78 -65
  89. package/src/store/chat/slices/enchance/action.ts +15 -10
  90. package/src/store/chat/slices/message/action.test.ts +36 -86
  91. package/src/store/chat/slices/message/action.ts +70 -79
  92. package/src/store/chat/slices/message/reducer.ts +18 -1
  93. package/src/store/chat/slices/message/selectors.test.ts +38 -68
  94. package/src/store/chat/slices/message/selectors.ts +1 -22
  95. package/src/store/chat/slices/plugin/action.test.ts +147 -203
  96. package/src/store/chat/slices/plugin/action.ts +96 -82
  97. package/src/store/chat/slices/share/action.test.ts +3 -3
  98. package/src/store/chat/slices/share/action.ts +1 -1
  99. package/src/store/chat/slices/topic/action.ts +7 -2
  100. package/src/store/tool/selectors/tool.ts +6 -24
  101. package/src/store/tool/slices/builtin/action.test.ts +90 -0
  102. package/src/types/llm.ts +1 -1
  103. package/src/types/message/index.ts +9 -4
  104. package/src/types/message/tools.ts +57 -0
  105. package/src/types/openai/chat.ts +6 -0
  106. package/src/utils/fetch.test.ts +245 -1
  107. package/src/utils/fetch.ts +120 -44
  108. package/src/utils/toolCall.ts +21 -0
  109. package/src/features/Conversation/Messages/Assistant.tsx +0 -26
  110. package/src/features/Conversation/Messages/Function.tsx +0 -35
  111. package/src/libs/agent-runtime/ollama/stream.ts +0 -31
  112. /package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/PluginResultJSON.tsx +0 -0
  113. /package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/Settings.tsx +0 -0
  114. /package/src/features/Conversation/{Plugins → Messages/Tool}/Inspector/style.ts +0 -0
@@ -1,13 +1,12 @@
1
1
  import { Loading3QuartersOutlined } from '@ant-design/icons';
2
- import { LobePluginType } from '@lobehub/chat-plugin-sdk';
3
- import { ActionIcon, Avatar, Highlighter, Icon } from '@lobehub/ui';
2
+ import { ActionIcon, Avatar, Highlighter, Icon, Tag } from '@lobehub/ui';
4
3
  import { Tabs } from 'antd';
5
4
  import isEqual from 'fast-deep-equal';
6
5
  import {
7
6
  LucideBug,
8
7
  LucideBugOff,
9
8
  LucideChevronDown,
10
- LucideChevronUp,
9
+ LucideChevronRight,
11
10
  LucideToyBrick,
12
11
  } from 'lucide-react';
13
12
  import { memo, useState } from 'react';
@@ -16,6 +15,7 @@ import { Flexbox } from 'react-layout-kit';
16
15
 
17
16
  import { pluginHelpers, useToolStore } from '@/store/tool';
18
17
  import { pluginSelectors, toolSelectors } from '@/store/tool/selectors';
18
+ import { ChatPluginPayload } from '@/types/message';
19
19
 
20
20
  import PluginResult from './PluginResultJSON';
21
21
  import Settings from './Settings';
@@ -23,36 +23,34 @@ import { useStyles } from './style';
23
23
 
24
24
  export interface InspectorProps {
25
25
  arguments?: string;
26
- command?: any;
27
26
  content: string;
28
- id?: string;
27
+ identifier?: string;
29
28
  loading?: boolean;
29
+ payload?: ChatPluginPayload;
30
30
  setShow?: (showRender: boolean) => void;
31
31
  showRender?: boolean;
32
- type?: LobePluginType;
33
32
  }
34
33
 
35
34
  const Inspector = memo<InspectorProps>(
36
35
  ({
37
36
  arguments: requestArgs = '{}',
38
- command,
37
+ payload,
39
38
  showRender,
40
39
  loading,
41
40
  setShow,
42
41
  content,
43
- id = 'unknown',
44
- // type,
42
+ identifier = 'unknown',
45
43
  }) => {
46
44
  const { t } = useTranslation('plugin');
47
45
  const { styles } = useStyles();
48
46
  const [open, setOpen] = useState(false);
49
47
 
50
- const pluginMeta = useToolStore(toolSelectors.getMetaById(id), isEqual);
48
+ const pluginMeta = useToolStore(toolSelectors.getMetaById(identifier), isEqual);
51
49
 
52
- const showRightAction = useToolStore(pluginSelectors.isPluginHasUI(id));
50
+ const showRightAction = useToolStore(pluginSelectors.isPluginHasUI(identifier));
53
51
  const pluginAvatar = pluginHelpers.getPluginAvatar(pluginMeta);
54
52
 
55
- const pluginTitle = pluginHelpers.getPluginTitle(pluginMeta) ?? t('plugins.loading');
53
+ const pluginTitle = pluginHelpers.getPluginTitle(pluginMeta) ?? t('unknownPlugin');
56
54
 
57
55
  const avatar = pluginAvatar ? (
58
56
  <Avatar avatar={pluginAvatar} size={32} />
@@ -62,7 +60,7 @@ const Inspector = memo<InspectorProps>(
62
60
 
63
61
  let args, params;
64
62
  try {
65
- args = JSON.stringify(command, null, 2);
63
+ args = JSON.stringify(payload, null, 2);
66
64
  params = JSON.stringify(JSON.parse(requestArgs), null, 2);
67
65
  } catch {
68
66
  args = '';
@@ -81,29 +79,30 @@ const Inspector = memo<InspectorProps>(
81
79
  setShow?.(!showRender);
82
80
  }}
83
81
  >
84
- {loading ? (
85
- <div>
86
- <Loading3QuartersOutlined spin />
87
- </div>
88
- ) : (
89
- avatar
90
- )}
91
- {pluginTitle}
92
- {showRightAction && <Icon icon={showRender ? LucideChevronUp : LucideChevronDown} />}
93
- </Flexbox>
94
- {
95
- <Flexbox horizontal>
96
- {/*{type === 'standalone' && <ActionIcon icon={LucideOrbit} />}*/}
97
- <ActionIcon
98
- icon={open ? LucideBugOff : LucideBug}
99
- onClick={() => {
100
- setOpen(!open);
101
- }}
102
- title={t(open ? 'debug.off' : 'debug.on')}
103
- />
104
- <Settings id={id} />
82
+ <Flexbox align={'center'} gap={8} horizontal>
83
+ {loading ? (
84
+ <div>
85
+ <Loading3QuartersOutlined spin />
86
+ </div>
87
+ ) : (
88
+ avatar
89
+ )}
90
+ <div>{pluginTitle}</div>
91
+ <Tag>{payload?.apiName}</Tag>
105
92
  </Flexbox>
106
- }
93
+ {showRightAction && <Icon icon={showRender ? LucideChevronDown : LucideChevronRight} />}
94
+ </Flexbox>
95
+
96
+ <Flexbox horizontal>
97
+ <ActionIcon
98
+ icon={open ? LucideBugOff : LucideBug}
99
+ onClick={() => {
100
+ setOpen(!open);
101
+ }}
102
+ title={t(open ? 'debug.off' : 'debug.on')}
103
+ />
104
+ <Settings id={identifier} />
105
+ </Flexbox>
107
106
  </Flexbox>
108
107
  {open && (
109
108
  <Tabs
@@ -0,0 +1,44 @@
1
+ import { Snippet } from '@lobehub/ui';
2
+ import { memo, useState } from 'react';
3
+ import { Flexbox } from 'react-layout-kit';
4
+
5
+ import { useChatStore } from '@/store/chat';
6
+ import { chatSelectors } from '@/store/chat/selectors';
7
+ import { ChatMessage } from '@/types/message';
8
+
9
+ import PluginRender from '../../Plugins/Render';
10
+ import Inspector from './Inspector';
11
+
12
+ export const ToolMessage = memo<ChatMessage>(({ id, content, plugin }) => {
13
+ const loading = useChatStore(chatSelectors.isMessageGenerating(id));
14
+
15
+ const [showRender, setShow] = useState(plugin?.type !== 'default');
16
+
17
+ return (
18
+ <Flexbox gap={12} id={id} width={'100%'}>
19
+ <Inspector
20
+ arguments={plugin?.arguments}
21
+ content={content}
22
+ identifier={plugin?.identifier}
23
+ loading={loading}
24
+ payload={plugin}
25
+ setShow={setShow}
26
+ showRender={showRender}
27
+ />
28
+ {showRender || loading ? (
29
+ <PluginRender
30
+ content={content}
31
+ id={id}
32
+ identifier={plugin?.identifier}
33
+ loading={loading}
34
+ payload={plugin}
35
+ type={plugin?.type}
36
+ />
37
+ ) : (
38
+ <Flexbox>
39
+ <Snippet>{plugin?.arguments || ''}</Snippet>
40
+ </Flexbox>
41
+ )}
42
+ </Flexbox>
43
+ );
44
+ });
@@ -6,13 +6,14 @@ import { sessionSelectors } from '@/store/session/selectors';
6
6
  import { OnAvatarsClick, RenderMessage } from '../types';
7
7
  import { AssistantMessage } from './Assistant';
8
8
  import { DefaultMessage } from './Default';
9
- import { FunctionMessage } from './Function';
9
+ import { ToolMessage } from './Tool';
10
10
  import { UserMessage } from './User';
11
11
 
12
12
  export const renderMessages: Record<string, RenderMessage> = {
13
13
  assistant: AssistantMessage,
14
14
  default: DefaultMessage,
15
- function: FunctionMessage,
15
+ function: DefaultMessage,
16
+ tool: ToolMessage,
16
17
  user: UserMessage,
17
18
  };
18
19
 
@@ -126,7 +126,7 @@ const IFrameRender = memo<IFrameRenderProps>(({ url, id, payload, width = 600, h
126
126
  // we need to know which message to trigger
127
127
  if (messageId !== id) return;
128
128
 
129
- triggerAIMessage(id);
129
+ triggerAIMessage({ parentId: id });
130
130
  });
131
131
 
132
132
  // when plugin want to create an assistant message
@@ -34,14 +34,14 @@ const SkeletonList = memo<SkeletonListProps>(({ mobile }) => {
34
34
  <Skeleton
35
35
  active
36
36
  avatar={{ size: mobile ? 32 : 40 }}
37
- className={styles.message}
37
+ className={cx(styles.message, styles.user)}
38
38
  paragraph={{ width: mobile ? ['80%', '40%'] : ['50%', '30%'] }}
39
39
  title={false}
40
40
  />
41
41
  <Skeleton
42
42
  active
43
43
  avatar={{ size: mobile ? 32 : 40 }}
44
- className={cx(styles.message, styles.user)}
44
+ className={styles.message}
45
45
  paragraph={{ width: mobile ? ['80%', '40%'] : ['50%', '30%'] }}
46
46
  title={false}
47
47
  />
@@ -1,9 +1,8 @@
1
- import { Suspense, lazy } from 'react';
1
+ import { Suspense } from 'react';
2
2
  import { Flexbox } from 'react-layout-kit';
3
3
 
4
4
  import SkeletonList from './components/SkeletonList';
5
-
6
- const ChatList = lazy(() => import('./components/VirtualizedList'));
5
+ import ChatList from './components/VirtualizedList';
7
6
 
8
7
  interface ConversationProps {
9
8
  mobile?: boolean;
@@ -1,4 +1,3 @@
1
- import { StreamingTextResponse } from 'ai';
2
1
  import OpenAI from 'openai';
3
2
 
4
3
  import { ChatModelCard } from '@/types/llm';
@@ -7,10 +6,7 @@ import { ChatCompetitionOptions, ChatStreamPayload } from './types';
7
6
 
8
7
  export interface LobeRuntimeAI {
9
8
  baseURL?: string;
10
- chat(
11
- payload: ChatStreamPayload,
12
- options?: ChatCompetitionOptions,
13
- ): Promise<StreamingTextResponse>;
9
+ chat(payload: ChatStreamPayload, options?: ChatCompetitionOptions): Promise<Response>;
14
10
 
15
11
  models?(): Promise<any>;
16
12
  }
@@ -19,10 +15,7 @@ export abstract class LobeOpenAICompatibleRuntime {
19
15
  abstract baseURL: string;
20
16
  abstract client: OpenAI;
21
17
 
22
- abstract chat(
23
- payload: ChatStreamPayload,
24
- options?: ChatCompetitionOptions,
25
- ): Promise<StreamingTextResponse>;
18
+ abstract chat(payload: ChatStreamPayload, options?: ChatCompetitionOptions): Promise<Response>;
26
19
 
27
20
  abstract models(): Promise<ChatModelCard[]>;
28
21
  }
@@ -1,6 +1,9 @@
1
1
  // @vitest-environment node
2
2
  import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
3
 
4
+ import { ChatCompletionTool } from '@/libs/agent-runtime';
5
+
6
+ import * as anthropicHelpers from '../utils/anthropicHelpers';
4
7
  import * as debugStreamModule from '../utils/debugStream';
5
8
  import { LobeAnthropicAI } from './index';
6
9
 
@@ -16,6 +19,10 @@ beforeEach(() => {
16
19
 
17
20
  // 使用 vi.spyOn 来模拟 chat.completions.create 方法
18
21
  vi.spyOn(instance['client'].messages, 'create').mockReturnValue(new ReadableStream() as any);
22
+
23
+ vi.spyOn(instance['client'].beta.tools.messages, 'create').mockReturnValue({
24
+ content: [],
25
+ } as any);
19
26
  });
20
27
 
21
28
  afterEach(() => {
@@ -233,6 +240,54 @@ describe('LobeAnthropicAI', () => {
233
240
  process.env.DEBUG_ANTHROPIC_CHAT_COMPLETION = originalDebugValue;
234
241
  });
235
242
 
243
+ describe('chat with tools', () => {
244
+ it('should call client.beta.tools.messages.create when tools are provided', async () => {
245
+ // Arrange
246
+ const tools: ChatCompletionTool[] = [
247
+ { function: { name: 'tool1', description: 'desc1' }, type: 'function' },
248
+ ];
249
+ const spyOn = vi.spyOn(anthropicHelpers, 'buildAnthropicTools');
250
+
251
+ // Act
252
+ await instance.chat({
253
+ messages: [{ content: 'Hello', role: 'user' }],
254
+ model: 'claude-3-haiku-20240307',
255
+ temperature: 1,
256
+ tools,
257
+ });
258
+
259
+ // Assert
260
+ expect(instance['client'].beta.tools.messages.create).toHaveBeenCalled();
261
+ expect(spyOn).toHaveBeenCalledWith(tools);
262
+ });
263
+
264
+ it('should handle text and tool_use content correctly in transformResponseToStream', async () => {
265
+ // Arrange
266
+ const mockResponse = {
267
+ content: [
268
+ { type: 'text', text: 'Hello' },
269
+ { type: 'tool_use', id: 'tool1', name: 'tool1', input: 'input1' },
270
+ ],
271
+ };
272
+ // @ts-ignore
273
+ vi.spyOn(instance, 'transformResponseToStream').mockReturnValue(new ReadableStream());
274
+ vi.spyOn(instance['client'].beta.tools.messages, 'create').mockResolvedValue(
275
+ mockResponse as any,
276
+ );
277
+
278
+ // Act
279
+ await instance.chat({
280
+ messages: [{ content: 'Hello', role: 'user' }],
281
+ model: 'claude-3-haiku-20240307',
282
+ temperature: 0,
283
+ tools: [{ function: { name: 'tool1', description: 'desc1' }, type: 'function' }],
284
+ });
285
+
286
+ // Assert
287
+ expect(instance['transformResponseToStream']).toHaveBeenCalledWith(mockResponse);
288
+ });
289
+ });
290
+
236
291
  describe('Error', () => {
237
292
  it('should throw InvalidAnthropicAPIKey error on API_KEY_INVALID error', async () => {
238
293
  // Arrange
@@ -305,5 +360,145 @@ describe('LobeAnthropicAI', () => {
305
360
  }
306
361
  });
307
362
  });
363
+
364
+ describe('Error handling', () => {
365
+ it('should throw LocationNotSupportError on 403 error', async () => {
366
+ // Arrange
367
+ const apiError = { status: 403 };
368
+ (instance['client'].messages.create as Mock).mockRejectedValue(apiError);
369
+
370
+ // Act & Assert
371
+ await expect(
372
+ instance.chat({
373
+ messages: [{ content: 'Hello', role: 'user' }],
374
+ model: 'claude-3-haiku-20240307',
375
+ temperature: 1,
376
+ }),
377
+ ).rejects.toEqual({
378
+ endpoint: 'https://api.anthropic.com',
379
+ error: apiError,
380
+ errorType: 'LocationNotSupportError',
381
+ provider,
382
+ });
383
+ });
384
+
385
+ it('should throw AnthropicBizError on other error status codes', async () => {
386
+ // Arrange
387
+ const apiError = { status: 500 };
388
+ (instance['client'].messages.create as Mock).mockRejectedValue(apiError);
389
+
390
+ // Act & Assert
391
+ await expect(
392
+ instance.chat({
393
+ messages: [{ content: 'Hello', role: 'user' }],
394
+ model: 'claude-3-haiku-20240307',
395
+ temperature: 1,
396
+ }),
397
+ ).rejects.toEqual({
398
+ endpoint: 'https://api.anthropic.com',
399
+ error: apiError,
400
+ errorType: 'AnthropicBizError',
401
+ provider,
402
+ });
403
+ });
404
+
405
+ it('should desensitize custom baseURL in error message', async () => {
406
+ // Arrange
407
+ const apiError = { status: 401 };
408
+ const customInstance = new LobeAnthropicAI({
409
+ apiKey: 'test',
410
+ baseURL: 'https://api.custom.com/v1',
411
+ });
412
+ vi.spyOn(customInstance['client'].messages, 'create').mockRejectedValue(apiError);
413
+
414
+ // Act & Assert
415
+ await expect(
416
+ customInstance.chat({
417
+ messages: [{ content: 'Hello', role: 'user' }],
418
+ model: 'claude-3-haiku-20240307',
419
+ temperature: 0,
420
+ }),
421
+ ).rejects.toEqual({
422
+ endpoint: 'https://api.cu****om.com/v1',
423
+ error: apiError,
424
+ errorType: 'InvalidAnthropicAPIKey',
425
+ provider,
426
+ });
427
+ });
428
+ });
429
+
430
+ describe('Options', () => {
431
+ it('should pass signal to API call', async () => {
432
+ // Arrange
433
+ const controller = new AbortController();
434
+
435
+ // Act
436
+ await instance.chat(
437
+ {
438
+ messages: [{ content: 'Hello', role: 'user' }],
439
+ model: 'claude-3-haiku-20240307',
440
+ temperature: 1,
441
+ },
442
+ { signal: controller.signal },
443
+ );
444
+
445
+ // Assert
446
+ expect(instance['client'].messages.create).toHaveBeenCalledWith(
447
+ expect.objectContaining({}),
448
+ { signal: controller.signal },
449
+ );
450
+ });
451
+
452
+ it('should apply callback to the returned stream', async () => {
453
+ // Arrange
454
+ const callback = vi.fn();
455
+
456
+ // Act
457
+ await instance.chat(
458
+ {
459
+ messages: [{ content: 'Hello', role: 'user' }],
460
+ model: 'claude-3-haiku-20240307',
461
+ temperature: 0,
462
+ },
463
+ {
464
+ callback: { onStart: callback },
465
+ },
466
+ );
467
+
468
+ // Assert
469
+ expect(callback).toHaveBeenCalled();
470
+ });
471
+
472
+ it('should set headers on the response', async () => {
473
+ // Arrange
474
+ const headers = { 'X-Test-Header': 'test' };
475
+
476
+ // Act
477
+ const result = await instance.chat(
478
+ {
479
+ messages: [{ content: 'Hello', role: 'user' }],
480
+ model: 'claude-3-haiku-20240307',
481
+ temperature: 1,
482
+ },
483
+ { headers },
484
+ );
485
+
486
+ // Assert
487
+ expect(result.headers.get('X-Test-Header')).toBe('test');
488
+ });
489
+ });
490
+
491
+ describe('Edge cases', () => {
492
+ it('should handle empty messages array', async () => {
493
+ // Act & Assert
494
+ await expect(
495
+ instance.chat({
496
+ messages: [],
497
+ model: 'claude-3-haiku-20240307',
498
+ temperature: 1,
499
+ }),
500
+ ).resolves.toBeInstanceOf(Response);
501
+ });
502
+ });
308
503
  });
309
504
  });
@@ -1,7 +1,6 @@
1
1
  // sort-imports-ignore
2
2
  import '@anthropic-ai/sdk/shims/web';
3
3
  import Anthropic from '@anthropic-ai/sdk';
4
- import { AnthropicStream, StreamingTextResponse } from 'ai';
5
4
  import { ClientOptions } from 'openai';
6
5
 
7
6
  import { LobeRuntimeAI } from '../BaseAI';
@@ -10,7 +9,9 @@ import { ChatCompetitionOptions, ChatStreamPayload, ModelProvider } from '../typ
10
9
  import { AgentRuntimeError } from '../utils/createError';
11
10
  import { debugStream } from '../utils/debugStream';
12
11
  import { desensitizeUrl } from '../utils/desensitizeUrl';
13
- import { buildAnthropicMessages } from '../utils/anthropicHelpers';
12
+ import { buildAnthropicMessages, buildAnthropicTools } from '../utils/anthropicHelpers';
13
+ import { StreamingResponse } from '../utils/response';
14
+ import { AnthropicStream } from '../utils/streams';
14
15
 
15
16
  const DEFAULT_BASE_URL = 'https://api.anthropic.com';
16
17
 
@@ -30,18 +31,40 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
30
31
  try {
31
32
  const anthropicPayload = this.buildAnthropicPayload(payload);
32
33
 
33
- const response = await this.client.messages.create(
34
- { ...anthropicPayload, stream: true },
34
+ // if there is no tool, we can use the normal chat API
35
+ if (!anthropicPayload.tools || anthropicPayload.tools.length === 0) {
36
+ const response = await this.client.messages.create(
37
+ { ...anthropicPayload, stream: true },
38
+ {
39
+ signal: options?.signal,
40
+ },
41
+ );
42
+
43
+ const [prod, debug] = response.tee();
44
+
45
+ if (process.env.DEBUG_ANTHROPIC_CHAT_COMPLETION === '1') {
46
+ debugStream(debug.toReadableStream()).catch(console.error);
47
+ }
48
+
49
+ return StreamingResponse(AnthropicStream(prod, options?.callback), {
50
+ headers: options?.headers,
51
+ });
52
+ }
53
+
54
+ // or we should call the tool API
55
+ const response = await this.client.beta.tools.messages.create(
56
+ { ...anthropicPayload, stream: false },
35
57
  { signal: options?.signal },
36
58
  );
37
59
 
38
- const [prod, debug] = response.tee();
39
-
40
60
  if (process.env.DEBUG_ANTHROPIC_CHAT_COMPLETION === '1') {
41
- debugStream(debug.toReadableStream()).catch(console.error);
61
+ console.log('\n[no stream response]\n');
62
+ console.log(JSON.stringify(response) + '\n');
42
63
  }
43
64
 
44
- return new StreamingTextResponse(AnthropicStream(prod, options?.callback), {
65
+ const stream = this.transformResponseToStream(response);
66
+
67
+ return StreamingResponse(AnthropicStream(stream, options?.callback), {
45
68
  headers: options?.headers,
46
69
  });
47
70
  } catch (error) {
@@ -85,20 +108,53 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
85
108
  }
86
109
 
87
110
  private buildAnthropicPayload(payload: ChatStreamPayload) {
88
- const { messages, model, max_tokens, temperature, top_p } = payload;
111
+ const { messages, model, max_tokens = 4096, temperature, top_p, tools } = payload;
89
112
  const system_message = messages.find((m) => m.role === 'system');
90
113
  const user_messages = messages.filter((m) => m.role !== 'system');
91
114
 
92
115
  return {
93
- max_tokens: max_tokens || 4096,
116
+ max_tokens,
94
117
  messages: buildAnthropicMessages(user_messages),
95
- model: model,
96
- stream: true,
118
+ model,
97
119
  system: system_message?.content as string,
98
- temperature: temperature,
99
- top_p: top_p,
100
- };
120
+ temperature,
121
+ // TODO: Anthropic sdk don't have tools interface currently
122
+ // @ts-ignore
123
+ tools: buildAnthropicTools(tools),
124
+ top_p,
125
+ } satisfies Anthropic.MessageCreateParams;
101
126
  }
127
+
128
+ private transformResponseToStream = (response: Anthropic.Beta.Tools.ToolsBetaMessage) => {
129
+ return new ReadableStream<Anthropic.MessageStreamEvent>({
130
+ start(controller) {
131
+ response.content.forEach((content) => {
132
+ switch (content.type) {
133
+ case 'text': {
134
+ controller.enqueue({
135
+ delta: { text: content.text, type: 'text_delta' },
136
+ type: 'content_block_delta',
137
+ } as Anthropic.ContentBlockDeltaEvent);
138
+ break;
139
+ }
140
+ case 'tool_use': {
141
+ controller.enqueue({
142
+ delta: {
143
+ tool_use: { id: content.id, input: content.input, name: content.name },
144
+ type: 'tool_use',
145
+ },
146
+ type: 'content_block_delta',
147
+ } as any);
148
+ }
149
+ }
150
+ });
151
+
152
+ controller.enqueue({ type: 'message_stop' } as Anthropic.MessageStopEvent);
153
+
154
+ controller.close();
155
+ },
156
+ });
157
+ };
102
158
  }
103
159
 
104
160
  export default LobeAnthropicAI;
@@ -4,13 +4,14 @@ import {
4
4
  GetChatCompletionsOptions,
5
5
  OpenAIClient,
6
6
  } from '@azure/openai';
7
- import { OpenAIStream, StreamingTextResponse } from 'ai';
8
7
 
9
8
  import { LobeRuntimeAI } from '../BaseAI';
10
9
  import { AgentRuntimeErrorType } from '../error';
11
10
  import { ChatCompetitionOptions, ChatStreamPayload, ModelProvider } from '../types';
12
11
  import { AgentRuntimeError } from '../utils/createError';
13
12
  import { debugStream } from '../utils/debugStream';
13
+ import { StreamingResponse } from '../utils/response';
14
+ import { OpenAIStream } from '../utils/streams';
14
15
 
15
16
  export class LobeAzureOpenAI implements LobeRuntimeAI {
16
17
  client: OpenAIClient;
@@ -40,15 +41,15 @@ export class LobeAzureOpenAI implements LobeRuntimeAI {
40
41
  { ...params, abortSignal: options?.signal, maxTokens } as GetChatCompletionsOptions,
41
42
  );
42
43
 
43
- const stream = OpenAIStream(response as any);
44
-
45
- const [debug, prod] = stream.tee();
44
+ const [debug, prod] = response.tee();
46
45
 
47
46
  if (process.env.DEBUG_AZURE_CHAT_COMPLETION === '1') {
48
47
  debugStream(debug).catch(console.error);
49
48
  }
50
49
 
51
- return new StreamingTextResponse(prod);
50
+ return StreamingResponse(OpenAIStream(prod, options?.callback), {
51
+ headers: options?.headers,
52
+ });
52
53
  } catch (e) {
53
54
  let error = e as { [key: string]: any; code: string; message: string };
54
55