@lobehub/chat 0.138.2 → 0.139.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 (80) hide show
  1. package/.env.example +6 -0
  2. package/CHANGELOG.md +25 -0
  3. package/Dockerfile +3 -0
  4. package/docs/self-hosting/environment-variables/model-provider.mdx +14 -4
  5. package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +8 -0
  6. package/locales/ar/common.json +2 -1
  7. package/locales/ar/error.json +7 -1
  8. package/locales/ar/setting.json +16 -0
  9. package/locales/de-DE/common.json +2 -1
  10. package/locales/de-DE/error.json +7 -1
  11. package/locales/de-DE/setting.json +16 -0
  12. package/locales/en-US/common.json +2 -1
  13. package/locales/en-US/error.json +7 -1
  14. package/locales/en-US/setting.json +16 -0
  15. package/locales/es-ES/common.json +2 -1
  16. package/locales/es-ES/error.json +7 -1
  17. package/locales/es-ES/setting.json +16 -0
  18. package/locales/fr-FR/common.json +2 -1
  19. package/locales/fr-FR/error.json +7 -1
  20. package/locales/fr-FR/setting.json +16 -0
  21. package/locales/it-IT/common.json +2 -1
  22. package/locales/it-IT/error.json +7 -1
  23. package/locales/it-IT/setting.json +16 -0
  24. package/locales/ja-JP/common.json +2 -1
  25. package/locales/ja-JP/error.json +7 -1
  26. package/locales/ja-JP/setting.json +16 -0
  27. package/locales/ko-KR/common.json +2 -1
  28. package/locales/ko-KR/error.json +7 -1
  29. package/locales/ko-KR/setting.json +16 -0
  30. package/locales/nl-NL/common.json +2 -1
  31. package/locales/nl-NL/error.json +7 -1
  32. package/locales/nl-NL/setting.json +16 -0
  33. package/locales/pl-PL/common.json +2 -1
  34. package/locales/pl-PL/error.json +7 -1
  35. package/locales/pl-PL/setting.json +16 -0
  36. package/locales/pt-BR/common.json +2 -1
  37. package/locales/pt-BR/error.json +7 -1
  38. package/locales/pt-BR/setting.json +16 -0
  39. package/locales/ru-RU/common.json +2 -1
  40. package/locales/ru-RU/error.json +7 -1
  41. package/locales/ru-RU/setting.json +16 -0
  42. package/locales/tr-TR/common.json +2 -1
  43. package/locales/tr-TR/error.json +7 -1
  44. package/locales/tr-TR/setting.json +16 -0
  45. package/locales/vi-VN/common.json +2 -1
  46. package/locales/vi-VN/error.json +7 -1
  47. package/locales/vi-VN/setting.json +16 -0
  48. package/locales/zh-CN/common.json +2 -1
  49. package/locales/zh-CN/error.json +7 -1
  50. package/locales/zh-CN/setting.json +16 -0
  51. package/locales/zh-TW/common.json +2 -1
  52. package/locales/zh-TW/error.json +7 -1
  53. package/locales/zh-TW/setting.json +16 -0
  54. package/package.json +1 -1
  55. package/src/app/api/chat/[provider]/agentRuntime.test.ts +26 -0
  56. package/src/app/api/chat/[provider]/agentRuntime.ts +14 -0
  57. package/src/app/api/config/route.ts +2 -0
  58. package/src/app/api/errorResponse.test.ts +7 -0
  59. package/src/app/api/errorResponse.ts +3 -0
  60. package/src/app/settings/llm/OpenRouter/index.tsx +64 -0
  61. package/src/app/settings/llm/index.tsx +2 -0
  62. package/src/config/modelProviders/index.ts +3 -0
  63. package/src/config/modelProviders/openrouter.ts +44 -0
  64. package/src/config/server/provider.ts +8 -0
  65. package/src/const/settings.ts +4 -0
  66. package/src/features/Conversation/Error/APIKeyForm/OpenRouter.tsx +40 -0
  67. package/src/features/Conversation/Error/APIKeyForm/index.tsx +5 -0
  68. package/src/features/Conversation/Error/index.tsx +1 -0
  69. package/src/libs/agent-runtime/error.ts +3 -0
  70. package/src/libs/agent-runtime/index.ts +1 -0
  71. package/src/libs/agent-runtime/openrouter/index.test.ts +347 -0
  72. package/src/libs/agent-runtime/openrouter/index.ts +86 -0
  73. package/src/libs/agent-runtime/types/type.ts +1 -0
  74. package/src/locales/default/common.ts +1 -0
  75. package/src/locales/default/error.ts +7 -0
  76. package/src/locales/default/setting.ts +16 -0
  77. package/src/services/_auth.test.ts +10 -0
  78. package/src/services/_auth.ts +4 -0
  79. package/src/store/global/slices/settings/selectors/modelProvider.ts +14 -0
  80. package/src/types/settings/modelProvider.ts +7 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.138.2",
