@lobehub/chat 1.6.6 → 1.6.8

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 (33) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +2 -1
  3. package/README.zh-CN.md +1 -1
  4. package/docs/usage/providers/novita.mdx +80 -0
  5. package/docs/usage/providers/novita.zh-CN.mdx +76 -0
  6. package/package.json +1 -1
  7. package/src/app/(main)/settings/llm/ProviderList/providers.tsx +9 -2
  8. package/src/app/api/chat/agentRuntime.ts +7 -0
  9. package/src/components/ModelProviderIcon/index.tsx +5 -0
  10. package/src/config/llm.ts +6 -0
  11. package/src/config/modelProviders/index.ts +4 -0
  12. package/src/config/modelProviders/novita.ts +91 -0
  13. package/src/const/settings/llm.ts +5 -0
  14. package/src/database/server/models/__tests__/user.test.ts +1 -1
  15. package/src/database/server/models/user.ts +1 -1
  16. package/src/features/Conversation/Error/APIKeyForm/ProviderAvatar.tsx +4 -0
  17. package/src/libs/agent-runtime/AgentRuntime.ts +8 -1
  18. package/src/libs/agent-runtime/novita/index.test.ts +251 -0
  19. package/src/libs/agent-runtime/novita/index.ts +15 -0
  20. package/src/libs/agent-runtime/types/type.ts +1 -0
  21. package/src/server/globalConfig/index.ts +2 -0
  22. package/src/server/routers/edge/config/__snapshots__/index.test.ts.snap +12 -6
  23. package/src/server/routers/edge/config/index.test.ts +1 -1
  24. package/src/server/routers/edge/upload.ts +1 -1
  25. package/src/server/routers/lambda/importer.ts +15 -10
  26. package/src/server/{modules/DataImporter → services/dataImporter}/__tests__/index.test.ts +3 -3
  27. package/src/server/{modules/DataImporter → services/dataImporter}/index.ts +1 -1
  28. package/src/types/user/settings/keyVaults.ts +1 -0
  29. package/vitest.config.ts +1 -1
  30. /package/src/server/{keyVaultsEncrypt → modules/KeyVaultsEncrypt}/index.test.ts +0 -0
  31. /package/src/server/{keyVaultsEncrypt → modules/KeyVaultsEncrypt}/index.ts +0 -0
  32. /package/src/server/{files/s3.ts → modules/S3/index.ts} +0 -0
  33. /package/src/server/{modules/DataImporter → services/dataImporter}/__tests__/fixtures/messages.json +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 1.6.8](https://github.com/lobehub/lobe-chat/compare/v1.6.7...v1.6.8)
6
+
7
+ <sup>Released on **2024-07-23**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **misc**: Move server modules.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Code refactoring
19
+
20
+ - **misc**: Move server modules, closes [#3291](https://github.com/lobehub/lobe-chat/issues/3291) ([c7c9f39](https://github.com/lobehub/lobe-chat/commit/c7c9f39))
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.6.7](https://github.com/lobehub/lobe-chat/compare/v1.6.6...v1.6.7)
31
+
32
+ <sup>Released on **2024-07-23**</sup>
33
+
34
+ #### 💄 Styles
35
+
36
+ - **misc**: Add new model provider Novita AI.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### Styles
44
+
45
+ - **misc**: Add new model provider Novita AI, closes [#3177](https://github.com/lobehub/lobe-chat/issues/3177) ([08b063f](https://github.com/lobehub/lobe-chat/commit/08b063f))
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.6.6](https://github.com/lobehub/lobe-chat/compare/v1.6.5...v1.6.6)
6
56
 
7
57
  <sup>Released on **2024-07-22**</sup>
package/README.md CHANGED
@@ -134,6 +134,7 @@ We have implemented support for the following model service providers:
134
134
  - **Minimax**: Integrated the Minimax models, including the MoE model **abab6**, offers a broader range of choices. [Learn more](https://www.minimaxi.com/)
135
135
  - **DeepSeek**: Integrated with the DeepSeek series models, an innovative AI startup from China, The product has been designed to provide a model that balances performance with price. [Learn more](https://www.deepseek.com/)
136
136
  - **Qwen**: Integrated the Qwen series models, including the latest **qwen-turbo**, **qwen-plus** and **qwen-max**. [Lean more](https://help.aliyun.com/zh/dashscope/developer-reference/model-introduction)
137
+ - **Novita AI**: Access **Llama**, **Mistral**, and other leading open-source models at cheapest prices. Engage in uncensored role-play, spark creative discussions, and foster unrestricted innovation. **Pay For What You Use.** [Learn more](https://novita.ai/llm-api?utm_source=lobechat&utm_medium=ch&utm_campaign=api)
137
138
 
138
139
  At the same time, we are also planning to support more model service providers, such as Replicate and Perplexity, to further enrich our service provider library. If you would like LobeChat to support your favorite service provider, feel free to join our [community discussion](https://github.com/lobehub/lobe-chat/discussions/1284).
139
140
 
@@ -272,7 +273,7 @@ Our marketplace is not just a showcase platform but also a collaborative space.
272
273
  | [Convert SQL Table Structure to Dao and Mapper](https://chat-preview.lobehub.com/market?agent=my-batis-generator)<br/><sup>By **[MeYoung](https://github.com/MeYoung)** on **2024-07-17**</sup> | Generate entity class and MyBatis Mapper based on a table structure<br/>`sql` `sql` `mybatis` |
273
274
  | [Auto Extraction Data](https://chat-preview.lobehub.com/market?agent=the-20-autoextract)<br/><sup>By **[vkhoilq](https://github.com/vkhoilq)** on **2024-07-17**</sup> | The20 Auto Extraction Data<br/>`the-20` `autoextract` |
274
275
 
275
- > 📊 Total agents: [<kbd>**301**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
276
+ > 📊 Total agents: [<kbd>**302**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
276
277
 
277
278
  <!-- AGENT LIST -->
278
279
 
package/README.zh-CN.md CHANGED
@@ -261,7 +261,7 @@ LobeChat 的插件生态系统是其核心功能的重要扩展,它极大地
261
261
  | [SQL 表结构转 Dao 和 Mapper](https://chat-preview.lobehub.com/market?agent=my-batis-generator)<br/><sup>By **[MeYoung](https://github.com/MeYoung)** on **2024-07-17**</sup> | 给与一个表结构,生成表的实体和 MyBatis 的 Mapper<br/>`sql` `sql` `mybatis` |
262
262
  | [自动提取数据](https://chat-preview.lobehub.com/market?agent=the-20-autoextract)<br/><sup>By **[vkhoilq](https://github.com/vkhoilq)** on **2024-07-17**</sup> | The20 自动提取数据<br/>`the-20` `autoextract` |
263
263
 
264
- > 📊 Total agents: [<kbd>**301**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
264
+ > 📊 Total agents: [<kbd>**302**</kbd> ](https://github.com/lobehub/lobe-chat-agents)
265
265
 
266
266
  <!-- AGENT LIST -->
267
267
 
@@ -0,0 +1,80 @@
1
+ ---
2
+ title: Using Novita AI API Key in LobeChat
3
+ description: >-
4
+ Learn how to integrate Novita AI's language model APIs into LobeChat. Follow the steps to register, create an Novita AI API key,
5
+ configure settings, and chat with our various AI models.
6
+
7
+ tags:
8
+ - Novita AI
9
+ - Llama3
10
+ - Mistral
11
+ - uncensored
12
+ - API key
13
+ - Web UI
14
+ ---
15
+
16
+ # Using Novita AI in LobeChat
17
+
18
+ <Image
19
+ alt={'Using Novita AI in LobeChat'}
20
+ cover
21
+ src={'https://github.com/user-attachments/assets/b2b36128-6a43-4a1f-9c08-99fe73fb565f'}
22
+ />
23
+
24
+ [Novita AI](https://novita.ai/) is an AI API platform that provides a variety of LLM and image generation APIs, supporting Llama3 (8B, 70B), Mistral, and many other cutting-edge models. We offer a variety of censored and uncensored models to meet your different needs.
25
+
26
+ This document will guide you on how to integrate Novita AI in LobeChat:
27
+
28
+ <Steps>
29
+
30
+ ### Step 1: Register and Log in to Novita AI
31
+
32
+ - Visit [Novita.ai](https://novita.ai/) and create an account
33
+ - You can log in with your Google or Github account
34
+ - Upon registration, Novita AI will provide a $0.5 credit.
35
+
36
+ <Image
37
+ alt={'Register OpenRouter'}
38
+ height={457}
39
+ inStep
40
+ src={'https://github.com/user-attachments/assets/f3177ce2-281c-4ed4-a061-239547b466c6'}
41
+ />
42
+
43
+ ### Step 2: Obtain the API Key
44
+
45
+ - Visit Novita AI's [key management page](https://novita.ai/dashboard/key), create and copy an API Key.
46
+
47
+ <Image
48
+ alt={'Obtain Novita AI API key'}
49
+ inStep
50
+ src={'https://github.com/user-attachments/assets/1e33aff2-6186-4e1f-80a8-4a2c855d8cc1'}
51
+ />
52
+
53
+ ### Step 3: Configure Novita AI in LobeChat
54
+
55
+ - Visit the `Settings` interface in LobeChat
56
+ - Find the setting for `novita.ai` under `Language Model`
57
+
58
+ <Image
59
+ alt={'Enter Novita AI API key in LobeChat'}
60
+ inStep
61
+ src={'https://github.com/user-attachments/assets/00c02637-873e-4e7e-9dc3-a95085b16dd7'}
62
+ />
63
+
64
+ - Open novita.ai and enter the obtained API key
65
+ - Choose a Novita AI model for your assistant to start the conversation
66
+
67
+ <Image
68
+ alt={'Select and use Novita AI model'}
69
+ inStep
70
+ src={'https://github.com/user-attachments/assets/6f9f400a-72e0-49de-94cb-5069fddf1163'}
71
+ />
72
+
73
+ <Callout type={'warning'}>
74
+ During usage, you may need to pay the API service provider, please refer to Novita AI's pricing
75
+ policy.
76
+ </Callout>
77
+
78
+ </Steps>
79
+
80
+ You can now engage in conversations using the models provided by Novita AI in LobeChat.
@@ -0,0 +1,76 @@
1
+ ---
2
+ title: 在 LobeChat 中使用 Novita AI API Key
3
+ description: 学习如何将 Novita AI 的大语言模型 API 集成到 LobeChat 中。跟随以下步骤注册 Novita AI 账号、创建 API Key、充值信用额度并在 LobeChat 中进行设置。并与我们的多种 AI 模型交谈。
4
+ tags:
5
+ - Novita AI
6
+ - Llama3
7
+ - Mistral
8
+ - uncensored
9
+ - API key
10
+ - Web UI
11
+ ---
12
+
13
+ # 在 LobeChat 中使用 Novita AI
14
+
15
+ <Image
16
+ alt={'在 LobeChat 中使用 Novita AI'}
17
+ cover
18
+ src={'https://github.com/user-attachments/assets/b2b36128-6a43-4a1f-9c08-99fe73fb565f'}
19
+ />
20
+
21
+ [Novita AI](https://novita.ai/) 是一个 AI API 平台,它提供多种大语言模型与 AI 图像生成的 API 服务。支持 Llama3 (8B, 70B),Mistral 和其他最新的模型。
22
+
23
+ 本文档将指导你如何在 LobeChat 中使用 Novita AI:
24
+
25
+ <Steps>
26
+
27
+ ### 步骤一:注册 Novita AI 账号并登录
28
+
29
+ - 访问 [Novita.ai](https://novita.ai/) 并创建账号
30
+ - 你可以使用 Google 或者 Github 账号登录
31
+ - 注册后,Novita AI 会赠送 0.5 美元的使用额度
32
+
33
+ <Image
34
+ alt={'注册 Novita AI'}
35
+ height={457}
36
+ inStep
37
+ src={'https://github.com/user-attachments/assets/f3177ce2-281c-4ed4-a061-239547b466c6'}
38
+ />
39
+
40
+ ### 步骤二:创建 API 密钥
41
+
42
+ - 访问 Novita AI 的 [密钥管理页面](https://novita.ai/dashboard/key) ,创建并且复制一个 API 密钥.
43
+
44
+ <Image
45
+ alt={'创建 Novita AI API 密钥'}
46
+ inStep
47
+ src={'https://github.com/user-attachments/assets/1e33aff2-6186-4e1f-80a8-4a2c855d8cc1'}
48
+ />
49
+
50
+ ### 步骤三:在 LobeChat 中配置 Novita AI
51
+
52
+ - 访问 LobeChat 的 `设置` 界面
53
+ - 在 `语言模型` 下找到 `novita.ai` 的设置项
54
+ - 打开 novita.ai 并填入获得的 API 密钥
55
+
56
+ <Image
57
+ alt={'在 LobeChat 中输入 Novita AI API 密钥'}
58
+ inStep
59
+ src={'https://github.com/user-attachments/assets/00c02637-873e-4e7e-9dc3-a95085b16dd7'}
60
+ />
61
+
62
+ - 为你的助手选择一个 Novita AI 模型即可开始对话
63
+
64
+ <Image
65
+ alt={'Select and use Novita AI model'}
66
+ inStep
67
+ src={'https://github.com/user-attachments/assets/6f9f400a-72e0-49de-94cb-5069fddf1163'}
68
+ />
69
+
70
+ <Callout type={'warning'}>
71
+ 在使用过程中你可能需要向 API 服务提供商付费,请参考 Novita AI 的相关费用政策。
72
+ </Callout>
73
+
74
+ </Steps>
75
+
76
+ 至此你已经可以在 LobeChat 中使用 Novita AI 提供的模型进行对话了。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.6.6",
3
+ "version": "1.6.8",
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",
@@ -11,6 +11,7 @@ import {
11
11
  Minimax,
12
12
  Mistral,
13
13
  Moonshot,
14
+ Novita,
14
15
  OpenRouter,
15
16
  Perplexity,
16
17
  Stepfun,
@@ -35,6 +36,7 @@ import {
35
36
  MinimaxProviderCard,
36
37
  MistralProviderCard,
37
38
  MoonshotProviderCard,
39
+ NovitaProviderCard,
38
40
  OpenRouterProviderCard,
39
41
  PerplexityProviderCard,
40
42
  QwenProviderCard,
@@ -126,6 +128,11 @@ export const useProviderList = (): ProviderItem[] => {
126
128
  docUrl: urlJoin(BASE_DOC_URL, 'openrouter'),
127
129
  title: <OpenRouter.Combine iconProps={{ color: OpenRouter.colorPrimary }} size={20} />,
128
130
  },
131
+ {
132
+ ...NovitaProviderCard,
133
+ docUrl: urlJoin(BASE_DOC_URL, 'novita'),
134
+ title: <Novita.Combine size={20} type={'color'} />,
135
+ },
129
136
  {
130
137
  ...TogetherAIProviderCard,
131
138
  docUrl: urlJoin(BASE_DOC_URL, 'togetherai'),
@@ -179,12 +186,12 @@ export const useProviderList = (): ProviderItem[] => {
179
186
  {
180
187
  ...BaichuanProviderCard,
181
188
  docUrl: urlJoin(BASE_DOC_URL, 'baichuan'),
182
- title: <Baichuan.Combine size={ 20 } type={ 'color' } />,
189
+ title: <Baichuan.Combine size={20} type={'color'} />,
183
190
  },
184
191
  {
185
192
  ...TaichuProviderCard,
186
193
  docUrl: urlJoin(BASE_DOC_URL, 'taichu'),
187
- title: <AiMass.Combine size={ 28 } type={ 'color' } />,
194
+ title: <AiMass.Combine size={28} type={'color'} />,
188
195
  },
189
196
  {
190
197
  ...Ai360ProviderCard,
@@ -172,6 +172,13 @@ const getLlmOptionsFromPayload = (provider: string, payload: JWTPayload) => {
172
172
 
173
173
  return { apiKey };
174
174
  }
175
+ case ModelProvider.Novita: {
176
+ const { NOVITA_API_KEY } = getLLMConfig();
177
+
178
+ const apiKey = apiKeyManager.pick(payload?.apiKey || NOVITA_API_KEY);
179
+
180
+ return { apiKey };
181
+ }
175
182
  case ModelProvider.Baichuan: {
176
183
  const { BAICHUAN_API_KEY } = getLLMConfig();
177
184
 
@@ -12,6 +12,7 @@ import {
12
12
  Minimax,
13
13
  Mistral,
14
14
  Moonshot,
15
+ Novita,
15
16
  Ollama,
16
17
  OpenAI,
17
18
  OpenRouter,
@@ -117,6 +118,10 @@ const ModelProviderIcon = memo<ModelProviderIconProps>(({ provider }) => {
117
118
  return <Stepfun size={20} />;
118
119
  }
119
120
 
121
+ case ModelProvider.Novita: {
122
+ return <Novita size={20} />;
123
+ }
124
+
120
125
  case ModelProvider.Baichuan: {
121
126
  return <Baichuan size={20} />;
122
127
  }
package/src/config/llm.ts CHANGED
@@ -76,6 +76,9 @@ export const getLLMConfig = () => {
76
76
  ENABLED_STEPFUN: z.boolean(),
77
77
  STEPFUN_API_KEY: z.string().optional(),
78
78
 
79
+ ENABLED_NOVITA: z.boolean(),
80
+ NOVITA_API_KEY: z.string().optional(),
81
+
79
82
  ENABLED_BAICHUAN: z.boolean(),
80
83
  BAICHUAN_API_KEY: z.string().optional(),
81
84
 
@@ -157,6 +160,9 @@ export const getLLMConfig = () => {
157
160
  ENABLED_STEPFUN: !!process.env.STEPFUN_API_KEY,
158
161
  STEPFUN_API_KEY: process.env.STEPFUN_API_KEY,
159
162
 
163
+ ENABLED_NOVITA: !!process.env.NOVITA_API_KEY,
164
+ NOVITA_API_KEY: process.env.NOVITA_API_KEY,
165
+
160
166
  ENABLED_BAICHUAN: !!process.env.BAICHUAN_API_KEY,
161
167
  BAICHUAN_API_KEY: process.env.BAICHUAN_API_KEY,
162
168
 
@@ -11,6 +11,7 @@ import GroqProvider from './groq';
11
11
  import MinimaxProvider from './minimax';
12
12
  import MistralProvider from './mistral';
13
13
  import MoonshotProvider from './moonshot';
14
+ import NovitaProvider from './novita';
14
15
  import OllamaProvider from './ollama';
15
16
  import OpenAIProvider from './openai';
16
17
  import OpenRouterProvider from './openrouter';
@@ -40,6 +41,7 @@ export const LOBE_DEFAULT_MODEL_LIST: ChatModelCard[] = [
40
41
  AnthropicProvider.chatModels,
41
42
  ZeroOneProvider.chatModels,
42
43
  StepfunProvider.chatModels,
44
+ NovitaProvider.chatModels,
43
45
  BaichuanProvider.chatModels,
44
46
  TaichuProvider.chatModels,
45
47
  Ai360Provider.chatModels,
@@ -64,6 +66,7 @@ export const DEFAULT_MODEL_PROVIDER_LIST = [
64
66
  ZeroOneProvider,
65
67
  ZhiPuProvider,
66
68
  StepfunProvider,
69
+ NovitaProvider,
67
70
  BaichuanProvider,
68
71
  TaichuProvider,
69
72
  Ai360Provider,
@@ -89,6 +92,7 @@ export { default as GroqProviderCard } from './groq';
89
92
  export { default as MinimaxProviderCard } from './minimax';
90
93
  export { default as MistralProviderCard } from './mistral';
91
94
  export { default as MoonshotProviderCard } from './moonshot';
95
+ export { default as NovitaProviderCard } from './novita';
92
96
  export { default as OllamaProviderCard } from './ollama';
93
97
  export { default as OpenAIProviderCard } from './openai';
94
98
  export { default as OpenRouterProviderCard } from './openrouter';
@@ -0,0 +1,91 @@
1
+ import { ModelProviderCard } from '@/types/llm';
2
+
3
+ const Novita: ModelProviderCard = {
4
+ chatModels: [
5
+ {
6
+ displayName: 'Llama3 8B Instruct',
7
+ enabled: true,
8
+ id: 'meta-llama/llama-3-8b-instruct',
9
+ tokens: 8192,
10
+ },
11
+ {
12
+ displayName: 'Llama3 70B Instruct',
13
+ enabled: true,
14
+ id: 'meta-llama/llama-3-70b-instruct',
15
+ tokens: 8192,
16
+ },
17
+ {
18
+ displayName: 'Nous Hermes 2 Pro - Llama3 8B',
19
+ enabled: true,
20
+ id: 'nousresearch/hermes-2-pro-llama-3-8b',
21
+ tokens: 8192,
22
+ },
23
+ {
24
+ displayName: 'Nous Hermes - Llama2 8B',
25
+ enabled: true,
26
+ id: 'nousresearch/nous-hermes-llama2-13b',
27
+ tokens: 4096,
28
+ },
29
+ {
30
+ displayName: 'Mistral 7B Instruct',
31
+ enabled: true,
32
+ id: 'mistralai/mistral-7b-instruct',
33
+ tokens: 32_768,
34
+ },
35
+ {
36
+ displayName: 'Dolphin Mixtral 8x22B',
37
+ enabled: true,
38
+ id: 'cognitivecomputations/dolphin-mixtral-8x22b',
39
+ tokens: 16_000,
40
+ },
41
+ {
42
+ displayName: 'L3-70b-Euryale-v2.1',
43
+ enabled: true,
44
+ id: 'sao10k/l3-70b-euryale-v2.1',
45
+ tokens: 16_000,
46
+ },
47
+ {
48
+ displayName: 'Midnight Rose 70B',
49
+ enabled: true,
50
+ id: 'sophosympatheia/midnight-rose-70b',
51
+ tokens: 4096,
52
+ },
53
+ {
54
+ displayName: 'Mythomax L2 13b',
55
+ enabled: true,
56
+ id: 'gryphe/mythomax-l2-13b',
57
+ tokens: 4096,
58
+ },
59
+ {
60
+ displayName: 'Nous Hermes 2 - Mixtral 8x7B-DPO',
61
+ enabled: true,
62
+ id: 'Nous-Hermes-2-Mixtral-8x7B-DPO',
63
+ tokens: 32_768,
64
+ },
65
+ {
66
+ displayName: 'Lzlv 70b',
67
+ enabled: true,
68
+ id: 'lzlv_70b',
69
+ tokens: 4096,
70
+ },
71
+ {
72
+ displayName: 'Open Hermes 2.5 Mistral 7B',
73
+ enabled: true,
74
+ id: 'teknium/openhermes-2.5-mistral-7b',
75
+ tokens: 4096,
76
+ },
77
+ {
78
+ displayName: 'Wizardlm2 8x22B',
79
+ enabled: true,
80
+ id: 'microsoft/wizardlm-2-8x22b',
81
+ tokens: 65_535,
82
+ },
83
+ ],
84
+ checkModel: 'meta-llama/llama-3-70b-instruct',
85
+ disableBrowserRequest: true,
86
+ id: 'novita',
87
+ modelList: { showModelFetcher: true },
88
+ name: 'Novita',
89
+ };
90
+
91
+ export default Novita;
@@ -9,6 +9,7 @@ import {
9
9
  MinimaxProviderCard,
10
10
  MistralProviderCard,
11
11
  MoonshotProviderCard,
12
+ NovitaProviderCard,
12
13
  OllamaProviderCard,
13
14
  OpenAIProviderCard,
14
15
  OpenRouterProviderCard,
@@ -68,6 +69,10 @@ export const DEFAULT_LLM_CONFIG: UserModelProviderConfig = {
68
69
  enabled: false,
69
70
  enabledModels: filterEnabledModels(MoonshotProviderCard),
70
71
  },
72
+ novita: {
73
+ enabled: false,
74
+ enabledModels: filterEnabledModels(NovitaProviderCard),
75
+ },
71
76
  ollama: {
72
77
  enabled: true,
73
78
  enabledModels: filterEnabledModels(OllamaProviderCard),
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
3
 
4
4
  import { INBOX_SESSION_ID } from '@/const/session';
5
5
  import { getTestDBInstance } from '@/database/server/core/dbForTest';
6
- import { KeyVaultsGateKeeper } from '@/server/keyVaultsEncrypt';
6
+ import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
7
7
  import { UserPreference } from '@/types/user';
8
8
  import { UserSettings } from '@/types/user/settings';
9
9
 
@@ -3,7 +3,7 @@ import { eq } from 'drizzle-orm';
3
3
  import { DeepPartial } from 'utility-types';
4
4
 
5
5
  import { serverDB } from '@/database/server/core/db';
6
- import { KeyVaultsGateKeeper } from '@/server/keyVaultsEncrypt';
6
+ import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
7
7
  import { UserPreference } from '@/types/user';
8
8
  import { UserSettings } from '@/types/user/settings';
9
9
  import { merge } from '@/utils/merge';
@@ -9,6 +9,7 @@ import {
9
9
  Minimax,
10
10
  Mistral,
11
11
  Moonshot,
12
+ Novita,
12
13
  OpenAI,
13
14
  OpenRouter,
14
15
  Perplexity,
@@ -94,6 +95,9 @@ const ProviderAvatar = memo<ProviderAvatarProps>(({ provider }) => {
94
95
  case ModelProvider.ZeroOne: {
95
96
  return <ZeroOne color={ZeroOne.colorPrimary} size={56} />;
96
97
  }
98
+ case ModelProvider.Novita: {
99
+ return <Novita color={Novita.colorPrimary} size={56} />;
100
+ }
97
101
 
98
102
  case ModelProvider.Ai360: {
99
103
  return <Ai360 color={Ai360.colorPrimary} size={56} />;
@@ -14,6 +14,7 @@ import { LobeGroq } from './groq';
14
14
  import { LobeMinimaxAI } from './minimax';
15
15
  import { LobeMistralAI } from './mistral';
16
16
  import { LobeMoonshotAI } from './moonshot';
17
+ import { LobeNovitaAI } from './novita';
17
18
  import { LobeOllamaAI } from './ollama';
18
19
  import { LobeOpenAI } from './openai';
19
20
  import { LobeOpenRouterAI } from './openrouter';
@@ -115,6 +116,7 @@ class AgentRuntime {
115
116
  minimax: Partial<ClientOptions>;
116
117
  mistral: Partial<ClientOptions>;
117
118
  moonshot: Partial<ClientOptions>;
119
+ novita: Partial<ClientOptions>;
118
120
  ollama: Partial<ClientOptions>;
119
121
  openai: Partial<ClientOptions>;
120
122
  openrouter: Partial<ClientOptions>;
@@ -226,8 +228,13 @@ class AgentRuntime {
226
228
  break;
227
229
  }
228
230
 
231
+ case ModelProvider.Novita: {
232
+ runtimeModel = new LobeNovitaAI(params.novita ?? {});
233
+ break;
234
+ }
235
+
229
236
  case ModelProvider.Baichuan: {
230
- runtimeModel = new LobeBaichuanAI(params.baichuan);
237
+ runtimeModel = new LobeBaichuanAI(params.baichuan ?? {});
231
238
  break;
232
239
  }
233
240
 
@@ -0,0 +1,251 @@
1
+ // @vitest-environment node
2
+ import OpenAI from 'openai';
3
+ import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { LobeOpenAICompatibleRuntime } from '@/libs/agent-runtime';
6
+ import { ModelProvider } from '@/libs/agent-runtime';
7
+ import { AgentRuntimeErrorType } from '@/libs/agent-runtime';
8
+
9
+ import * as debugStreamModule from '../utils/debugStream';
10
+ import { LobeNovitaAI } from './index';
11
+
12
+ const provider = ModelProvider.Novita;
13
+ const defaultBaseURL = 'https://api.novita.ai/v3/openai';
14
+ const bizErrorType = AgentRuntimeErrorType.ProviderBizError;
15
+ const invalidErrorType = AgentRuntimeErrorType.InvalidProviderAPIKey;
16
+
17
+ // Mock the console.error to avoid polluting test output
18
+ vi.spyOn(console, 'error').mockImplementation(() => {});
19
+
20
+ let instance: LobeOpenAICompatibleRuntime;
21
+
22
+ beforeEach(() => {
23
+ instance = new LobeNovitaAI({ apiKey: 'test' });
24
+
25
+ // 使用 vi.spyOn 来模拟 chat.completions.create 方法
26
+ vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
27
+ new ReadableStream() as any,
28
+ );
29
+ });
30
+
31
+ afterEach(() => {
32
+ vi.clearAllMocks();
33
+ });
34
+
35
+ describe('NovitaAI', () => {
36
+ describe('init', () => {
37
+ it('should correctly initialize with an API key', async () => {
38
+ const instance = new LobeNovitaAI({ apiKey: 'test_api_key' });
39
+ expect(instance).toBeInstanceOf(LobeNovitaAI);
40
+ expect(instance.baseURL).toEqual(defaultBaseURL);
41
+ });
42
+ });
43
+
44
+ describe('chat', () => {
45
+ describe('Error', () => {
46
+ it('should return Error with an openai error response when OpenAI.APIError is thrown', async () => {
47
+ // Arrange
48
+ const apiError = new OpenAI.APIError(
49
+ 400,
50
+ {
51
+ status: 400,
52
+ error: {
53
+ message: 'Bad Request',
54
+ },
55
+ },
56
+ 'Error message',
57
+ {},
58
+ );
59
+
60
+ vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
61
+
62
+ // Act
63
+ try {
64
+ await instance.chat({
65
+ messages: [{ content: 'Hello', role: 'user' }],
66
+ model: 'meta-llama/llama-3-8b-instruct',
67
+ temperature: 0.999,
68
+ });
69
+ } catch (e) {
70
+ expect(e).toEqual({
71
+ endpoint: defaultBaseURL,
72
+ error: {
73
+ error: { message: 'Bad Request' },
74
+ status: 400,
75
+ },
76
+ errorType: bizErrorType,
77
+ provider,
78
+ });
79
+ }
80
+ });
81
+
82
+ it('should throw AgentRuntimeError if no apiKey is provided', async () => {
83
+ try {
84
+ new LobeNovitaAI({});
85
+ } catch (e) {
86
+ expect(e).toEqual({ errorType: invalidErrorType });
87
+ }
88
+ });
89
+
90
+ it('should return Error with the cause when OpenAI.APIError is thrown with cause', async () => {
91
+ // Arrange
92
+ const errorInfo = {
93
+ stack: 'abc',
94
+ cause: {
95
+ message: 'api is undefined',
96
+ },
97
+ };
98
+ const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
99
+
100
+ vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
101
+
102
+ // Act
103
+ try {
104
+ await instance.chat({
105
+ messages: [{ content: 'Hello', role: 'user' }],
106
+ model: 'meta-llama/llama-3-8b-instruct',
107
+ temperature: 0.999,
108
+ });
109
+ } catch (e) {
110
+ expect(e).toEqual({
111
+ endpoint: defaultBaseURL,
112
+ error: {
113
+ cause: { message: 'api is undefined' },
114
+ stack: 'abc',
115
+ },
116
+ errorType: bizErrorType,
117
+ provider,
118
+ });
119
+ }
120
+ });
121
+
122
+ it('should return Error with an cause response with desensitize Url', async () => {
123
+ // Arrange
124
+ const errorInfo = {
125
+ stack: 'abc',
126
+ cause: { message: 'api is undefined' },
127
+ };
128
+ const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
129
+
130
+ instance = new LobeNovitaAI({
131
+ apiKey: 'test',
132
+
133
+ baseURL: 'https://api.abc.com/v1',
134
+ });
135
+
136
+ vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);
137
+
138
+ // Act
139
+ try {
140
+ await instance.chat({
141
+ messages: [{ content: 'Hello', role: 'user' }],
142
+ model: 'meta-llama/llama-3-8b-instruct',
143
+ temperature: 0.999,
144
+ });
145
+ } catch (e) {
146
+ expect(e).toEqual({
147
+ endpoint: 'https://api.***.com/v1',
148
+ error: {
149
+ cause: { message: 'api is undefined' },
150
+ stack: 'abc',
151
+ },
152
+ errorType: bizErrorType,
153
+ provider,
154
+ });
155
+ }
156
+ });
157
+
158
+ it('should throw an error type on 401 status code', async () => {
159
+ // Mock the API call to simulate a 401 error
160
+ const error = new Error('InvalidApiKey') as any;
161
+ error.status = 401;
162
+ vi.mocked(instance['client'].chat.completions.create).mockRejectedValue(error);
163
+
164
+ try {
165
+ await instance.chat({
166
+ messages: [{ content: 'Hello', role: 'user' }],
167
+ model: 'meta-llama/llama-3-8b-instruct',
168
+ temperature: 0.999,
169
+ });
170
+ } catch (e) {
171
+ expect(e).toEqual({
172
+ endpoint: defaultBaseURL,
173
+ error: new Error('InvalidApiKey'),
174
+ errorType: invalidErrorType,
175
+ provider,
176
+ });
177
+ }
178
+ });
179
+
180
+ it('should return AgentRuntimeError for non-OpenAI errors', async () => {
181
+ // Arrange
182
+ const genericError = new Error('Generic Error');
183
+
184
+ vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(genericError);
185
+
186
+ // Act
187
+ try {
188
+ await instance.chat({
189
+ messages: [{ content: 'Hello', role: 'user' }],
190
+ model: 'meta-llama/llama-3-8b-instruct',
191
+ temperature: 0.999,
192
+ });
193
+ } catch (e) {
194
+ expect(e).toEqual({
195
+ endpoint: defaultBaseURL,
196
+ errorType: 'AgentRuntimeError',
197
+ provider,
198
+ error: {
199
+ name: genericError.name,
200
+ cause: genericError.cause,
201
+ message: genericError.message,
202
+ stack: genericError.stack,
203
+ },
204
+ });
205
+ }
206
+ });
207
+ });
208
+
209
+ describe('DEBUG', () => {
210
+ it('should call debugStream and return StreamingTextResponse when DEBUG_NOVITA_CHAT_COMPLETION is 1', async () => {
211
+ // Arrange
212
+ const mockProdStream = new ReadableStream() as any; // 模拟的 prod 流
213
+ const mockDebugStream = new ReadableStream({
214
+ start(controller) {
215
+ controller.enqueue('Debug stream content');
216
+ controller.close();
217
+ },
218
+ }) as any;
219
+ mockDebugStream.toReadableStream = () => mockDebugStream; // 添加 toReadableStream 方法
220
+
221
+ // 模拟 chat.completions.create 返回值,包括模拟的 tee 方法
222
+ (instance['client'].chat.completions.create as Mock).mockResolvedValue({
223
+ tee: () => [mockProdStream, { toReadableStream: () => mockDebugStream }],
224
+ });
225
+
226
+ // 保存原始环境变量值
227
+ const originalDebugValue = process.env.DEBUG_NOVITA_CHAT_COMPLETION;
228
+
229
+ // 模拟环境变量
230
+ process.env.DEBUG_NOVITA_CHAT_COMPLETION = '1';
231
+ vi.spyOn(debugStreamModule, 'debugStream').mockImplementation(() => Promise.resolve());
232
+
233
+ // 执行测试
234
+ // 运行你的测试函数,确保它会在条件满足时调用 debugStream
235
+ // 假设的测试函数调用,你可能需要根据实际情况调整
236
+ await instance.chat({
237
+ messages: [{ content: 'Hello', role: 'user' }],
238
+ model: 'meta-llama/llama-3-8b-instruct',
239
+ stream: true,
240
+ temperature: 0.999,
241
+ });
242
+
243
+ // 验证 debugStream 被调用
244
+ expect(debugStreamModule.debugStream).toHaveBeenCalled();
245
+
246
+ // 恢复原始环境变量值
247
+ process.env.DEBUG_NOVITA_CHAT_COMPLETION = originalDebugValue;
248
+ });
249
+ });
250
+ });
251
+ });
@@ -0,0 +1,15 @@
1
+ import { ModelProvider } from '../types';
2
+ import { LobeOpenAICompatibleFactory } from '../utils/openaiCompatibleFactory';
3
+
4
+ export const LobeNovitaAI = LobeOpenAICompatibleFactory({
5
+ baseURL: 'https://api.novita.ai/v3/openai',
6
+ constructorOptions: {
7
+ defaultHeaders: {
8
+ 'X-Novita-Source': 'lobechat',
9
+ },
10
+ },
11
+ debug: {
12
+ chatCompletion: () => process.env.DEBUG_NOVITA_CHAT_COMPLETION === '1',
13
+ },
14
+ provider: ModelProvider.Novita,
15
+ });
@@ -33,6 +33,7 @@ export enum ModelProvider {
33
33
  Minimax = 'minimax',
34
34
  Mistral = 'mistral',
35
35
  Moonshot = 'moonshot',
36
+ Novita = 'novita',
36
37
  Ollama = 'ollama',
37
38
  OpenAI = 'openai',
38
39
  OpenRouter = 'openrouter',
@@ -32,6 +32,7 @@ export const getServerGlobalConfig = () => {
32
32
  ENABLED_ANTHROPIC,
33
33
  ENABLED_MINIMAX,
34
34
  ENABLED_MISTRAL,
35
+ ENABLED_NOVITA,
35
36
  ENABLED_QWEN,
36
37
  ENABLED_STEPFUN,
37
38
  ENABLED_BAICHUAN,
@@ -82,6 +83,7 @@ export const getServerGlobalConfig = () => {
82
83
  minimax: { enabled: ENABLED_MINIMAX },
83
84
  mistral: { enabled: ENABLED_MISTRAL },
84
85
  moonshot: { enabled: ENABLED_MOONSHOT },
86
+ novita: { enabled: ENABLED_NOVITA },
85
87
  ollama: {
86
88
  enabled: ENABLED_OLLAMA,
87
89
  fetchOnClient: !OLLAMA_PROXY_URL,
@@ -93,19 +93,25 @@ exports[`configRouter > getGlobalConfig > Model Provider env > OPENROUTER_MODEL_
93
93
  {
94
94
  "enabled": false,
95
95
  "enabledModels": [
96
- "google/gemma-7b-it",
97
- "mistralai/mistral-7b-instruct",
96
+ "google/gemma-7b-it:free",
97
+ "mistralai/mistral-7b-instruct:free",
98
98
  ],
99
99
  "serverModelCards": [
100
100
  {
101
- "displayName": "google/gemma-7b-it",
101
+ "displayName": "Google: Gemma 7B (free)",
102
102
  "enabled": true,
103
- "id": "google/gemma-7b-it",
103
+ "functionCall": false,
104
+ "id": "google/gemma-7b-it:free",
105
+ "tokens": 8192,
106
+ "vision": false,
104
107
  },
105
108
  {
106
- "displayName": "Mistral-7B-Instruct",
109
+ "displayName": "Mistral 7B Instruct (free)",
107
110
  "enabled": true,
108
- "id": "mistralai/mistral-7b-instruct",
111
+ "functionCall": false,
112
+ "id": "mistralai/mistral-7b-instruct:free",
113
+ "tokens": 32768,
114
+ "vision": false,
109
115
  },
110
116
  ],
111
117
  }
@@ -130,7 +130,7 @@ describe('configRouter', () => {
130
130
  describe('OPENROUTER_MODEL_LIST', () => {
131
131
  it('custom deletion, addition, and renaming of models', async () => {
132
132
  process.env.OPENROUTER_MODEL_LIST =
133
- '-all,+google/gemma-7b-it,+mistralai/mistral-7b-instruct=Mistral-7B-Instruct';
133
+ '-all,+google/gemma-7b-it:free,+mistralai/mistral-7b-instruct:free';
134
134
 
135
135
  const response = await router.getGlobalConfig();
136
136
 
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
 
3
3
  import { passwordProcedure, router } from '@/libs/trpc';
4
- import { S3 } from '@/server/files/s3';
4
+ import { S3 } from '@/server/modules/S3';
5
5
 
6
6
  export const uploadRouter = router({
7
7
  createS3PreSignedUrl: passwordProcedure
@@ -2,12 +2,21 @@ import { TRPCError } from '@trpc/server';
2
2
  import { z } from 'zod';
3
3
 
4
4
  import { authedProcedure, router } from '@/libs/trpc';
5
- import { S3 } from '@/server/files/s3';
6
- import { DataImporter } from '@/server/modules/DataImporter';
5
+ import { S3 } from '@/server/modules/S3';
6
+ import { DataImporterService } from '@/server/services/dataImporter';
7
7
  import { ImportResults, ImporterEntryData } from '@/types/importer';
8
8
 
9
+ const importProcedure = authedProcedure.use(async (opts) => {
10
+ const { ctx } = opts;
11
+ const dataImporterService = new DataImporterService(ctx.userId);
12
+
13
+ return opts.next({
14
+ ctx: { dataImporterService },
15
+ });
16
+ });
17
+
9
18
  export const importerRouter = router({
10
- importByFile: authedProcedure
19
+ importByFile: importProcedure
11
20
  .input(z.object({ pathname: z.string() }))
12
21
  .mutation(async ({ input, ctx }): Promise<ImportResults> => {
13
22
  let data: ImporterEntryData | undefined;
@@ -27,12 +36,10 @@ export const importerRouter = router({
27
36
  });
28
37
  }
29
38
 
30
- const dataImporter = new DataImporter(ctx.userId);
31
-
32
- return dataImporter.importData(data);
39
+ return ctx.dataImporterService.importData(data);
33
40
  }),
34
41
 
35
- importByPost: authedProcedure
42
+ importByPost: importProcedure
36
43
  .input(
37
44
  z.object({
38
45
  data: z.object({
@@ -45,8 +52,6 @@ export const importerRouter = router({
45
52
  }),
46
53
  )
47
54
  .mutation(async ({ input, ctx }): Promise<ImportResults> => {
48
- const dataImporter = new DataImporter(ctx.userId);
49
-
50
- return dataImporter.importData(input.data);
55
+ return ctx.dataImporterService.importData(input.data);
51
56
  }),
52
57
  });
@@ -16,7 +16,7 @@ import { CURRENT_CONFIG_VERSION } from '@/migrations';
16
16
  import { ImportResult } from '@/services/config';
17
17
  import { ImporterEntryData } from '@/types/importer';
18
18
 
19
- import { DataImporter } from '../index';
19
+ import { DataImporterService } from '../index';
20
20
  import mockImportData from './fixtures/messages.json';
21
21
 
22
22
  let serverDB = await getTestDBInstance();
@@ -28,7 +28,7 @@ vi.mock('@/database/server/core/db', async () => ({
28
28
  }));
29
29
 
30
30
  const userId = 'test-user-id';
31
- let importer: DataImporter;
31
+ let importer: DataImporterService;
32
32
 
33
33
  beforeEach(async () => {
34
34
  await serverDB.delete(users);
@@ -38,7 +38,7 @@ beforeEach(async () => {
38
38
  await tx.insert(users).values({ id: userId });
39
39
  });
40
40
 
41
- importer = new DataImporter(userId);
41
+ importer = new DataImporterService(userId);
42
42
  });
43
43
 
44
44
  describe('DataImporter', () => {
@@ -15,7 +15,7 @@ import {
15
15
  import { ImportResult } from '@/services/config';
16
16
  import { ImporterEntryData } from '@/types/importer';
17
17
 
18
- export class DataImporter {
18
+ export class DataImporterService {
19
19
  private userId: string;
20
20
 
21
21
  /**
@@ -28,6 +28,7 @@ export interface UserKeyVaults {
28
28
  minimax?: OpenAICompatibleKeyVault;
29
29
  mistral?: OpenAICompatibleKeyVault;
30
30
  moonshot?: OpenAICompatibleKeyVault;
31
+ novita?: OpenAICompatibleKeyVault;
31
32
  ollama?: OpenAICompatibleKeyVault;
32
33
  openai?: OpenAICompatibleKeyVault;
33
34
  openrouter?: OpenAICompatibleKeyVault;
package/vitest.config.ts CHANGED
@@ -30,7 +30,7 @@ export default defineConfig({
30
30
  '**/dist/**',
31
31
  '**/build/**',
32
32
  'src/database/server/**/**',
33
- 'src/server/modules/**/**',
33
+ 'src/server/services/**/**',
34
34
  ],
35
35
  globals: true,
36
36
  server: {
File without changes