@lobehub/chat 0.122.8 → 0.123.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 (227) hide show
  1. package/.env.example +22 -2
  2. package/.eslintrc.js +1 -0
  3. package/CHANGELOG.md +50 -0
  4. package/README.md +11 -9
  5. package/README.zh-CN.md +11 -9
  6. package/docs/Deployment/Environment-Variable.md +65 -23
  7. package/docs/Deployment/Environment-Variable.zh-CN.md +64 -22
  8. package/locales/ar/chat.json +12 -1
  9. package/locales/ar/common.json +16 -0
  10. package/locales/ar/error.json +35 -6
  11. package/locales/ar/setting.json +53 -6
  12. package/locales/de-DE/chat.json +12 -1
  13. package/locales/de-DE/common.json +16 -0
  14. package/locales/de-DE/error.json +35 -6
  15. package/locales/de-DE/setting.json +53 -6
  16. package/locales/en-US/chat.json +13 -2
  17. package/locales/en-US/common.json +16 -0
  18. package/locales/en-US/error.json +35 -6
  19. package/locales/en-US/setting.json +53 -6
  20. package/locales/es-ES/chat.json +12 -1
  21. package/locales/es-ES/common.json +16 -0
  22. package/locales/es-ES/error.json +35 -6
  23. package/locales/es-ES/setting.json +53 -6
  24. package/locales/fr-FR/chat.json +13 -2
  25. package/locales/fr-FR/common.json +16 -0
  26. package/locales/fr-FR/error.json +35 -6
  27. package/locales/fr-FR/setting.json +53 -6
  28. package/locales/it-IT/chat.json +12 -1
  29. package/locales/it-IT/common.json +16 -0
  30. package/locales/it-IT/error.json +35 -6
  31. package/locales/it-IT/setting.json +53 -6
  32. package/locales/ja-JP/chat.json +12 -1
  33. package/locales/ja-JP/common.json +16 -0
  34. package/locales/ja-JP/error.json +35 -6
  35. package/locales/ja-JP/setting.json +53 -6
  36. package/locales/ko-KR/chat.json +12 -1
  37. package/locales/ko-KR/common.json +16 -0
  38. package/locales/ko-KR/error.json +35 -6
  39. package/locales/ko-KR/setting.json +53 -6
  40. package/locales/nl-NL/chat.json +12 -1
  41. package/locales/nl-NL/common.json +16 -0
  42. package/locales/nl-NL/error.json +35 -6
  43. package/locales/nl-NL/setting.json +53 -6
  44. package/locales/pl-PL/chat.json +12 -1
  45. package/locales/pl-PL/common.json +16 -0
  46. package/locales/pl-PL/error.json +35 -6
  47. package/locales/pl-PL/setting.json +53 -6
  48. package/locales/pt-BR/chat.json +12 -1
  49. package/locales/pt-BR/common.json +16 -0
  50. package/locales/pt-BR/error.json +35 -6
  51. package/locales/pt-BR/setting.json +53 -6
  52. package/locales/ru-RU/chat.json +12 -1
  53. package/locales/ru-RU/common.json +16 -0
  54. package/locales/ru-RU/error.json +35 -6
  55. package/locales/ru-RU/setting.json +53 -6
  56. package/locales/tr-TR/chat.json +12 -1
  57. package/locales/tr-TR/common.json +16 -0
  58. package/locales/tr-TR/error.json +35 -6
  59. package/locales/tr-TR/setting.json +53 -6
  60. package/locales/vi-VN/chat.json +12 -1
  61. package/locales/vi-VN/common.json +16 -0
  62. package/locales/vi-VN/error.json +35 -6
  63. package/locales/vi-VN/setting.json +53 -6
  64. package/locales/zh-CN/chat.json +12 -1
  65. package/locales/zh-CN/common.json +16 -0
  66. package/locales/zh-CN/error.json +34 -5
  67. package/locales/zh-CN/setting.json +59 -12
  68. package/locales/zh-TW/chat.json +12 -1
  69. package/locales/zh-TW/common.json +16 -0
  70. package/locales/zh-TW/error.json +35 -6
  71. package/locales/zh-TW/setting.json +53 -6
  72. package/next.config.mjs +8 -2
  73. package/package.json +8 -1
  74. package/src/app/api/chat/[provider]/agentRuntime.ts +129 -0
  75. package/src/app/api/chat/[provider]/route.ts +64 -0
  76. package/src/app/api/chat/auth.ts +42 -0
  77. package/src/app/api/chat/google/route.ts +86 -0
  78. package/src/app/api/config/route.ts +6 -1
  79. package/src/app/api/config.ts +3 -3
  80. package/src/app/api/errorResponse.test.ts +8 -8
  81. package/src/app/api/errorResponse.ts +43 -6
  82. package/src/app/api/openai/createBizOpenAI/createAzureOpenai.ts +1 -1
  83. package/src/app/api/openai/createBizOpenAI/createOpenai.ts +1 -1
  84. package/src/app/api/openai/createBizOpenAI/index.ts +2 -2
  85. package/src/app/chat/(desktop)/features/ChatHeader.tsx +6 -4
  86. package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +13 -3
  87. package/src/app/chat/(desktop)/features/ChatInput/Header/index.tsx +16 -24
  88. package/src/app/chat/(desktop)/features/SideBar/index.tsx +1 -0
  89. package/src/app/chat/features/ChatHeader/ShareButton/Preview.tsx +3 -3
  90. package/src/app/chat/features/SessionListContent/List/Item/index.tsx +2 -3
  91. package/src/app/settings/llm/Azure/index.tsx +145 -0
  92. package/src/app/settings/llm/Bedrock/index.tsx +109 -0
  93. package/src/app/settings/llm/{LLM/Checker.tsx → Checker.tsx} +20 -22
  94. package/src/app/settings/llm/Google/index.tsx +74 -0
  95. package/src/app/settings/llm/{LLM → OpenAI}/index.tsx +27 -45
  96. package/src/app/settings/llm/Zhipu/index.tsx +78 -0
  97. package/src/app/settings/llm/const.ts +13 -0
  98. package/src/app/settings/llm/page.tsx +42 -2
  99. package/src/app/settings/llm/useSyncSettings.ts +23 -0
  100. package/src/chains/__tests__/langDetect.test.ts +2 -2
  101. package/src/chains/__tests__/pickEmoji.test.ts +2 -2
  102. package/src/chains/langDetect.ts +2 -2
  103. package/src/chains/pickEmoji.ts +2 -2
  104. package/src/chains/summaryAgentName.ts +2 -2
  105. package/src/chains/summaryDescription.ts +2 -2
  106. package/src/chains/summaryTags.ts +2 -2
  107. package/src/chains/summaryTitle.ts +2 -2
  108. package/src/chains/translate.ts +2 -2
  109. package/src/components/ModelIcon/index.tsx +37 -0
  110. package/src/components/ModelProviderIcon/index.tsx +44 -0
  111. package/src/components/ModelSelect/index.tsx +133 -0
  112. package/src/components/ModelTag/ModelIcon.tsx +35 -0
  113. package/src/components/ModelTag/index.tsx +13 -0
  114. package/src/config/modelProviders/bedrock.ts +43 -0
  115. package/src/config/modelProviders/google.ts +20 -0
  116. package/src/config/modelProviders/index.ts +18 -0
  117. package/src/config/modelProviders/openai.ts +102 -0
  118. package/src/config/modelProviders/zhipu.ts +34 -0
  119. package/src/config/{server.ts → server/app.ts} +1 -24
  120. package/src/config/server/index.ts +13 -0
  121. package/src/config/server/provider.ts +78 -0
  122. package/src/const/auth.ts +30 -0
  123. package/src/const/fetch.ts +3 -0
  124. package/src/const/settings.ts +24 -4
  125. package/src/database/core/migrations/migrateSettingsToUser/index.test.ts +2 -1
  126. package/src/database/core/migrations/migrateSettingsToUser/index.ts +1 -18
  127. package/src/database/core/migrations/migrateSettingsToUser/type.ts +100 -0
  128. package/src/database/models/message.ts +3 -1
  129. package/src/database/schemas/message.ts +1 -0
  130. package/src/database/schemas/session.ts +1 -0
  131. package/src/features/AgentSetting/AgentConfig/ModelSelect.tsx +71 -0
  132. package/src/features/AgentSetting/AgentConfig/index.tsx +171 -161
  133. package/src/features/AgentSetting/AgentConfig/useSyncConfig.ts +23 -0
  134. package/src/features/AgentSetting/AgentPrompt/TokenTag.tsx +39 -0
  135. package/src/features/AgentSetting/AgentPrompt/index.tsx +4 -26
  136. package/src/features/AgentSetting/store/index.ts +10 -2
  137. package/src/features/ChatInput/ActionBar/FileUpload.tsx +11 -4
  138. package/src/features/ChatInput/ActionBar/ModelSwitch.tsx +56 -14
  139. package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +42 -7
  140. package/src/features/ChatInput/ActionBar/Token/index.tsx +5 -2
  141. package/src/features/ChatInput/ActionBar/Tools/index.tsx +11 -1
  142. package/src/features/ChatInput/useChatInput.ts +5 -1
  143. package/src/features/Conversation/Error/APIKeyForm/Bedrock.tsx +80 -0
  144. package/src/features/Conversation/Error/APIKeyForm/Google.tsx +61 -0
  145. package/src/features/Conversation/Error/APIKeyForm/OpenAI.tsx +63 -0
  146. package/src/features/Conversation/Error/APIKeyForm/Zhipu.tsx +62 -0
  147. package/src/features/Conversation/Error/APIKeyForm/index.tsx +72 -0
  148. package/src/features/Conversation/Error/AccessCodeForm.tsx +63 -0
  149. package/src/features/Conversation/Error/ErrorJsonViewer.tsx +26 -0
  150. package/src/features/Conversation/Error/InvalidAPIKey.tsx +16 -0
  151. package/src/features/Conversation/Error/InvalidAccessCode.tsx +50 -0
  152. package/src/features/Conversation/Error/OpenAiBizError.tsx +9 -17
  153. package/src/features/Conversation/Error/{Plugin/PluginSettings.tsx → PluginSettings.tsx} +8 -3
  154. package/src/features/Conversation/Error/index.tsx +75 -70
  155. package/src/features/Conversation/Error/style.tsx +9 -3
  156. package/src/features/Conversation/Extras/Assistant.tsx +2 -3
  157. package/src/features/Conversation/Plugins/Inspector/index.tsx +8 -2
  158. package/src/features/Conversation/components/ChatItem/index.tsx +9 -24
  159. package/src/features/Conversation/types/index.tsx +2 -5
  160. package/src/hooks/_header.ts +35 -0
  161. package/src/libs/agent-runtime/BaseAI.ts +9 -0
  162. package/src/libs/agent-runtime/azureOpenai/index.ts +72 -0
  163. package/src/libs/agent-runtime/bedrock/index.ts +134 -0
  164. package/src/libs/agent-runtime/error.ts +26 -0
  165. package/src/libs/agent-runtime/google/index.ts +160 -0
  166. package/src/libs/agent-runtime/index.ts +9 -0
  167. package/src/libs/agent-runtime/openai/index.test.ts +176 -0
  168. package/src/libs/agent-runtime/openai/index.ts +107 -0
  169. package/src/libs/agent-runtime/types/chat.ts +119 -0
  170. package/src/libs/agent-runtime/types/index.ts +2 -0
  171. package/src/libs/agent-runtime/types/type.ts +34 -0
  172. package/src/libs/agent-runtime/utils/createError.ts +10 -0
  173. package/src/libs/agent-runtime/utils/debugStream.ts +18 -0
  174. package/src/libs/agent-runtime/utils/env.ts +1 -0
  175. package/src/libs/agent-runtime/utils/handleOpenAIError.ts +39 -0
  176. package/src/libs/agent-runtime/utils/uriParser.ts +16 -0
  177. package/src/libs/agent-runtime/zhipu/authToken.ts +22 -0
  178. package/src/libs/agent-runtime/zhipu/index.ts +125 -0
  179. package/src/locales/default/chat.ts +12 -2
  180. package/src/locales/default/common.ts +16 -0
  181. package/src/locales/default/error.ts +42 -5
  182. package/src/locales/default/setting.ts +61 -12
  183. package/src/services/__tests__/chat.test.ts +8 -2
  184. package/src/services/__tests__/message.test.ts +1 -1
  185. package/src/services/_auth.ts +106 -0
  186. package/src/services/_header.ts +1 -0
  187. package/src/services/_url.ts +0 -2
  188. package/src/services/chat.ts +32 -14
  189. package/src/store/chat/slices/message/action.ts +9 -6
  190. package/src/store/global/slices/common/action.ts +8 -2
  191. package/src/store/global/slices/settings/action.test.ts +2 -2
  192. package/src/store/global/slices/settings/action.ts +11 -4
  193. package/src/store/global/slices/settings/selectors/__snapshots__/modelProvider.test.ts.snap +242 -78
  194. package/src/store/global/slices/settings/selectors/__snapshots__/selectors.test.ts.snap +2 -0
  195. package/src/store/global/slices/settings/selectors/modelProvider.test.ts +171 -47
  196. package/src/store/global/slices/settings/selectors/modelProvider.ts +136 -43
  197. package/src/store/session/slices/agent/selectors.test.ts +0 -48
  198. package/src/store/session/slices/agent/selectors.ts +7 -12
  199. package/src/types/agent/index.ts +6 -2
  200. package/src/types/fetch.ts +4 -2
  201. package/src/types/files.ts +3 -3
  202. package/src/types/llm.ts +27 -0
  203. package/src/types/message/index.ts +7 -3
  204. package/src/types/openai/chat.ts +6 -11
  205. package/src/types/settings/index.ts +1 -0
  206. package/src/types/settings/modelProvider.ts +37 -1
  207. package/src/utils/__snapshots__/parseModels.test.ts.snap +63 -0
  208. package/src/utils/parseModels.test.ts +24 -0
  209. package/src/utils/parseModels.ts +37 -0
  210. package/src/app/api/openai/chat/createChatCompletion.test.ts +0 -166
  211. package/src/app/api/openai/chat/createChatCompletion.ts +0 -76
  212. package/src/app/api/openai/chat/route.ts +0 -19
  213. package/src/app/api/openai/models/route.ts +0 -17
  214. package/src/app/settings/llm/LLM/ModelList.tsx +0 -51
  215. package/src/app/settings/llm/LLM/getModelList.ts +0 -9
  216. package/src/app/settings/llm/index.tsx +0 -36
  217. package/src/const/llm.ts +0 -32
  218. package/src/features/Conversation/Error/ApiKeyForm.tsx +0 -87
  219. package/src/features/Conversation/Error/ErrorActionContainer.tsx +0 -23
  220. package/src/features/Conversation/Error/InvalidAccess.tsx +0 -90
  221. package/src/features/Conversation/Error/OpenAPIKey.tsx +0 -13
  222. package/src/features/Conversation/Error/Plugin/PluginError.tsx +0 -30
  223. package/src/services/modelList.ts +0 -15
  224. /package/src/app/chat/(desktop)/features/ChatInput/{Header → Footer}/DragUpload.tsx +0 -0
  225. /package/src/{app/api/openai/chat → libs/agent-runtime/utils}/desensitizeUrl.test.ts +0 -0
  226. /package/src/{app/api/openai/chat → libs/agent-runtime/utils}/desensitizeUrl.ts +0 -0
  227. /package/src/types/{translate.ts → message/translate.ts} +0 -0
