@lobehub/chat 1.3.5 → 1.4.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 1.4.0](https://github.com/lobehub/lobe-chat/compare/v1.3.6...v1.4.0)
6
+
7
+ <sup>Released on **2024-07-12**</sup>
8
+
9
+ #### ✨ Features
10
+
11
+ - **misc**: Add 360AI model provider.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### What's improved
19
+
20
+ - **misc**: Add 360AI model provider, closes [#3130](https://github.com/lobehub/lobe-chat/issues/3130) ([79c5f86](https://github.com/lobehub/lobe-chat/commit/79c5f86))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ### [Version 1.3.6](https://github.com/lobehub/lobe-chat/compare/v1.3.5...v1.3.6)
31
+
32
+ <sup>Released on **2024-07-11**</sup>
33
+
34
+ #### ♻ Code Refactoring
35
+
36
+ - **misc**: Improve agent runtime code.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### Code refactoring
44
+
45
+ - **misc**: Improve agent runtime code, closes [#3199](https://github.com/lobehub/lobe-chat/issues/3199) ([9f211e2](https://github.com/lobehub/lobe-chat/commit/9f211e2))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 1.3.5](https://github.com/lobehub/lobe-chat/compare/v1.3.4...v1.3.5)
6
56
 
7
57
  <sup>Released on **2024-07-10**</sup>
package/README.md CHANGED
@@ -267,12 +267,12 @@ Our marketplace is not just a showcase platform but also a collaborative space.
267
267
 
268
268
  | Recent Submits | Description |
269
269
  | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
270
+ | [Ducky Programming Assistant](https://chat-preview.lobehub.com/market?agent=rubber-duck-programming)<br/><sup>By **[JiyuShao](https://github.com/JiyuShao)** on **2024-07-10**</sup> | Ducky Programming Assistant<br/>`programming` |
270
271
  | [AOSP Source Code Expert](https://chat-preview.lobehub.com/market?agent=aosp-development)<br/><sup>By **[viruscoding](https://github.com/viruscoding)** on **2024-06-24**</sup> | An expert in AOSP (Android Open Source Project) for Android, with a deep understanding and analytical ability of the latest AOSP source code.<br/>`aosp` |
271
272
  | [Fastapi Project Development Assistant](https://chat-preview.lobehub.com/market?agent=fastapi-development)<br/><sup>By **[xwxw098](https://github.com/xwxw098)** on **2024-06-19**</sup> | Proficient in Python modular development, skilled in using FastAPI, PostgreSQL, Tortoise-ORM, and other technologies, able to provide clear code structure and detailed comments for large projects.<br/>`fast-api` `python` `modular-development` |
272
273
  | [IT Systems Architect](https://chat-preview.lobehub.com/market?agent=it-system-architect)<br/><sup>By **[a562314](https://github.com/a562314)** on **2024-06-19**</sup> | Senior IT architect specializing in requirements analysis, system design, technology selection, and cross-platform system optimization. With over 5 years of experience, proficient in Windows, macOS, and Linux operating systems, skilled in troubleshooting, and security protection.<br/>`it-architecture-design` `problem-solving` `agile-development` `system-optimization` `cross-platform-skills` `teamwork` |
273
- | [Linux Kernel Expert](https://chat-preview.lobehub.com/market?agent=linux-kernel)<br/><sup>By **[wming126](https://github.com/wming126)** on **2024-06-19**</sup> | Role Description: I am an expert in Linux kernel, with a deep understanding and analytical ability of the latest kernel source code (as of June 2024). I can provide users with detailed and accurate information about the Linux kernel.<br/>`linux` `kernel` |
274
274
 
275
- > 📊 Total agents: [<kbd>**293**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
275
+ > 📊 Total agents: [<kbd>**294**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
276
276
 
277
277
  <!-- AGENT LIST -->
278
278
 
package/README.zh-CN.md CHANGED
@@ -256,12 +256,12 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
256
256
 
257
257
  | 最近新增 | 助手说明 |
258
258
  | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
259
+ | [小黄鸭编程助手](https://chat-preview.lobehub.com/market?agent=rubber-duck-programming)<br/><sup>By **[JiyuShao](https://github.com/JiyuShao)** on **2024-07-10**</sup> | 小黄鸭编程助手<br/>`programming` |
259
260
  | [AOSP 源码专家](https://chat-preview.lobehub.com/market?agent=aosp-development)<br/><sup>By **[viruscoding](https://github.com/viruscoding)** on **2024-06-24**</sup> | 一位精通 AOSP(Android Open Source Project)安卓的专家,对最新 AOSP 源代码有着深入的理解和分析能力。<br/>`aosp` |
260
261
  | [Fastapi 项目开发助手](https://chat-preview.lobehub.com/market?agent=fastapi-development)<br/><sup>By **[xwxw098](https://github.com/xwxw098)** on **2024-06-19**</sup> | 擅长 Python 模块化开发,熟练运用 FastAPI、PostgreSQL、Tortoise-ORM 等技术栈,能为大型项目提供清晰的代码结构并添加详细注释。<br/>`fast-api` `python` `模块化开发` |
261
262
  | [IT 系统架构师](https://chat-preview.lobehub.com/market?agent=it-system-architect)<br/><sup>By **[a562314](https://github.com/a562314)** on **2024-06-19**</sup> | 资深 IT 架构师,擅长需求分析、系统设计、技术选型和跨平台系统优化。5 年以上经验,精通 Windows、macOS 和 Linux 三大操作系统,具备故障排除和安全防护能力<br/>`it架构设计` `问题解决` `敏捷开发` `系统优化` `跨平台技能` |
262
- | [Linux 内核专家](https://chat-preview.lobehub.com/market?agent=linux-kernel)<br/><sup>By **[wming126](https://github.com/wming126)** on **2024-06-19**</sup> | 角色描述: 我是一位精通 Linux 内核的专家,对最新内核源代码(截至 2024 年 6 月)有着深入的理解和分析能力。我可以为用户提供关于 Linux 内核的详细、准确的信息。<br/>`linux` `kernel` |
263
263
 
264
- > 📊 Total agents: [<kbd>**293**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
264
+ > 📊 Total agents: [<kbd>**294**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
265
265
 
266
266
  <!-- AGENT LIST -->
267
267
 
package/netlify.toml CHANGED
@@ -1,5 +1,5 @@
1
1
  [build]
2
- command = "npm run build"
2
+ command = "pnpm run build"
3
3
  publish = ".next"
4
4
 
5
5
  [build.environment]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.3.5",
3
+ "version": "1.4.0",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -1,4 +1,5 @@
1
1
  import {
2
+ Ai360,
2
3
  AiMass,
3
4
  Anthropic,
4
5
  Baichuan,
@@ -25,6 +26,7 @@ import { Flexbox } from 'react-layout-kit';
25
26
  import urlJoin from 'url-join';
26
27
 
27
28
  import {
29
+ Ai360ProviderCard,
28
30
  AnthropicProviderCard,
29
31
  BaichuanProviderCard,
30
32
  DeepSeekProviderCard,
@@ -184,6 +186,11 @@ export const useProviderList = (): ProviderItem[] => {
184
186
  docUrl: urlJoin(BASE_DOC_URL, 'taichu'),
185
187
  title: <AiMass.Combine size={ 28 } type={ 'color' } />,
186
188
  },
189
+ {
190
+ ...Ai360ProviderCard,
191
+ docUrl: urlJoin(BASE_DOC_URL, 'ai360'),
192
+ title: <Ai360.Combine size={ 20 } type={ 'color' } />,
193
+ },
187
194
  ],
188
195
  [azureProvider, ollamaProvider, ollamaProvider, bedrockProvider],
189
196
  );
@@ -184,6 +184,13 @@ const getLlmOptionsFromPayload = (provider: string, payload: JWTPayload) => {
184
184
 
185
185
  const apiKey = apiKeyManager.pick(payload?.apiKey || TAICHU_API_KEY);
186
186
 
187
+ return { apiKey };
188
+ }
189
+ case ModelProvider.Ai360: {
190
+ const { AI360_API_KEY } = getLLMConfig();
191
+
192
+ const apiKey = apiKeyManager.pick(payload?.apiKey || AI360_API_KEY);
193
+
187
194
  return { apiKey };
188
195
  }
189
196
  }
@@ -193,11 +200,16 @@ const getLlmOptionsFromPayload = (provider: string, payload: JWTPayload) => {
193
200
  * Initializes the agent runtime with the user payload in backend
194
201
  * @param provider - The provider name.
195
202
  * @param payload - The JWT payload.
203
+ * @param params
196
204
  * @returns A promise that resolves when the agent runtime is initialized.
197
205
  */
198
- export const initAgentRuntimeWithUserPayload = (provider: string, payload: JWTPayload) => {
206
+ export const initAgentRuntimeWithUserPayload = (
207
+ provider: string,
208
+ payload: JWTPayload,
209
+ params: any = {},
210
+ ) => {
199
211
  return AgentRuntime.initializeWithProviderOptions(provider, {
200
- [provider]: getLlmOptionsFromPayload(provider, payload),
212
+ [provider]: { ...getLlmOptionsFromPayload(provider, payload), ...params },
201
213
  });
202
214
  };
203
215
 
@@ -2,6 +2,7 @@ import {
2
2
  AiMass,
3
3
  Adobe,
4
4
  Ai21,
5
+ Ai360,
5
6
  Aws,
6
7
  Azure,
7
8
  Baichuan,
@@ -68,6 +69,7 @@ const ModelIcon = memo<ModelProviderIconProps>(({ model: originModel, size = 12
68
69
  if (model.includes('dbrx')) return <Dbrx.Avatar size={size} />;
69
70
  if (model.includes('step')) return <Stepfun.Avatar size={size} />;
70
71
  if (model.includes('taichu')) return <AiMass.Avatar size={size} />;
72
+ if (model.includes('360gpt')) return <Ai360.Avatar size={size} />;
71
73
 
72
74
  // below: To be supported in providers, move up if supported
73
75
  if (model.includes('baichuan'))
@@ -1,4 +1,5 @@
1
1
  import {
2
+ Ai360,
2
3
  AiMass,
3
4
  Anthropic,
4
5
  Azure,
@@ -124,6 +125,10 @@ const ModelProviderIcon = memo<ModelProviderIconProps>(({ provider }) => {
124
125
  return <AiMass size={20} />;
125
126
  }
126
127
 
128
+ case ModelProvider.Ai360: {
129
+ return <Ai360 size={20} />;
130
+ }
131
+
127
132
  default: {
128
133
  return null;
129
134
  }
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AdobeFirefly,
3
3
  Ai21,
4
+ Ai360,
4
5
  AiMass,
5
6
  Aws,
6
7
  Azure,
@@ -63,6 +64,7 @@ const ModelIcon = memo<ModelIconProps>(({ model, size = 12 }) => {
63
64
  if (model.includes('command')) return <Cohere size={size} />;
64
65
  if (model.includes('dbrx')) return <Dbrx size={size} />;
65
66
  if (model.includes('taichu')) return <AiMass size={size} />;
67
+ if (model.includes('360gpt')) return <Ai360 size={size} />;
66
68
 
67
69
  // below: To be supported in providers, move up if supported
68
70
  if (model.includes('baichuan')) return <Baichuan size={size} />;
package/src/config/llm.ts CHANGED
@@ -88,6 +88,9 @@ export const getLLMConfig = () => {
88
88
 
89
89
  ENABLED_TAICHU: z.boolean(),
90
90
  TAICHU_API_KEY: z.string().optional(),
91
+
92
+ ENABLED_AI360: z.boolean(),
93
+ AI360_API_KEY: z.string().optional(),
91
94
  },
92
95
  runtimeEnv: {
93
96
  API_KEY_SELECT_MODE: process.env.API_KEY_SELECT_MODE,
@@ -167,6 +170,9 @@ export const getLLMConfig = () => {
167
170
 
168
171
  ENABLED_TAICHU: !!process.env.TAICHU_API_KEY,
169
172
  TAICHU_API_KEY: process.env.TAICHU_API_KEY,
173
+
174
+ ENABLED_AI360: !!process.env.AI360_API_KEY,
175
+ AI360_API_KEY: process.env.AI360_API_KEY,
170
176
  },
171
177
  });
172
178
  };
@@ -0,0 +1,38 @@
1
+ import { ModelProviderCard } from '@/types/llm';
2
+
3
+ // ref https://ai.360.cn/platform/docs/overview
4
+ const Ai360: ModelProviderCard = {
5
+ chatModels: [
6
+ {
7
+ displayName: '360GPT Pro',
8
+ enabled: true,
9
+ functionCall: false,
10
+ id: '360gpt-pro',
11
+ maxOutput: 7000,
12
+ tokens: 8192,
13
+ },
14
+ {
15
+ displayName: '360GPT Turbo',
16
+ enabled: true,
17
+ functionCall: false,
18
+ id: '360gpt-turbo',
19
+ maxOutput: 8192,
20
+ tokens: 8192,
21
+ },
22
+ {
23
+ displayName: '360GPT Turbo Responsibility 8K',
24
+ enabled: true,
25
+ functionCall: false,
26
+ id: '360gpt-turbo-responsibility-8k',
27
+ maxOutput: 2048,
28
+ tokens: 8192,
29
+ },
30
+ ],
31
+ checkModel: '360gpt-turbo',
32
+ disableBrowserRequest: true,
33
+ id: 'ai360',
34
+ modelList: { showModelFetcher: true },
35
+ name: '360智脑',
36
+ };
37
+
38
+ export default Ai360;
@@ -1,5 +1,6 @@
1
1
  import { ChatModelCard, ModelProviderCard } from '@/types/llm';
2
2
 
3
+ import Ai360Provider from './ai360';
3
4
  import AnthropicProvider from './anthropic';
4
5
  import AzureProvider from './azure';
5
6
  import BaichuanProvider from './baichuan';
@@ -41,6 +42,7 @@ export const LOBE_DEFAULT_MODEL_LIST: ChatModelCard[] = [
41
42
  StepfunProvider.chatModels,
42
43
  BaichuanProvider.chatModels,
43
44
  TaichuProvider.chatModels,
45
+ Ai360Provider.chatModels,
44
46
  ].flat();
45
47
 
46
48
  export const DEFAULT_MODEL_PROVIDER_LIST = [
@@ -64,6 +66,7 @@ export const DEFAULT_MODEL_PROVIDER_LIST = [
64
66
  StepfunProvider,
65
67
  BaichuanProvider,
66
68
  TaichuProvider,
69
+ Ai360Provider,
67
70
  ];
68
71
 
69
72
  export const filterEnabledModels = (provider: ModelProviderCard) => {
@@ -75,6 +78,7 @@ export const isProviderDisableBroswerRequest = (id: string) => {
75
78
  return !!provider;
76
79
  };
77
80
 
81
+ export { default as Ai360ProviderCard } from './ai360';
78
82
  export { default as AnthropicProviderCard } from './anthropic';
79
83
  export { default as AzureProviderCard } from './azure';
80
84
  export { default as BaichuanProviderCard } from './baichuan';
@@ -1,4 +1,5 @@
1
1
  import {
2
+ Ai360ProviderCard,
2
3
  AnthropicProviderCard,
3
4
  BaichuanProviderCard,
4
5
  BedrockProviderCard,
@@ -24,6 +25,10 @@ import { ModelProvider } from '@/libs/agent-runtime';
24
25
  import { UserModelProviderConfig } from '@/types/user/settings';
25
26
 
26
27
  export const DEFAULT_LLM_CONFIG: UserModelProviderConfig = {
28
+ ai360: {
29
+ enabled: false,
30
+ enabledModels: filterEnabledModels(Ai360ProviderCard),
31
+ },
27
32
  anthropic: {
28
33
  enabled: false,
29
34
  enabledModels: filterEnabledModels(AnthropicProviderCard),
@@ -1,4 +1,5 @@
1
1
  import {
2
+ Ai360,
2
3
  AiMass,
3
4
  Anthropic,
4
5
  Baichuan,
@@ -94,6 +95,10 @@ const ProviderAvatar = memo<ProviderAvatarProps>(({ provider }) => {
94
95
  return <ZeroOne color={ZeroOne.colorPrimary} size={56} />;
95
96
  }
96
97
 
98
+ case ModelProvider.Ai360: {
99
+ return <Ai360 color={Ai360.colorPrimary} size={56} />;
100
+ }
101
+
97
102
  default:
98
103
  case ModelProvider.OpenAI: {
99
104
  return <OpenAI color={theme.colorText} size={64} />;
@@ -3,6 +3,7 @@ import { ClientOptions } from 'openai';
3
3
  import type { TracePayload } from '@/const/trace';
4
4
 
5
5
  import { LobeRuntimeAI } from './BaseAI';
6
+ import { LobeAi360AI } from './ai360';
6
7
  import { LobeAnthropicAI } from './anthropic';
7
8
  import { LobeAzureOpenAI } from './azureOpenai';
8
9
  import { LobeBaichuanAI } from './baichuan';
@@ -103,6 +104,7 @@ class AgentRuntime {
103
104
  static async initializeWithProviderOptions(
104
105
  provider: string,
105
106
  params: Partial<{
107
+ ai360: Partial<ClientOptions>;
106
108
  anthropic: Partial<ClientOptions>;
107
109
  azure: { apiVersion?: string; apikey?: string; endpoint?: string };
108
110
  baichuan: Partial<ClientOptions>;
@@ -145,92 +147,97 @@ class AgentRuntime {
145
147
  }
146
148
 
147
149
  case ModelProvider.ZhiPu: {
148
- runtimeModel = await LobeZhipuAI.fromAPIKey(params.zhipu ?? {});
150
+ runtimeModel = await LobeZhipuAI.fromAPIKey(params.zhipu);
149
151
  break;
150
152
  }
151
153
 
152
154
  case ModelProvider.Google: {
153
- runtimeModel = new LobeGoogleAI(params.google ?? {});
155
+ runtimeModel = new LobeGoogleAI(params.google);
154
156
  break;
155
157
  }
156
158
 
157
159
  case ModelProvider.Moonshot: {
158
- runtimeModel = new LobeMoonshotAI(params.moonshot ?? {});
160
+ runtimeModel = new LobeMoonshotAI(params.moonshot);
159
161
  break;
160
162
  }
161
163
 
162
164
  case ModelProvider.Bedrock: {
163
- runtimeModel = new LobeBedrockAI(params.bedrock ?? {});
165
+ runtimeModel = new LobeBedrockAI(params.bedrock);
164
166
  break;
165
167
  }
166
168
 
167
169
  case ModelProvider.Ollama: {
168
- runtimeModel = new LobeOllamaAI(params.ollama ?? {});
170
+ runtimeModel = new LobeOllamaAI(params.ollama);
169
171
  break;
170
172
  }
171
173
 
172
174
  case ModelProvider.Perplexity: {
173
- runtimeModel = new LobePerplexityAI(params.perplexity ?? {});
175
+ runtimeModel = new LobePerplexityAI(params.perplexity);
174
176
  break;
175
177
  }
176
178
 
177
179
  case ModelProvider.Anthropic: {
178
- runtimeModel = new LobeAnthropicAI(params.anthropic ?? {});
180
+ runtimeModel = new LobeAnthropicAI(params.anthropic);
179
181
  break;
180
182
  }
181
183
 
182
184
  case ModelProvider.DeepSeek: {
183
- runtimeModel = new LobeDeepSeekAI(params.deepseek ?? {});
185
+ runtimeModel = new LobeDeepSeekAI(params.deepseek);
184
186
  break;
185
187
  }
186
188
 
187
189
  case ModelProvider.Minimax: {
188
- runtimeModel = new LobeMinimaxAI(params.minimax ?? {});
190
+ runtimeModel = new LobeMinimaxAI(params.minimax);
189
191
  break;
190
192
  }
191
193
 
192
194
  case ModelProvider.Mistral: {
193
- runtimeModel = new LobeMistralAI(params.mistral ?? {});
195
+ runtimeModel = new LobeMistralAI(params.mistral);
194
196
  break;
195
197
  }
196
198
 
197
199
  case ModelProvider.Groq: {
198
- runtimeModel = new LobeGroq(params.groq ?? {});
200
+ runtimeModel = new LobeGroq(params.groq);
199
201
  break;
200
202
  }
201
203
 
202
204
  case ModelProvider.OpenRouter: {
203
- runtimeModel = new LobeOpenRouterAI(params.openrouter ?? {});
205
+ runtimeModel = new LobeOpenRouterAI(params.openrouter);
204
206
  break;
205
207
  }
206
208
 
207
209
  case ModelProvider.TogetherAI: {
208
- runtimeModel = new LobeTogetherAI(params.togetherai ?? {});
210
+ runtimeModel = new LobeTogetherAI(params.togetherai);
209
211
  break;
210
212
  }
211
213
 
212
214
  case ModelProvider.ZeroOne: {
213
- runtimeModel = new LobeZeroOneAI(params.zeroone ?? {});
215
+ runtimeModel = new LobeZeroOneAI(params.zeroone);
214
216
  break;
215
217
  }
216
218
 
217
219
  case ModelProvider.Qwen: {
218
- runtimeModel = new LobeQwenAI(params.qwen ?? {});
220
+ runtimeModel = new LobeQwenAI(params.qwen);
219
221
  break;
220
222
  }
221
223
 
222
224
  case ModelProvider.Stepfun: {
223
- runtimeModel = new LobeStepfunAI(params.stepfun ?? {});
225
+ runtimeModel = new LobeStepfunAI(params.stepfun);
224
226
  break;
225
227
  }
226
228
 
227
229
  case ModelProvider.Baichuan: {
228
- runtimeModel = new LobeBaichuanAI(params.baichuan ?? {});
229
- break
230
+ runtimeModel = new LobeBaichuanAI(params.baichuan);
231
+ break;
230
232
  }
231
233
 
232
234
  case ModelProvider.Taichu: {
233
- runtimeModel = new LobeTaichuAI(params.taichu ?? {});
235
+ runtimeModel = new LobeTaichuAI(params.taichu);
236
+ break;
237
+ }
238
+
239
+ case ModelProvider.Ai360: {
240
+ runtimeModel = new LobeAi360AI(params.ai360 ?? {});
234
241
  break
235
242
  }
236
243
  }
@@ -0,0 +1,255 @@
1
+ // @vitest-environment node
2
+ import OpenAI from 'openai';
3
+ import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import {
6
+ ChatStreamCallbacks,
7
+ LobeOpenAICompatibleRuntime,
8
+ ModelProvider,
9
+ } from '@/libs/agent-runtime';
10
+
11
+ import * as debugStreamModule from '../utils/debugStream';
12
+ import { LobeAi360AI } from './index';
13
+
14
+ const provider = ModelProvider.Ai360;
15
+ const defaultBaseURL = 'https://ai.360.cn/v1';
16
+
17
+ const bizErrorType = 'ProviderBizError';
18
+ const invalidErrorType = 'InvalidProviderAPIKey';
19
+
20
+ // Mock the console.error to avoid polluting test output
21
+ vi.spyOn(console, 'error').mockImplementation(() => {});
22
+
23
+ let instance: LobeOpenAICompatibleRuntime;
24
+
25
+ beforeEach(() => {
26
+ instance = new LobeAi360AI({ apiKey: 'test' });
27
+
28
+ // 使用 vi.spyOn 来模拟 chat.completions.create 方法
29
+ vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
30
+ new ReadableStream() as any,
31
+ );
32
+ });
33
+
34
+ afterEach(() => {
35
+ vi.clearAllMocks();
36
+ });
37
+
38
+ describe('LobeAi360AI', () => {
39
+ describe('init', () => {
40
+ it('should correctly initialize with an API key', async () => {
41
+ const instance = new LobeAi360AI({ apiKey: 'test_api_key' });
42
+ expect(instance).toBeInstanceOf(LobeAi360AI);
43
+ expect(instance.baseURL).toEqual(defaultBaseURL);
44
+ });
45
+ });
46
+
47
+ describe('chat', () => {
48
+ describe('Error', () => {
49
+ it('should return OpenAIBizError with an openai error response when OpenAI.APIError is thrown', async () => {
50
+ // Arrange
51
+ const apiError = new OpenAI.APIError(
52
+ 400,
53
+ {
54
+ status: 400,
55
+ error: {
56
+ message: 'Bad Request',
57
+ },
58
+ },
59
+ 'Error message',
60
+ {},
61
+ );
62
+
63
+ vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
64
+
65
+ // Act
66
+ try {
67
+ await instance.chat({
68
+ messages: [{ content: 'Hello', role: 'user' }],
69
+ model: '360gpt-turbo',
70
+ temperature: 0,
71
+ });
72
+ } catch (e) {
73
+ expect(e).toEqual({
74
+ endpoint: defaultBaseURL,
75
+ error: {
76
+ error: { message: 'Bad Request' },
77
+ status: 400,
78
+ },
79
+ errorType: bizErrorType,
80
+ provider,
81
+ });
82
+ }
83
+ });
84
+
85
+ it('should throw AgentRuntimeError with NoOpenAIAPIKey if no apiKey is provided', async () => {
86
+ try {
87
+ new LobeAi360AI({});
88
+ } catch (e) {
89
+ expect(e).toEqual({ errorType: invalidErrorType });
90
+ }
91
+ });
92
+
93
+ it('should return OpenAIBizError with the cause when OpenAI.APIError is thrown with cause', async () => {
94
+ // Arrange
95
+ const errorInfo = {
96
+ stack: 'abc',
97
+ cause: {
98
+ message: 'api is undefined',
99
+ },
100
+ };
101
+ const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
102
+
103
+ vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
104
+
105
+ // Act
106
+ try {
107
+ await instance.chat({
108
+ messages: [{ content: 'Hello', role: 'user' }],
109
+ model: '360gpt-turbo',
110
+ temperature: 0,
111
+ });
112
+ } catch (e) {
113
+ expect(e).toEqual({
114
+ endpoint: defaultBaseURL,
115
+ error: {
116
+ cause: { message: 'api is undefined' },
117
+ stack: 'abc',
118
+ },
119
+ errorType: bizErrorType,
120
+ provider,
121
+ });
122
+ }
123
+ });
124
+
125
+ it('should return OpenAIBizError with an cause response with desensitize Url', async () => {
126
+ // Arrange
127
+ const errorInfo = {
128
+ stack: 'abc',
129
+ cause: { message: 'api is undefined' },
130
+ };
131
+ const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
132
+
133
+ instance = new LobeAi360AI({
134
+ apiKey: 'test',
135
+
136
+ baseURL: 'https://api.abc.com/v1',
137
+ });
138
+
139
+ vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
140
+
141
+ // Act
142
+ try {
143
+ await instance.chat({
144
+ messages: [{ content: 'Hello', role: 'user' }],
145
+ model: '360gpt-turbo',
146
+ temperature: 0,
147
+ });
148
+ } catch (e) {
149
+ expect(e).toEqual({
150
+ endpoint: 'https://api.***.com/v1',
151
+ error: {
152
+ cause: { message: 'api is undefined' },
153
+ stack: 'abc',
154
+ },
155
+ errorType: bizErrorType,
156
+ provider,
157
+ });
158
+ }
159
+ });
160
+
161
+ it('should throw an InvalidAi360APIKey error type on 401 status code', async () => {
162
+ // Mock the API call to simulate a 401 error
163
+ const error = new Error('Unauthorized') as any;
164
+ error.status = 401;
165
+ vi.mocked(instance['client'].chat.completions.create).mockRejectedValue(error);
166
+
167
+ try {
168
+ await instance.chat({
169
+ messages: [{ content: 'Hello', role: 'user' }],
170
+ model: '360gpt-turbo',
171
+ temperature: 0,
172
+ });
173
+ } catch (e) {
174
+ // Expect the chat method to throw an error with InvalidAi360APIKey
175
+ expect(e).toEqual({
176
+ endpoint: defaultBaseURL,
177
+ error: new Error('Unauthorized'),
178
+ errorType: invalidErrorType,
179
+ provider,
180
+ });
181
+ }
182
+ });
183
+
184
+ it('should return AgentRuntimeError for non-OpenAI errors', async () => {
185
+ // Arrange
186
+ const genericError = new Error('Generic Error');
187
+
188
+ vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(genericError);
189
+
190
+ // Act
191
+ try {
192
+ await instance.chat({
193
+ messages: [{ content: 'Hello', role: 'user' }],
194
+ model: '360gpt-turbo',
195
+ temperature: 0,
196
+ });
197
+ } catch (e) {
198
+ expect(e).toEqual({
199
+ endpoint: defaultBaseURL,
200
+ errorType: 'AgentRuntimeError',
201
+ provider,
202
+ error: {
203
+ name: genericError.name,
204
+ cause: genericError.cause,
205
+ message: genericError.message,
206
+ stack: genericError.stack,
207
+ },
208
+ });
209
+ }
210
+ });
211
+ });
212
+
213
+ describe('DEBUG', () => {
214
+ it('should call debugStream and return StreamingTextResponse when DEBUG_AI360_CHAT_COMPLETION is 1', async () => {
215
+ // Arrange
216
+ const mockProdStream = new ReadableStream() as any; // 模拟的 prod 流
217
+ const mockDebugStream = new ReadableStream({
218
+ start(controller) {
219
+ controller.enqueue('Debug stream content');
220
+ controller.close();
221
+ },
222
+ }) as any;
223
+ mockDebugStream.toReadableStream = () => mockDebugStream; // 添加 toReadableStream 方法
224
+
225
+ // 模拟 chat.completions.create 返回值,包括模拟的 tee 方法
226
+ (instance['client'].chat.completions.create as Mock).mockResolvedValue({
227
+ tee: () => [mockProdStream, { toReadableStream: () => mockDebugStream }],
228
+ });
229
+
230
+ // 保存原始环境变量值
231
+ const originalDebugValue = process.env.DEBUG_AI360_CHAT_COMPLETION;
232
+
233
+ // 模拟环境变量
234
+ process.env.DEBUG_AI360_CHAT_COMPLETION = '1';
235
+ vi.spyOn(debugStreamModule, 'debugStream').mockImplementation(() => Promise.resolve());
236
+
237
+ // 执行测试
238
+ // 运行你的测试函数,确保它会在条件满足时调用 debugStream
239
+ // 假设的测试函数调用,你可能需要根据实际情况调整
240
+ await instance.chat({
241
+ messages: [{ content: 'Hello', role: 'user' }],
242
+ model: '360gpt-turbo',
243
+ stream: true,
244
+ temperature: 0,
245
+ });
246
+
247
+ // 验证 debugStream 被调用
248
+ expect(debugStreamModule.debugStream).toHaveBeenCalled();
249
+
250
+ // 恢复原始环境变量值
251
+ process.env.DEBUG_AI360_CHAT_COMPLETION = originalDebugValue;
252
+ });
253
+ });
254
+ });
255
+ });
@@ -0,0 +1,10 @@
1
+ import { ModelProvider } from '../types';
2
+ import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory';
3
+
4
+ export const LobeAi360AI = LobeOpenAICompatibleFactory({
5
+ baseURL: 'https://ai.360.cn/v1',
6
+ debug: {
7
+ chatCompletion: () => process.env.DEBUG_AI360_CHAT_COMPLETION === '1',
8
+ },
9
+ provider: ModelProvider.Ai360,
10
+ });
@@ -20,7 +20,7 @@ export class LobeAnthropicAI implements LobeRuntimeAI {
20
20
 
21
21
  baseURL: string;
22
22
 
23
- constructor({ apiKey, baseURL = DEFAULT_BASE_URL }: ClientOptions) {
23
+ constructor({ apiKey, baseURL = DEFAULT_BASE_URL }: ClientOptions = {}) {
24
24
  if (!apiKey) throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidProviderAPIKey);
25
25
 
26
26
  this.client = new Anthropic({ apiKey, baseURL });
@@ -28,7 +28,7 @@ export class LobeBedrockAI implements LobeRuntimeAI {
28
28
 
29
29
  region: string;
30
30
 
31
- constructor({ region, accessKeyId, accessKeySecret }: LobeBedrockAIParams) {
31
+ constructor({ region, accessKeyId, accessKeySecret }: LobeBedrockAIParams = {}) {
32
32
  if (!(accessKeyId && accessKeySecret))
33
33
  throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidBedrockCredentials);
34
34
 
@@ -41,7 +41,7 @@ export class LobeGoogleAI implements LobeRuntimeAI {
41
41
  private client: GoogleGenerativeAI;
42
42
  baseURL?: string;
43
43
 
44
- constructor({ apiKey, baseURL }: { apiKey?: string; baseURL?: string }) {
44
+ constructor({ apiKey, baseURL }: { apiKey?: string; baseURL?: string } = {}) {
45
45
  if (!apiKey) throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidProviderAPIKey);
46
46
 
47
47
  this.client = new GoogleGenerativeAI(apiKey);
@@ -63,7 +63,7 @@ function parseMinimaxResponse(chunk: string): MinimaxResponse | undefined {
63
63
  export class LobeMinimaxAI implements LobeRuntimeAI {
64
64
  apiKey: string;
65
65
 
66
- constructor({ apiKey }: { apiKey?: string }) {
66
+ constructor({ apiKey }: { apiKey?: string } = {}) {
67
67
  if (!apiKey) throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidProviderAPIKey);
68
68
 
69
69
  this.apiKey = apiKey;
@@ -18,7 +18,7 @@ export class LobeOllamaAI implements LobeRuntimeAI {
18
18
 
19
19
  baseURL?: string;
20
20
 
21
- constructor({ baseURL }: ClientOptions) {
21
+ constructor({ baseURL }: ClientOptions = {}) {
22
22
  try {
23
23
  if (baseURL) new URL(baseURL);
24
24
  } catch {
@@ -22,6 +22,7 @@ export interface CreateChatCompletionOptions {
22
22
  }
23
23
 
24
24
  export enum ModelProvider {
25
+ Ai360 = 'ai360',
25
26
  Anthropic = 'anthropic',
26
27
  Azure = 'azure',
27
28
  Baichuan = 'baichuan',
@@ -27,13 +27,18 @@ const CHAT_MODELS_BLOCK_LIST = [
27
27
  'dall-e',
28
28
  ];
29
29
 
30
- interface OpenAICompatibleFactoryOptions {
30
+ type ConstructorOptions<T extends Record<string, any> = any> = ClientOptions & T;
31
+
32
+ interface OpenAICompatibleFactoryOptions<T extends Record<string, any> = any> {
31
33
  baseURL?: string;
32
34
  chatCompletion?: {
33
- handleError?: (error: any) => Omit<ChatCompletionErrorPayload, 'provider'> | undefined;
35
+ handleError?: (
36
+ error: any,
37
+ options: ConstructorOptions<T>,
38
+ ) => Omit<ChatCompletionErrorPayload, 'provider'> | undefined;
34
39
  handlePayload?: (payload: ChatStreamPayload) => OpenAI.ChatCompletionCreateParamsStreaming;
35
40
  };
36
- constructorOptions?: ClientOptions;
41
+ constructorOptions?: ConstructorOptions<T>;
37
42
  debug?: {
38
43
  chatCompletion: () => boolean;
39
44
  };
@@ -49,7 +54,7 @@ interface OpenAICompatibleFactoryOptions {
49
54
  provider: string;
50
55
  }
51
56
 
52
- export const LobeOpenAICompatibleFactory = ({
57
+ export const LobeOpenAICompatibleFactory = <T extends Record<string, any> = any>({
53
58
  provider,
54
59
  baseURL: DEFAULT_BASE_URL,
55
60
  errorType,
@@ -57,7 +62,7 @@ export const LobeOpenAICompatibleFactory = ({
57
62
  constructorOptions,
58
63
  chatCompletion,
59
64
  models,
60
- }: OpenAICompatibleFactoryOptions) => {
65
+ }: OpenAICompatibleFactoryOptions<T>) => {
61
66
  const ErrorType = {
62
67
  bizError: errorType?.bizError || AgentRuntimeErrorType.ProviderBizError,
63
68
  invalidAPIKey: errorType?.invalidAPIKey || AgentRuntimeErrorType.InvalidProviderAPIKey,
@@ -67,8 +72,11 @@ export const LobeOpenAICompatibleFactory = ({
67
72
  client: OpenAI;
68
73
 
69
74
  baseURL: string;
75
+ private _options: ConstructorOptions<T>;
70
76
 
71
- constructor({ apiKey, baseURL = DEFAULT_BASE_URL, ...res }: ClientOptions) {
77
+ constructor(options: ClientOptions & Record<string, any> = {}) {
78
+ const { apiKey, baseURL = DEFAULT_BASE_URL, ...res } = options;
79
+ this._options = options as ConstructorOptions<T>;
72
80
  if (!apiKey) throw AgentRuntimeError.createError(ErrorType?.invalidAPIKey);
73
81
 
74
82
  this.client = new OpenAI({ apiKey, baseURL, ...constructorOptions, ...res });
@@ -115,48 +123,7 @@ export const LobeOpenAICompatibleFactory = ({
115
123
  headers: options?.headers,
116
124
  });
117
125
  } catch (error) {
118
- let desensitizedEndpoint = this.baseURL;
119
-
120
- // refs: https://github.com/lobehub/lobe-chat/issues/842
121
- if (this.baseURL !== DEFAULT_BASE_URL) {
122
- desensitizedEndpoint = desensitizeUrl(this.baseURL);
123
- }
124
-
125
- if ('status' in (error as any)) {
126
- switch ((error as Response).status) {
127
- case 401: {
128
- throw AgentRuntimeError.chat({
129
- endpoint: desensitizedEndpoint,
130
- error: error as any,
131
- errorType: ErrorType.invalidAPIKey,
132
- provider: provider as any,
133
- });
134
- }
135
-
136
- default: {
137
- break;
138
- }
139
- }
140
- }
141
-
142
- if (chatCompletion?.handleError) {
143
- const errorResult = chatCompletion.handleError(error);
144
-
145
- if (errorResult)
146
- throw AgentRuntimeError.chat({
147
- ...errorResult,
148
- provider,
149
- } as ChatCompletionErrorPayload);
150
- }
151
-
152
- const { errorResult, RuntimeError } = handleOpenAIError(error);
153
-
154
- throw AgentRuntimeError.chat({
155
- endpoint: desensitizedEndpoint,
156
- error: errorResult,
157
- errorType: RuntimeError || ErrorType.bizError,
158
- provider: provider as any,
159
- });
126
+ throw this.handleError(error);
160
127
  }
161
128
  }
162
129
 
@@ -191,48 +158,7 @@ export const LobeOpenAICompatibleFactory = ({
191
158
  const res = await this.client.images.generate(payload);
192
159
  return res.data.map((o) => o.url) as string[];
193
160
  } catch (error) {
194
- let desensitizedEndpoint = this.baseURL;
195
-
196
- // refs: https://github.com/lobehub/lobe-chat/issues/842
197
- if (this.baseURL !== DEFAULT_BASE_URL) {
198
- desensitizedEndpoint = desensitizeUrl(this.baseURL);
199
- }
200
-
201
- if ('status' in (error as any)) {
202
- switch ((error as Response).status) {
203
- case 401: {
204
- throw AgentRuntimeError.chat({
205
- endpoint: desensitizedEndpoint,
206
- error: error as any,
207
- errorType: ErrorType.invalidAPIKey,
208
- provider: provider as any,
209
- });
210
- }
211
-
212
- default: {
213
- break;
214
- }
215
- }
216
- }
217
-
218
- if (chatCompletion?.handleError) {
219
- const errorResult = chatCompletion.handleError(error);
220
-
221
- if (errorResult)
222
- throw AgentRuntimeError.chat({
223
- ...errorResult,
224
- provider,
225
- } as ChatCompletionErrorPayload);
226
- }
227
-
228
- const { errorResult, RuntimeError } = handleOpenAIError(error);
229
-
230
- throw AgentRuntimeError.chat({
231
- endpoint: desensitizedEndpoint,
232
- error: errorResult,
233
- errorType: RuntimeError || ErrorType.bizError,
234
- provider: provider as any,
235
- });
161
+ throw this.handleError(error);
236
162
  }
237
163
  }
238
164
 
@@ -289,5 +215,50 @@ export const LobeOpenAICompatibleFactory = ({
289
215
  },
290
216
  });
291
217
  }
218
+
219
+ private handleError(error: any): ChatCompletionErrorPayload {
220
+ let desensitizedEndpoint = this.baseURL;
221
+
222
+ // refs: https://github.com/lobehub/lobe-chat/issues/842
223
+ if (this.baseURL !== DEFAULT_BASE_URL) {
224
+ desensitizedEndpoint = desensitizeUrl(this.baseURL);
225
+ }
226
+
227
+ if (chatCompletion?.handleError) {
228
+ const errorResult = chatCompletion.handleError(error, this._options);
229
+
230
+ if (errorResult)
231
+ return AgentRuntimeError.chat({
232
+ ...errorResult,
233
+ provider,
234
+ } as ChatCompletionErrorPayload);
235
+ }
236
+
237
+ if ('status' in (error as any)) {
238
+ switch ((error as Response).status) {
239
+ case 401: {
240
+ return AgentRuntimeError.chat({
241
+ endpoint: desensitizedEndpoint,
242
+ error: error as any,
243
+ errorType: ErrorType.invalidAPIKey,
244
+ provider: provider as any,
245
+ });
246
+ }
247
+
248
+ default: {
249
+ break;
250
+ }
251
+ }
252
+ }
253
+
254
+ const { errorResult, RuntimeError } = handleOpenAIError(error);
255
+
256
+ return AgentRuntimeError.chat({
257
+ endpoint: desensitizedEndpoint,
258
+ error: errorResult,
259
+ errorType: RuntimeError || ErrorType.bizError,
260
+ provider: provider as any,
261
+ });
262
+ }
292
263
  };
293
264
  };
@@ -29,7 +29,7 @@ export class LobeZhipuAI implements LobeRuntimeAI {
29
29
  this.baseURL = this.client.baseURL;
30
30
  }
31
31
 
32
- static async fromAPIKey({ apiKey, baseURL = DEFAULT_BASE_URL, ...res }: ClientOptions) {
32
+ static async fromAPIKey({ apiKey, baseURL = DEFAULT_BASE_URL, ...res }: ClientOptions = {}) {
33
33
  const invalidZhipuAPIKey = AgentRuntimeError.createError(
34
34
  AgentRuntimeErrorType.InvalidProviderAPIKey,
35
35
  );
@@ -36,6 +36,7 @@ export const getServerGlobalConfig = () => {
36
36
  ENABLED_STEPFUN,
37
37
  ENABLED_BAICHUAN,
38
38
  ENABLED_TAICHU,
39
+ ENABLED_AI360,
39
40
 
40
41
  ENABLED_AZURE_OPENAI,
41
42
  AZURE_MODEL_LIST,
@@ -60,6 +61,7 @@ export const getServerGlobalConfig = () => {
60
61
  enabledAccessCode: ACCESS_CODES?.length > 0,
61
62
  enabledOAuthSSO: enableNextAuth,
62
63
  languageModel: {
64
+ ai360: { enabled: ENABLED_AI360 },
63
65
  anthropic: {
64
66
  enabled: ENABLED_ANTHROPIC,
65
67
  },
@@ -16,6 +16,7 @@ export interface AWSBedrockKeyVault {
16
16
  }
17
17
 
18
18
  export interface UserKeyVaults {
19
+ ai360?: OpenAICompatibleKeyVault;
19
20
  anthropic?: OpenAICompatibleKeyVault;
20
21
  azure?: AzureOpenAIKeyVault;
21
22
  baichuan?: OpenAICompatibleKeyVault;