3
+ "version": "0.139.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",
@@ -16,6 +16,7 @@ import {
16
16
  LobeMoonshotAI,
17
17
  LobeOllamaAI,
18
18
  LobeOpenAI,
19
+ LobeOpenRouterAI,
19
20
  LobePerplexityAI,
20
21
  LobeZhipuAI,
21
22
  ModelProvider,
@@ -42,6 +43,7 @@ vi.mock('@/config/server', () => ({
42
43
  PERPLEXITY_API_KEY: 'test-perplexity-key',
43
44
  ANTHROPIC_API_KEY: 'test-anthropic-key',
44
45
  MISTRAL_API_KEY: 'test-mistral-key',
46
+ OPENROUTER_API_KEY: 'test-openrouter-key',
45
47
  })),
46
48
  }));
47
49
 
@@ -313,6 +315,30 @@ describe('AgentRuntime', () => {
313
315
  });
314
316
  });
315
317
 
318
+ describe('OpenRouter AI provider', () => {
319
+ it('should initialize correctly', async () => {
320
+ const jwtPayload: JWTPayload = { apiKey: 'user-openrouter-key' };
321
+ const runtime = await AgentRuntime.initializeWithUserPayload(
322
+ ModelProvider.OpenRouter,
323
+ jwtPayload,
324
+ );
325
+
326
+ // 假设 LobeOpenRouterAI 是 OpenRouter 提供者的实现类
327
+ expect(runtime['_runtime']).toBeInstanceOf(LobeOpenRouterAI);
328
+ });
329
+
330
+ it('should initialize correctly without apiKey', async () => {
331
+ const jwtPayload: JWTPayload = {};
332
+ const runtime = await AgentRuntime.initializeWithUserPayload(
333
+ ModelProvider.OpenRouter,
334
+ jwtPayload,
335
+ );
336
+
337
+ // 假设 LobeOpenRouterAI 是 OpenRouter 提供者的实现类
338
+ expect(runtime['_runtime']).toBeInstanceOf(LobeOpenRouterAI);
339
+ });
340
+ });
341
+
316
342
  it('should handle unknown provider gracefully', async () => {
317
343
  const jwtPayload: JWTPayload = {};
318
344
  const runtime = await AgentRuntime.initializeWithUserPayload('unknown', jwtPayload);
@@ -18,6 +18,7 @@ import {
18
18
  LobeMoonshotAI,
19
19
  LobeOllamaAI,
20
20
  LobeOpenAI,
21
+ LobeOpenRouterAI,
21
22
  LobePerplexityAI,
22
23
  LobeRuntimeAI,
23
24
  LobeZhipuAI,
@@ -173,6 +174,11 @@ class AgentRuntime {
173
174
  runtimeModel = this.initGroq(payload);
174
175
  break;
175
176
  }
177
+
178
+ case ModelProvider.OpenRouter: {
179
+ runtimeModel = this.initOpenRouter(payload);
180
+ break;
181
+ }
176
182
  }
177
183
 
178
184
  return new AgentRuntime(runtimeModel);
@@ -281,6 +287,14 @@ class AgentRuntime {
281
287
 
282
288
  return new LobeGroq({ apiKey });
283
289
  }
290
+
291
+ private static initOpenRouter(payload: JWTPayload) {
292
+ const { OPENROUTER_API_KEY } = getServerConfig();
293
+ const apiKey = apiKeyManager.pick(payload?.apiKey || OPENROUTER_API_KEY);
294
+
295
+ return new LobeOpenRouterAI({ apiKey });
296
+ }
297
+
284
298
  }
285
299
 
286
300
  export default AgentRuntime;
@@ -22,6 +22,7 @@ export const GET = async () => {
22
22
  ENABLED_PERPLEXITY,
23
23
  ENABLED_ANTHROPIC,
24
24
  ENABLED_MISTRAL,
25
+ ENABLED_OPENROUTER,
25
26
  DEFAULT_AGENT_CONFIG,
26
27
  OLLAMA_CUSTOM_MODELS,
27
28
  } = getServerConfig();
@@ -41,6 +42,7 @@ export const GET = async () => {
41
42
  mistral: { enabled: ENABLED_MISTRAL },
42
43
  moonshot: { enabled: ENABLED_MOONSHOT },
43
44
  ollama: { customModelName: OLLAMA_CUSTOM_MODELS, enabled: ENABLE_OLLAMA },
45
+ openrouter: { enabled: ENABLED_OPENROUTER },
44
46
  perplexity: { enabled: ENABLED_PERPLEXITY },
45
47
  zhipu: { enabled: ENABLED_ZHIPU },
46
48
  },
@@ -86,6 +86,13 @@ describe('createErrorResponse', () => {
86
86
  expect(response.status).toBe(476);
87
87
  });
88
88
 
89
+ // 测试 OpenRouterBizError 错误类型返回477状态码
90
+ it('returns a 477 status for OpenRouterBizError error type', () => {
91
+ const errorType = AgentRuntimeErrorType.OpenRouterBizError;
92
+ const response = createErrorResponse(errorType);
93
+ expect(response.status).toBe(477);
94
+ });
95
+
89
96
  // 测试 OllamaBizError 错误类型返回478状态码
90
97
  it('returns a 478 status for OllamaBizError error type', () => {
91
98
  const errorType = AgentRuntimeErrorType.OllamaBizError;
@@ -37,6 +37,9 @@ const getStatus = (errorType: ILobeAgentRuntimeErrorType | ErrorType) => {
37
37
  case AgentRuntimeErrorType.MoonshotBizError: {
38
38
  return 476;
39
39
  }
40
+ case AgentRuntimeErrorType.OpenRouterBizError: {
41
+ return 477;
42
+ }
40
43
  case ChatErrorType.OllamaServiceUnavailable:
41
44
  case AgentRuntimeErrorType.OllamaBizError: {
42
45
  return 478;
@@ -0,0 +1,64 @@
1
+ import { OpenRouter } from '@lobehub/icons';
2
+ import { Input } from 'antd';
3
+ import { useTheme } from 'antd-style';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import { ModelProvider } from '@/libs/agent-runtime';
8
+
9
+ import Checker from '../components/Checker';
10
+ import ProviderConfig from '../components/ProviderConfig';
11
+ import { LLMProviderApiTokenKey, LLMProviderConfigKey } from '../const';
12
+
13
+ const providerKey = 'openrouter';
14
+
15
+ const OpenRouterProvider = memo(() => {
16
+ const { t } = useTranslation('setting');
17
+
18
+ const theme = useTheme();
19
+
20
+ return (
21
+ <ProviderConfig
22
+ configItems={[
23
+ {
24
+ children: (
25
+ <Input.Password
26
+ autoComplete={'new-password'}
27
+ placeholder={t('llm.OpenRouter.token.placeholder')}
28
+ />
29
+ ),
30
+ desc: t('llm.OpenRouter.token.desc'),
31
+ label: t('llm.OpenRouter.token.title'),
32
+ name: [LLMProviderConfigKey, providerKey, LLMProviderApiTokenKey],
33
+ },
34
+ {
35
+ children: (
36
+ <Input.TextArea
37
+ allowClear
38
+ placeholder={t('llm.OpenRouter.customModelName.placeholder')}
39
+ style={{ height: 100 }}
40
+ />
41
+ ),
42
+ desc: t('llm.OpenRouter.customModelName.desc'),
43
+ label: t('llm.OpenRouter.customModelName.title'),
44
+ name: [LLMProviderConfigKey, providerKey, 'customModelName'],
45
+ },
46
+ {
47
+ children: <Checker model={'mistralai/mistral-7b-instruct:free'} provider={ModelProvider.OpenRouter} />,
48
+ desc: t('llm.checker.desc'),
49
+ label: t('llm.checker.title'),
50
+ minWidth: '100%',
51
+ },
52
+ ]}
53
+ provider={providerKey}
54
+ title={
55
+ <OpenRouter.Combine
56
+ color={theme.isDarkMode ? theme.colorText : OpenRouter.colorPrimary}
57
+ size={24}
58
+ />
59
+ }
60
+ />
61
+ );
62
+ });
63
+
64
+ export default OpenRouterProvider;
@@ -16,6 +16,7 @@ import Mistral from './Mistral';
16
16
  import Moonshot from './Moonshot';
17
17
  import Ollama from './Ollama';
18
18
  import OpenAI from './OpenAI';
19
+ import OpenRouter from './OpenRouter';
19
20
  import Perplexity from './Perplexity';
20
21
  import Zhipu from './Zhipu';
21
22
 
@@ -34,6 +35,7 @@ export default memo<{ showOllama: boolean }>(({ showOllama }) => {
34
35
  <Bedrock />
35
36
  <Perplexity />
36
37
  <Mistral />
38
+ <OpenRouter />
37
39
  <Moonshot />
38
40
  <Zhipu />
39
41
  <Footer>
@@ -8,6 +8,7 @@ import MistralProvider from './mistral';
8
8
  import MoonshotProvider from './moonshot';
9
9
  import OllamaProvider from './ollama';
10
10
  import OpenAIProvider from './openai';
11
+ import OpenRouterProvider from './openrouter';
11
12
  import PerplexityProvider from './perplexity';
12
13
  import ZhiPuProvider from './zhipu';
13
14
 
@@ -20,6 +21,7 @@ export const LOBE_DEFAULT_MODEL_LIST: ChatModelCard[] = [
20
21
  MistralProvider.chatModels,
21
22
  MoonshotProvider.chatModels,
22
23
  OllamaProvider.chatModels,
24
+ OpenRouterProvider.chatModels,
23
25
  PerplexityProvider.chatModels,
24
26
  AnthropicProvider.chatModels,
25
27
  ].flat();
@@ -32,5 +34,6 @@ export { default as MistralProvider } from './mistral';
32
34
  export { default as MoonshotProvider } from './moonshot';
33
35
  export { default as OllamaProvider } from './ollama';
34
36
  export { default as OpenAIProvider } from './openai';
37
+ export { default as OpenRouterProvider } from './openrouter';
35
38
  export { default as PerplexityProvider } from './perplexity';
36
39
  export { default as ZhiPuProvider } from './zhipu';
@@ -0,0 +1,44 @@
1
+ import { ModelProviderCard } from '@/types/llm';
2
+
3
+ const OpenRouter: ModelProviderCard = {
4
+ chatModels: [
5
+ {
6
+ displayName: 'Mistral 7B Instruct (free)',
7
+ functionCall: false,
8
+ id: 'mistralai/mistral-7b-instruct:free',
9
+ tokens: 32_768,
10
+ vision: false,
11
+ },
12
+ {
13
+ displayName: 'Google: Gemma 7B (free)',
14
+ functionCall: false,
15
+ id: 'google/gemma-7b-it:free',
16
+ tokens: 8192,
17
+ vision: false,
18
+ },
19
+ {
20
+ displayName: 'OpenChat 3.5 (free)',
21
+ functionCall: false,
22
+ id: 'openchat/openchat-7b:free',
23
+ tokens: 8192,
24
+ vision: false,
25
+ },
26
+ {
27
+ displayName: 'Nous: Capybara 7B (free)',
28
+ functionCall: false,
29
+ id: 'nousresearch/nous-capybara-7b:free',
30
+ tokens: 4096,
31
+ vision: false,
32
+ },
33
+ {
34
+ displayName: 'Hugging Face: Zephyr 7B (free)',
35
+ functionCall: false,
36
+ id: 'huggingfaceh4/zephyr-7b-beta:free',
37
+ tokens: 4096,
38
+ vision: false,
39
+ }
40
+ ],
41
+ id: 'openrouter',
42
+ };
43
+
44
+ export default OpenRouter;
@@ -42,6 +42,9 @@ declare global {
42
42
 
43
43
  // Groq Provider
44
44
  GROQ_API_KEY?: string;
45
+
46
+ // OpenRouter Provider
47
+ OPENROUTER_API_KEY?: string;
45
48
 
46
49
  // AWS Credentials
47
50
  AWS_REGION?: string;
@@ -74,6 +77,8 @@ export const getProviderConfig = () => {
74
77
  const MISTRAL_API_KEY = process.env.MISTRAL_API_KEY || '';
75
78
 
76
79
  const GROQ_API_KEY = process.env.GROQ_API_KEY || '';
80
+
81
+ const OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY || '';
77
82
 
78
83
  // region format: iad1,sfo1
79
84
  let regions: string[] = [];
@@ -106,6 +111,9 @@ export const getProviderConfig = () => {
106
111
  ENABLED_MISTRAL: !!MISTRAL_API_KEY,
107
112
  MISTRAL_API_KEY,
108
113
 
114
+ ENABLED_OPENROUTER: !!OPENROUTER_API_KEY,
115
+ OPENROUTER_API_KEY,
116
+
109
117
  ENABLED_MOONSHOT: !!MOONSHOT_API_KEY,
110
118
  MOONSHOT_API_KEY,
111
119
  MOONSHOT_PROXY_URL: process.env.MOONSHOT_PROXY_URL,
@@ -87,6 +87,10 @@ export const DEFAULT_LLM_CONFIG: GlobalLLMConfig = {
87
87
  enabled: true,
88
88
  models: [],
89
89
  },
90
+ openrouter: {
91
+ apiKey: '',
92
+ enabled: false,
93
+ },
90
94
  perplexity: {
91
95
  apiKey: '',
92
96
  enabled: false,
@@ -0,0 +1,40 @@
1
+ import { OpenRouter } from '@lobehub/icons';
2
+ import { Input } from 'antd';
3
+ import { memo } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ import { ModelProvider } from '@/libs/agent-runtime';
7
+ import { useGlobalStore } from '@/store/global';
8
+ import { modelProviderSelectors } from '@/store/global/selectors';
9
+
10
+ import { FormAction } from '../style';
11
+
12
+ const OpenRouterForm = memo(() => {
13
+ const { t } = useTranslation('error');
14
+ // const [showProxy, setShow] = useState(false);
15
+
16
+ const [apiKey, setConfig] = useGlobalStore((s) => [
17
+ modelProviderSelectors.openrouterAPIKey(s),
18
+ s.setModelProviderConfig,
19
+ ]);
20
+
21
+ return (
22
+ <FormAction
23
+ avatar={<OpenRouter size={56} />}
24
+ description={t('unlock.apikey.OpenRouter.description')}
25
+ title={t('unlock.apikey.OpenRouter.title')}
26
+ >
27
+ <Input.Password
28
+ autoComplete={'new-password'}
29
+ onChange={(e) => {
30
+ setConfig(ModelProvider.OpenRouter, { apiKey: e.target.value });
31
+ }}
32
+ placeholder={'*********************************'}
33
+ type={'block'}
34
+ value={apiKey}
35
+ />
36
+ </FormAction>
37
+ );
38
+ });
39
+
40
+ export default OpenRouterForm;
@@ -13,6 +13,7 @@ import GroqForm from './Groq';
13
13
  import MistralForm from './Mistral';
14
14
  import MoonshotForm from './Moonshot';
15
15
  import OpenAIForm from './OpenAI';
16
+ import OpenRouterForm from './OpenRouter';
16
17
  import PerplexityForm from './Perplexity';
17
18
  import ZhipuForm from './Zhipu';
18
19
 
@@ -59,6 +60,10 @@ const APIKeyForm = memo<APIKeyFormProps>(({ id, provider }) => {
59
60
  case ModelProvider.Groq: {
60
61
  return <GroqForm />;
61
62
  }
63
+
64
+ case ModelProvider.OpenRouter: {
65
+ return <OpenRouterForm />;
66
+ }
62
67
 
63
68
  default:
64
69
  case ModelProvider.OpenAI: {
@@ -75,6 +75,7 @@ const ErrorMessageExtra = memo<{ data: ChatMessage }>(({ data }) => {
75
75
  case AgentRuntimeErrorType.InvalidPerplexityAPIKey:
76
76
  case AgentRuntimeErrorType.InvalidAnthropicAPIKey:
77
77
  case AgentRuntimeErrorType.InvalidGroqAPIKey:
78
+ case AgentRuntimeErrorType.InvalidOpenRouterAPIKey:
78
79
  case AgentRuntimeErrorType.NoOpenAIAPIKey: {
79
80
  return <InvalidAPIKey id={data.id} provider={data.error?.body?.provider} />;
80
81
  }
@@ -37,6 +37,9 @@ export const AgentRuntimeErrorType = {
37
37
 
38
38
  InvalidGroqAPIKey: 'InvalidGroqAPIKey',
39
39
  GroqBizError: 'GroqBizError',
40
+
41
+ InvalidOpenRouterAPIKey: 'InvalidOpenRouterAPIKey',
42
+ OpenRouterBizError: 'OpenRouterBizError',
40
43
  } as const;
41
44
 
42
45
  export type ILobeAgentRuntimeErrorType =
@@ -9,6 +9,7 @@ export { LobeMistralAI } from './mistral';
9
9
  export { LobeMoonshotAI } from './moonshot';
10
10
  export { LobeOllamaAI } from './ollama';
11
11
  export { LobeOpenAI } from './openai';
12
+ export { LobeOpenRouterAI } from './openrouter';
12
13
  export { LobePerplexityAI } from './perplexity';
13
14
  export * from './types';
14
15
  export { AgentRuntimeError } from './utils/createError';