@@ -0,0 +1,160 @@
1
+ import { Content, GoogleGenerativeAI, Part } from '@google/generative-ai';
2
+ import { GoogleGenerativeAIStream, StreamingTextResponse } from 'ai';
3
+
4
+ import { LobeRuntimeAI } from '../BaseAI';
5
+ import { AgentRuntimeErrorType, ILobeAgentRuntimeErrorType } from '../error';
6
+ import { ChatStreamPayload, OpenAIChatMessage, UserMessageContentPart } from '../types';
7
+ import { ModelProvider } from '../types/type';
8
+ import { AgentRuntimeError } from '../utils/createError';
9
+ import { debugStream } from '../utils/debugStream';
10
+ import { DEBUG_CHAT_COMPLETION } from '../utils/env';
11
+ import { parseDataUri } from '../utils/uriParser';
12
+
13
+ type GoogleChatErrors = GoogleChatError[];
14
+
15
+ interface GoogleChatError {
16
+ '@type': string;
17
+ 'domain': string;
18
+ 'metadata': {
19
+ service: string;
20
+ };
21
+ 'reason': string;
22
+ }
23
+
24
+ export class LobeGoogleAI implements LobeRuntimeAI {
25
+ private client: GoogleGenerativeAI;
26
+
27
+ constructor(apiKey: string) {
28
+ if (!apiKey) throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidGoogleAPIKey);
29
+
30
+ this.client = new GoogleGenerativeAI(apiKey);
31
+ }
32
+
33
+ async chat(payload: ChatStreamPayload) {
34
+ try {
35
+ const { contents, model } = this.buildGoogleMessages(payload.messages, payload.model);
36
+ const geminiStream = await this.client
37
+ .getGenerativeModel({
38
+ generationConfig: {
39
+ maxOutputTokens: payload.max_tokens,
40
+ temperature: payload.temperature,
41
+ topP: payload.top_p,
42
+ },
43
+ model,
44
+ })
45
+ .generateContentStream({ contents });
46
+
47
+ // Convert the response into a friendly text-stream
48
+ const stream = GoogleGenerativeAIStream(geminiStream);
49
+
50
+ const [debug, output] = stream.tee();
51
+
52
+ if (DEBUG_CHAT_COMPLETION) {
53
+ debugStream(debug).catch(console.error);
54
+ }
55
+
56
+ // Respond with the stream
57
+ return new StreamingTextResponse(output);
58
+ } catch (e) {
59
+ const err = e as Error;
60
+
61
+ const { errorType, error } = this.parseErrorMessage(err.message);
62
+
63
+ throw AgentRuntimeError.chat({ error, errorType, provider: ModelProvider.Google });
64
+ }
65
+ }
66
+
67
+ private convertContentToGooglePart = (content: UserMessageContentPart): Part => {
68
+ switch (content.type) {
69
+ case 'text': {
70
+ return { text: content.text };
71
+ }
72
+ case 'image_url': {
73
+ const { mimeType, base64 } = parseDataUri(content.image_url.url);
74
+
75
+ if (!base64) {
76
+ throw new TypeError("Image URL doesn't contain base64 data");
77
+ }
78
+
79
+ return {
80
+ inlineData: {
81
+ data: base64,
82
+ mimeType: mimeType || 'image/png',
83
+ },
84
+ };
85
+ }
86
+ }
87
+ };
88
+
89
+ private convertOAIMessagesToGoogleMessage = (message: OpenAIChatMessage): Content => {
90
+ const content = message.content as string | UserMessageContentPart[];
91
+
92
+ return {
93
+ parts:
94
+ typeof content === 'string'
95
+ ? [{ text: content }]
96
+ : content.map((c) => this.convertContentToGooglePart(c)),
97
+ role: message.role === 'user' ? 'user' : 'model',
98
+ };
99
+ };
100
+
101
+ // convert messages from the Vercel AI SDK Format to the format
102
+ // that is expected by the Google GenAI SDK
103
+ private buildGoogleMessages = (
104
+ messages: OpenAIChatMessage[],
105
+ model: string,
106
+ ): { contents: Content[]; model: string } => {
107
+ const contents = messages
108
+ .filter((message) => message.role === 'user' || message.role === 'assistant')
109
+ .map((msg) => this.convertOAIMessagesToGoogleMessage(msg));
110
+
111
+ // if message are all text message, use vision will return error
112
+ // use add an image to use models/gemini-pro-vision, or switch your model to a text model
113
+ const noImage = messages.every((m) => typeof m.content === 'string');
114
+
115
+ return { contents, model: noImage ? 'gemini-pro' : model };
116
+ };
117
+
118
+ private parseErrorMessage(message: string): {
119
+ error: any;
120
+ errorType: ILobeAgentRuntimeErrorType;
121
+ } {
122
+ const defaultError = {
123
+ error: { message },
124
+ errorType: AgentRuntimeErrorType.GoogleBizError,
125
+ };
126
+
127
+ if (message.includes('location is not supported'))
128
+ return { error: message, errorType: AgentRuntimeErrorType.LocationNotSupportError };
129
+
130
+ try {
131
+ const startIndex = message.lastIndexOf('[');
132
+ if (startIndex === -1) {
133
+ return defaultError;
134
+ }
135
+
136
+ // 从开始位置截取字符串到最后
137
+ const jsonString = message.slice(startIndex);
138
+
139
+ // 尝试解析 JSON 字符串
140
+ const json: GoogleChatErrors = JSON.parse(jsonString);
141
+
142
+ const bizError = json[0];
143
+
144
+ switch (bizError.reason) {
145
+ case 'API_KEY_INVALID': {
146
+ return { ...defaultError, errorType: AgentRuntimeErrorType.InvalidGoogleAPIKey };
147
+ }
148
+
149
+ default: {
150
+ return { error: json, errorType: AgentRuntimeErrorType.GoogleBizError };
151
+ }
152
+ }
153
+ } catch {
154
+ // 如果解析失败,则返回原始错误消息
155
+ return defaultError;
156
+ }
157
+ }
158
+ }
159
+
160
+ export default LobeGoogleAI;
@@ -0,0 +1,9 @@
1
+ export { LobeAzureOpenAI } from './azureOpenai';
2
+ export * from './BaseAI';
3
+ export { LobeBedrockAI } from './bedrock';
4
+ export * from './error';
5
+ export { LobeGoogleAI } from './google';
6
+ export { LobeOpenAI } from './openai';
7
+ export * from './types';
8
+ export { AgentRuntimeError } from './utils/createError';
9
+ export { LobeZhipuAI } from './zhipu';
@@ -0,0 +1,176 @@
1
+ import OpenAI from 'openai';
2
+ import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { LobeOpenAI } from './index';
5
+
6
+ // Mock the console.error to avoid polluting test output
7
+ vi.spyOn(console, 'error').mockImplementation(() => {});
8
+
9
+ describe('LobeOpenAI chat', () => {
10
+ let openaiInstance: LobeOpenAI;
11
+
12
+ beforeEach(() => {
13
+ openaiInstance = new LobeOpenAI({ apiKey: 'test', dangerouslyAllowBrowser: true });
14
+
15
+ // 使用 vi.spyOn 来模拟 chat.completions.create 方法
16
+ vi.spyOn(openaiInstance['client'].chat.completions, 'create').mockResolvedValue(
17
+ new ReadableStream() as any,
18
+ );
19
+ });
20
+
21
+ afterEach(() => {
22
+ vi.clearAllMocks();
23
+ });
24
+
25
+ describe('chat', () => {
26
+ it('should return a StreamingTextResponse on successful API call', async () => {
27
+ // Arrange
28
+ const mockStream = new ReadableStream();
29
+ const mockResponse = Promise.resolve(mockStream);
30
+
31
+ (openaiInstance['client'].chat.completions.create as Mock).mockResolvedValue(mockResponse);
32
+
33
+ // Act
34
+ const result = await openaiInstance.chat({
35
+ messages: [{ content: 'Hello', role: 'user' }],
36
+ model: 'text-davinci-003',
37
+ temperature: 0,
38
+ });
39
+
40
+ // Assert
41
+ expect(result).toBeInstanceOf(Response);
42
+ });
43
+
44
+ it('should return an openai error response when OpenAI.APIError is thrown', async () => {
45
+ // Arrange
46
+ const apiError = new OpenAI.APIError(
47
+ 400,
48
+ {
49
+ status: 400,
50
+ error: {
51
+ message: 'Bad Request',
52
+ },
53
+ },
54
+ 'Error message',
55
+ {},
56
+ );
57
+
58
+ vi.spyOn(openaiInstance['client'].chat.completions, 'create').mockRejectedValue(apiError);
59
+
60
+ // Act
61
+ try {
62
+ await openaiInstance.chat({
63
+ messages: [{ content: 'Hello', role: 'user' }],
64
+ model: 'text-davinci-003',
65
+ temperature: 0,
66
+ });
67
+ } catch (e) {
68
+ expect(e).toEqual({
69
+ endpoint: 'https://api.openai.com/v1',
70
+ error: {
71
+ error: { message: 'Bad Request' },
72
+ status: 400,
73
+ },
74
+ errorType: 'OpenAIBizError',
75
+ provider: 'openai',
76
+ });
77
+ }
78
+ });
79
+
80
+ it('should return an cause response when OpenAI.APIError is thrown with cause', async () => {
81
+ // Arrange
82
+ const errorInfo = {
83
+ stack: 'abc',
84
+ cause: {
85
+ message: 'api is undefined',
86
+ },
87
+ };
88
+ const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
89
+
90
+ vi.spyOn(openaiInstance['client'].chat.completions, 'create').mockRejectedValue(apiError);
91
+
92
+ // Act
93
+ try {
94
+ await openaiInstance.chat({
95
+ messages: [{ content: 'Hello', role: 'user' }],
96
+ model: 'text-davinci-003',
97
+ temperature: 0,
98
+ });
99
+ } catch (e) {
100
+ expect(e).toEqual({
101
+ endpoint: 'https://api.openai.com/v1',
102
+ error: {
103
+ cause: { message: 'api is undefined' },
104
+ stack: 'abc',
105
+ },
106
+ errorType: 'OpenAIBizError',
107
+ provider: 'openai',
108
+ });
109
+ }
110
+ });
111
+
112
+ it('should return an cause response with desensitize Url', async () => {
113
+ // Arrange
114
+ const errorInfo = {
115
+ stack: 'abc',
116
+ cause: { message: 'api is undefined' },
117
+ };
118
+ const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});
119
+
120
+ openaiInstance = new LobeOpenAI({
121
+ apiKey: 'test',
122
+ dangerouslyAllowBrowser: true,
123
+ baseURL: 'https://api.abc.com/v1',
124
+ });
125
+
126
+ vi.spyOn(openaiInstance['client'].chat.completions, 'create').mockRejectedValue(apiError);
127
+
128
+ // Act
129
+ try {
130
+ await openaiInstance.chat({
131
+ messages: [{ content: 'Hello', role: 'user' }],
132
+ model: 'gpt-3.5-turbo',
133
+ temperature: 0,
134
+ });
135
+ } catch (e) {
136
+ expect(e).toEqual({
137
+ endpoint: 'https://api.***.com/v1',
138
+ error: {
139
+ cause: { message: 'api is undefined' },
140
+ stack: 'abc',
141
+ },
142
+ errorType: 'OpenAIBizError',
143
+ provider: 'openai',
144
+ });
145
+ }
146
+ });
147
+
148
+ it('should return a 500 error response for non-OpenAI errors', async () => {
149
+ // Arrange
150
+ const genericError = new Error('Generic Error');
151
+
152
+ vi.spyOn(openaiInstance['client'].chat.completions, 'create').mockRejectedValue(genericError);
153
+
154
+ // Act
155
+ try {
156
+ await openaiInstance.chat({
157
+ messages: [{ content: 'Hello', role: 'user' }],
158
+ model: 'text-davinci-003',
159
+ temperature: 0,
160
+ });
161
+ } catch (e) {
162
+ expect(e).toEqual({
163
+ endpoint: 'https://api.openai.com/v1',
164
+ errorType: 'AgentRuntimeError',
165
+ provider: 'openai',
166
+ error: {
167
+ name: genericError.name,
168
+ cause: genericError.cause,
169
+ message: genericError.message,
170
+ stack: genericError.stack,
171
+ },
172
+ });
173
+ }
174
+ });
175
+ });
176
+ });
@@ -0,0 +1,107 @@
1
+ import { OpenAIStream, StreamingTextResponse } from 'ai';
2
+ import OpenAI, { ClientOptions } from 'openai';
3
+ import urlJoin from 'url-join';
4
+
5
+ import { ChatStreamPayload } from '@/types/openai/chat';
6
+
7
+ import { LobeRuntimeAI } from '../BaseAI';
8
+ import { AgentRuntimeErrorType } from '../error';
9
+ import { ModelProvider } from '../types';
10
+ import { AgentRuntimeError } from '../utils/createError';
11
+ import { debugStream } from '../utils/debugStream';
12
+ import { desensitizeUrl } from '../utils/desensitizeUrl';
13
+ import { DEBUG_CHAT_COMPLETION } from '../utils/env';
14
+ import { handleOpenAIError } from '../utils/handleOpenAIError';
15
+
16
+ const DEFAULT_BASE_URL = 'https://api.openai.com/v1';
17
+
18
+ interface AzureOpenAIOptions extends ClientOptions {
19
+ azureOptions?: {
20
+ apiVersion?: string;
21
+ model?: string;
22
+ };
23
+ useAzure?: boolean;
24
+ }
25
+ export class LobeOpenAI implements LobeRuntimeAI {
26
+ private client: OpenAI;
27
+
28
+ constructor(options: AzureOpenAIOptions) {
29
+ if (!options.apiKey) throw AgentRuntimeError.createError(AgentRuntimeErrorType.NoOpenAIAPIKey);
30
+
31
+ if (options.useAzure) {
32
+ this.client = LobeOpenAI.initWithAzureOpenAI(options);
33
+ } else {
34
+ this.client = new OpenAI(options);
35
+ }
36
+
37
+ this.baseURL = this.client.baseURL;
38
+ }
39
+
40
+ baseURL: string;
41
+
42
+ async chat(payload: ChatStreamPayload) {
43
+ // ============ 1. preprocess messages ============ //
44
+ const { messages, ...params } = payload;
45
+
46
+ // ============ 2. send api ============ //
47
+
48
+ try {
49
+ const response = await this.client.chat.completions.create(
50
+ {
51
+ messages,
52
+ ...params,
53
+ stream: true,
54
+ } as unknown as OpenAI.ChatCompletionCreateParamsStreaming,
55
+ { headers: { Accept: '*/*' } },
56
+ );
57
+
58
+ const stream = OpenAIStream(response);
59
+
60
+ const [debug, prod] = stream.tee();
61
+
62
+ if (DEBUG_CHAT_COMPLETION) {
63
+ debugStream(debug).catch(console.error);
64
+ }
65
+
66
+ return new StreamingTextResponse(prod);
67
+ } catch (error) {
68
+ const { errorResult, RuntimeError } = handleOpenAIError(error);
69
+
70
+ const errorType = RuntimeError || AgentRuntimeErrorType.OpenAIBizError;
71
+
72
+ let desensitizedEndpoint = this.baseURL;
73
+
74
+ // refs: https://github.com/lobehub/lobe-chat/issues/842
75
+ if (this.baseURL !== DEFAULT_BASE_URL) {
76
+ desensitizedEndpoint = desensitizeUrl(this.baseURL);
77
+ }
78
+
79
+ throw AgentRuntimeError.chat({
80
+ endpoint: desensitizedEndpoint,
81
+ error: errorResult,
82
+ errorType,
83
+ provider: ModelProvider.OpenAI,
84
+ });
85
+ }
86
+ }
87
+
88
+ static initWithAzureOpenAI(options: AzureOpenAIOptions) {
89
+ const endpoint = options.baseURL!;
90
+ const model = options.azureOptions?.model || '';
91
+
92
+ // refs: https://test-001.openai.azure.com/openai/deployments/gpt-35-turbo
93
+ const baseURL = urlJoin(endpoint, `/openai/deployments/${model.replace('.', '')}`);
94
+
95
+ const apiVersion = options.azureOptions?.apiVersion || '2023-08-01-preview';
96
+ const apiKey = options.apiKey!;
97
+
98
+ const config: ClientOptions = {
99
+ apiKey,
100
+ baseURL,
101
+ defaultHeaders: { 'api-key': apiKey },
102
+ defaultQuery: { 'api-version': apiVersion },
103
+ };
104
+
105
+ return new OpenAI(config);
106
+ }
107
+ }
@@ -0,0 +1,119 @@
1
+ export type LLMRoleType = 'user' | 'system' | 'assistant' | 'function';
2
+
3
+ interface UserMessageContentPartText {
4
+ text: string;
5
+ type: 'text';
6
+ }
7
+ interface UserMessageContentPartImage {
8
+ image_url: {
9
+ detail?: 'auto' | 'low' | 'high';
10
+ url: string;
11
+ };
12
+ type: 'image_url';
13
+ }
14
+
15
+ export type UserMessageContentPart = UserMessageContentPartText | UserMessageContentPartImage;
16
+
17
+ export interface OpenAIChatMessage {
18
+ /**
19
+ * @title 内容
20
+ * @description 消息内容
21
+ */
22
+ content: string | UserMessageContentPart[];
23
+
24
+ name?: string;
25
+ /**
26
+ * 角色
27
+ * @description 消息发送者的角色
28
+ */
29
+ role: LLMRoleType;
30
+ }
31
+
32
+ /**
33
+ * @title Chat Stream Payload
34
+ */
35
+ export interface ChatStreamPayload {
36
+ /**
37
+ * @title 控制生成文本中的惩罚系数,用于减少重复性
38
+ * @default 0
39
+ */
40
+ frequency_penalty?: number;
41
+ /**
42
+ * @title 生成文本的最大长度
43
+ */
44
+ max_tokens?: number;
45
+ /**
46
+ * @title 聊天信息列表
47
+ */
48
+ messages: OpenAIChatMessage[];
49
+ /**
50
+ * @title 模型名称
51
+ */
52
+ model: string;
53
+ /**
54
+ * @title 返回的文本数量
55
+ */
56
+ n?: number;
57
+ /**
58
+ * 开启的插件列表
59
+ */
60
+ plugins?: string[];
61
+ /**
62
+ * @title 控制生成文本中的惩罚系数,用于减少主题的变化
63
+ * @default 0
64
+ */
65
+ presence_penalty?: number;
66
+ /**
67
+ * @default openai
68
+ */
69
+ provider?: string;
70
+ /**
71
+ * @title 是否开启流式请求
72
+ * @default true
73
+ */
74
+ stream?: boolean;
75
+ /**
76
+ * @title 生成文本的随机度量,用于控制文本的创造性和多样性
77
+ * @default 0.5
78
+ */
79
+ temperature: number;
80
+ tool_choice?: string;
81
+ tools?: ChatCompletionTool[];
82
+ /**
83
+ * @title 控制生成文本中最高概率的单个令牌
84
+ * @default 1
85
+ */
86
+ top_p?: number;
87
+ }
88
+
89
+ export interface ChatCompletionFunctions {
90
+ /**
91
+ * The description of what the function does.
92
+ * @type {string}
93
+ * @memberof ChatCompletionFunctions
94
+ */
95
+ description?: string;
96
+ /**
97
+ * The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64.
98
+ * @type {string}
99
+ * @memberof ChatCompletionFunctions
100
+ */
101
+ name: string;
102
+ /**
103
+ * The parameters the functions accepts, described as a JSON Schema object. See the [guide](/docs/guides/gpt/function-calling) for examples, and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format.
104
+ * @type {{ [key: string]: any }}
105
+ * @memberof ChatCompletionFunctions
106
+ */
107
+ parameters?: {
108
+ [key: string]: any;
109
+ };
110
+ }
111
+
112
+ export interface ChatCompletionTool {
113
+ function: ChatCompletionFunctions;
114
+
115
+ /**
116
+ * The type of the tool. Currently, only `function` is supported.
117
+ */
118
+ type: 'function';
119
+ }
@@ -0,0 +1,2 @@
1
+ export * from './chat';
2
+ export * from './type';
@@ -0,0 +1,34 @@
1
+ import OpenAI from 'openai';
2
+
3
+ import { ILobeAgentRuntimeErrorType } from '../error';
4
+ import { ChatStreamPayload } from './chat';
5
+
6
+ export interface AgentInitErrorPayload {
7
+ error: object;
8
+ type: string | number;
9
+ }
10
+
11
+ export interface ChatCompletionErrorPayload {
12
+ [key: string]: any;
13
+ endpoint?: string;
14
+ error: object;
15
+ errorType: ILobeAgentRuntimeErrorType;
16
+ provider: ModelProvider;
17
+ }
18
+
19
+ export interface CreateChatCompletionOptions {
20
+ chatModel: OpenAI;
21
+ payload: ChatStreamPayload;
22
+ }
23
+
24
+ export enum ModelProvider {
25
+ Anthropic = 'anthropic',
26
+ Azure = 'azure',
27
+ Bedrock = 'bedrock',
28
+ ChatGLM = 'chatglm',
29
+ Google = 'google',
30
+ Mistral = 'mistral',
31
+ OpenAI = 'openai',
32
+ Tongyi = 'tongyi',
33
+ ZhiPu = 'zhipu',
34
+ }
@@ -0,0 +1,10 @@
1
+ import { ILobeAgentRuntimeErrorType } from '../error';
2
+ import { AgentInitErrorPayload, ChatCompletionErrorPayload } from '../types';
3
+
4
+ export const AgentRuntimeError = {
5
+ chat: (error: ChatCompletionErrorPayload): ChatCompletionErrorPayload => error,
6
+ createError: (
7
+ errorType: ILobeAgentRuntimeErrorType | string | number,
8
+ error?: any,
9
+ ): AgentInitErrorPayload => ({ error, type: errorType }),
10
+ };
@@ -0,0 +1,18 @@
1
+ export const debugStream = async (stream: ReadableStream) => {
2
+ let done = false;
3
+ let chunk = 0;
4
+ const decoder = new TextDecoder();
5
+
6
+ const reader = stream.getReader();
7
+ while (!done) {
8
+ const { value, done: _done } = await reader.read();
9
+ const chunkValue = decoder.decode(value, { stream: true });
10
+ if (!_done) {
11
+ console.log(`chunk ${chunk}:`);
12
+ console.log(chunkValue);
13
+ }
14
+
15
+ done = _done;
16
+ chunk++;
17
+ }
18
+ };
@@ -0,0 +1 @@
1
+ export const DEBUG_CHAT_COMPLETION = process.env.DEBUG_CHAT_COMPLETION === '1';
@@ -0,0 +1,39 @@
1
+ import OpenAI from 'openai';
2
+
3
+ import { AgentRuntimeErrorType } from '../error';
4
+
5
+ export const handleOpenAIError = (
6
+ error: any,
7
+ ): { RuntimeError?: 'AgentRuntimeError'; errorResult: any } => {
8
+ let errorResult: any;
9
+
10
+ // Check if the error is an OpenAI APIError
11
+ if (error instanceof OpenAI.APIError) {
12
+ // if error is definitely OpenAI APIError, there will be an error object
13
+ if (error.error) {
14
+ errorResult = error.error;
15
+ }
16
+ // Or if there is a cause, we use error cause
17
+ // This often happened when there is a bug of the `openai` package.
18
+ else if (error.cause) {
19
+ errorResult = error.cause;
20
+ }
21
+ // if there is no other request error, the error object is a Response like object
22
+ else {
23
+ errorResult = { headers: error.headers, stack: error.stack, status: error.status };
24
+ }
25
+
26
+ return {
27
+ errorResult,
28
+ };
29
+ } else {
30
+ const err = error as Error;
31
+
32
+ errorResult = { cause: err.cause, message: err.message, name: err.name, stack: err.stack };
33
+
34
+ return {
35
+ RuntimeError: AgentRuntimeErrorType.AgentRuntimeError,
36
+ errorResult,
37
+ };
38
+ }
39
+ };
@@ -0,0 +1,16 @@
1
+ export const parseDataUri = (
2
+ dataUri: string,
3
+ ): { base64: string | null; mimeType: string | null } => {
4
+ // 正则表达式匹配整个 Data URI 结构
5
+ const dataUriMatch = dataUri.match(/^data:([^;]+);base64,(.+)$/);
6
+
7
+ // 如果匹配成功,则返回 mimeType 和 base64,否则返回 null
8
+ if (dataUriMatch) {
9
+ return {
10
+ base64: dataUriMatch[2],
11
+ mimeType: dataUriMatch[1],
12
+ };
13
+ }
14
+
15
+ return { base64: null, mimeType: null };
16
+ };