@lobehub/chat 0.122.9 → 0.123.1
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/.env.example +22 -2
- package/.eslintrc.js +1 -0
- package/CHANGELOG.md +59 -0
- package/Dockerfile +7 -1
- package/README.md +12 -10
- package/README.zh-CN.md +12 -10
- package/docs/Deployment/Docker-Deployment.md +1 -1
- package/docs/Deployment/Docker-Deployment.zh-CN.md +1 -1
- package/docs/Deployment/Environment-Variable.md +65 -23
- package/docs/Deployment/Environment-Variable.zh-CN.md +64 -22
- package/locales/ar/chat.json +12 -1
- package/locales/ar/common.json +16 -0
- package/locales/ar/error.json +35 -6
- package/locales/ar/setting.json +53 -6
- package/locales/de-DE/chat.json +12 -1
- package/locales/de-DE/common.json +16 -0
- package/locales/de-DE/error.json +35 -6
- package/locales/de-DE/setting.json +53 -6
- package/locales/en-US/chat.json +13 -2
- package/locales/en-US/common.json +16 -0
- package/locales/en-US/error.json +35 -6
- package/locales/en-US/setting.json +53 -6
- package/locales/es-ES/chat.json +12 -1
- package/locales/es-ES/common.json +16 -0
- package/locales/es-ES/error.json +35 -6
- package/locales/es-ES/setting.json +53 -6
- package/locales/fr-FR/chat.json +13 -2
- package/locales/fr-FR/common.json +16 -0
- package/locales/fr-FR/error.json +35 -6
- package/locales/fr-FR/setting.json +53 -6
- package/locales/it-IT/chat.json +12 -1
- package/locales/it-IT/common.json +16 -0
- package/locales/it-IT/error.json +35 -6
- package/locales/it-IT/setting.json +53 -6
- package/locales/ja-JP/chat.json +12 -1
- package/locales/ja-JP/common.json +16 -0
- package/locales/ja-JP/error.json +35 -6
- package/locales/ja-JP/setting.json +53 -6
- package/locales/ko-KR/chat.json +12 -1
- package/locales/ko-KR/common.json +16 -0
- package/locales/ko-KR/error.json +35 -6
- package/locales/ko-KR/setting.json +53 -6
- package/locales/nl-NL/chat.json +12 -1
- package/locales/nl-NL/common.json +16 -0
- package/locales/nl-NL/error.json +35 -6
- package/locales/nl-NL/setting.json +53 -6
- package/locales/pl-PL/chat.json +12 -1
- package/locales/pl-PL/common.json +16 -0
- package/locales/pl-PL/error.json +35 -6
- package/locales/pl-PL/setting.json +53 -6
- package/locales/pt-BR/chat.json +12 -1
- package/locales/pt-BR/common.json +16 -0
- package/locales/pt-BR/error.json +35 -6
- package/locales/pt-BR/setting.json +53 -6
- package/locales/ru-RU/chat.json +12 -1
- package/locales/ru-RU/common.json +16 -0
- package/locales/ru-RU/error.json +35 -6
- package/locales/ru-RU/setting.json +53 -6
- package/locales/tr-TR/chat.json +12 -1
- package/locales/tr-TR/common.json +16 -0
- package/locales/tr-TR/error.json +35 -6
- package/locales/tr-TR/setting.json +53 -6
- package/locales/vi-VN/chat.json +12 -1
- package/locales/vi-VN/common.json +16 -0
- package/locales/vi-VN/error.json +35 -6
- package/locales/vi-VN/setting.json +53 -6
- package/locales/zh-CN/chat.json +12 -1
- package/locales/zh-CN/common.json +16 -0
- package/locales/zh-CN/error.json +34 -5
- package/locales/zh-CN/setting.json +59 -12
- package/locales/zh-TW/chat.json +12 -1
- package/locales/zh-TW/common.json +16 -0
- package/locales/zh-TW/error.json +35 -6
- package/locales/zh-TW/setting.json +53 -6
- package/next.config.mjs +8 -2
- package/package.json +8 -2
- package/src/app/api/chat/[provider]/agentRuntime.ts +129 -0
- package/src/app/api/chat/[provider]/route.ts +64 -0
- package/src/app/api/chat/auth.ts +42 -0
- package/src/app/api/chat/google/route.ts +86 -0
- package/src/app/api/config/route.ts +6 -1
- package/src/app/api/config.ts +3 -3
- package/src/app/api/errorResponse.test.ts +8 -8
- package/src/app/api/errorResponse.ts +43 -6
- package/src/app/api/openai/createBizOpenAI/createAzureOpenai.ts +1 -1
- package/src/app/api/openai/createBizOpenAI/createOpenai.ts +1 -1
- package/src/app/api/openai/createBizOpenAI/index.ts +2 -2
- package/src/app/api/plugin/gateway/route.ts +4 -1
- package/src/app/chat/(desktop)/features/ChatHeader.tsx +6 -4
- package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +13 -3
- package/src/app/chat/(desktop)/features/ChatInput/Header/index.tsx +16 -24
- package/src/app/chat/(desktop)/features/SideBar/index.tsx +1 -0
- package/src/app/chat/features/ChatHeader/ShareButton/Preview.tsx +3 -3
- package/src/app/chat/features/SessionListContent/List/Item/index.tsx +2 -3
- package/src/app/settings/llm/Azure/index.tsx +145 -0
- package/src/app/settings/llm/Bedrock/index.tsx +109 -0
- package/src/app/settings/llm/{LLM/Checker.tsx → Checker.tsx} +11 -14
- package/src/app/settings/llm/Google/index.tsx +74 -0
- package/src/app/settings/llm/{LLM → OpenAI}/index.tsx +26 -44
- package/src/app/settings/llm/Zhipu/index.tsx +78 -0
- package/src/app/settings/llm/const.ts +13 -0
- package/src/app/settings/llm/page.tsx +42 -2
- package/src/app/settings/llm/useSyncSettings.ts +23 -0
- package/src/chains/__tests__/langDetect.test.ts +2 -2
- package/src/chains/__tests__/pickEmoji.test.ts +2 -2
- package/src/chains/langDetect.ts +2 -2
- package/src/chains/pickEmoji.ts +2 -2
- package/src/chains/summaryAgentName.ts +2 -2
- package/src/chains/summaryDescription.ts +2 -2
- package/src/chains/summaryTags.ts +2 -2
- package/src/chains/summaryTitle.ts +2 -2
- package/src/chains/translate.ts +2 -2
- package/src/components/ModelIcon/index.tsx +37 -0
- package/src/components/ModelProviderIcon/index.tsx +44 -0
- package/src/components/ModelSelect/index.tsx +133 -0
- package/src/components/ModelTag/ModelIcon.tsx +35 -0
- package/src/components/ModelTag/index.tsx +13 -0
- package/src/config/modelProviders/bedrock.ts +43 -0
- package/src/config/modelProviders/google.ts +20 -0
- package/src/config/modelProviders/index.ts +18 -0
- package/src/config/modelProviders/openai.ts +110 -0
- package/src/config/modelProviders/zhipu.ts +34 -0
- package/src/config/{server.ts → server/app.ts} +1 -24
- package/src/config/server/index.ts +13 -0
- package/src/config/server/provider.ts +78 -0
- package/src/const/auth.ts +30 -0
- package/src/const/fetch.ts +3 -0
- package/src/const/settings.ts +24 -4
- package/src/database/core/migrations/migrateSettingsToUser/index.test.ts +2 -1
- package/src/database/core/migrations/migrateSettingsToUser/index.ts +1 -18
- package/src/database/core/migrations/migrateSettingsToUser/type.ts +100 -0
- package/src/database/models/message.ts +3 -1
- package/src/database/schemas/message.ts +1 -0
- package/src/database/schemas/session.ts +1 -0
- package/src/features/AgentSetting/AgentConfig/ModelSelect.tsx +71 -0
- package/src/features/AgentSetting/AgentConfig/index.tsx +171 -161
- package/src/features/AgentSetting/AgentConfig/useSyncConfig.ts +23 -0
- package/src/features/AgentSetting/AgentPrompt/TokenTag.tsx +39 -0
- package/src/features/AgentSetting/AgentPrompt/index.tsx +4 -26
- package/src/features/AgentSetting/store/index.ts +10 -2
- package/src/features/ChatInput/ActionBar/FileUpload.tsx +15 -5
- package/src/features/ChatInput/ActionBar/ModelSwitch.tsx +56 -14
- package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +42 -7
- package/src/features/ChatInput/ActionBar/Token/index.tsx +5 -2
- package/src/features/ChatInput/ActionBar/Tools/index.tsx +11 -1
- package/src/features/ChatInput/useChatInput.ts +5 -1
- package/src/features/Conversation/Error/APIKeyForm/Bedrock.tsx +80 -0
- package/src/features/Conversation/Error/APIKeyForm/Google.tsx +61 -0
- package/src/features/Conversation/Error/APIKeyForm/OpenAI.tsx +63 -0
- package/src/features/Conversation/Error/APIKeyForm/Zhipu.tsx +62 -0
- package/src/features/Conversation/Error/APIKeyForm/index.tsx +72 -0
- package/src/features/Conversation/Error/AccessCodeForm.tsx +63 -0
- package/src/features/Conversation/Error/ErrorJsonViewer.tsx +26 -0
- package/src/features/Conversation/Error/InvalidAPIKey.tsx +16 -0
- package/src/features/Conversation/Error/InvalidAccessCode.tsx +50 -0
- package/src/features/Conversation/Error/OpenAiBizError.tsx +9 -17
- package/src/features/Conversation/Error/{Plugin/PluginSettings.tsx → PluginSettings.tsx} +8 -3
- package/src/features/Conversation/Error/index.tsx +75 -70
- package/src/features/Conversation/Error/style.tsx +9 -3
- package/src/features/Conversation/Extras/Assistant.tsx +2 -3
- package/src/features/Conversation/Plugins/Inspector/index.tsx +8 -2
- package/src/features/Conversation/components/ChatItem/index.tsx +9 -24
- package/src/features/Conversation/types/index.tsx +2 -5
- package/src/hooks/_header.ts +35 -0
- package/src/libs/agent-runtime/BaseAI.ts +9 -0
- package/src/libs/agent-runtime/azureOpenai/index.ts +72 -0
- package/src/libs/agent-runtime/bedrock/index.ts +134 -0
- package/src/libs/agent-runtime/error.ts +26 -0
- package/src/libs/agent-runtime/google/index.ts +160 -0
- package/src/libs/agent-runtime/index.ts +9 -0
- package/src/libs/agent-runtime/openai/index.test.ts +176 -0
- package/src/libs/agent-runtime/openai/index.ts +107 -0
- package/src/libs/agent-runtime/types/chat.ts +119 -0
- package/src/libs/agent-runtime/types/index.ts +2 -0
- package/src/libs/agent-runtime/types/type.ts +34 -0
- package/src/libs/agent-runtime/utils/createError.ts +10 -0
- package/src/libs/agent-runtime/utils/debugStream.ts +18 -0
- package/src/libs/agent-runtime/utils/env.ts +1 -0
- package/src/libs/agent-runtime/utils/handleOpenAIError.ts +39 -0
- package/src/libs/agent-runtime/utils/uriParser.ts +16 -0
- package/src/libs/agent-runtime/zhipu/authToken.ts +22 -0
- package/src/libs/agent-runtime/zhipu/index.ts +125 -0
- package/src/locales/default/chat.ts +12 -2
- package/src/locales/default/common.ts +16 -0
- package/src/locales/default/error.ts +42 -5
- package/src/locales/default/setting.ts +61 -12
- package/src/services/__tests__/chat.test.ts +8 -2
- package/src/services/__tests__/message.test.ts +1 -1
- package/src/services/_auth.ts +106 -0
- package/src/services/_header.ts +1 -0
- package/src/services/_url.ts +0 -2
- package/src/services/chat.ts +34 -14
- package/src/store/chat/slices/message/action.ts +9 -6
- package/src/store/global/slices/common/action.ts +8 -2
- package/src/store/global/slices/settings/action.test.ts +2 -2
- package/src/store/global/slices/settings/action.ts +11 -4
- package/src/store/global/slices/settings/selectors/__snapshots__/modelProvider.test.ts.snap +24 -92
- package/src/store/global/slices/settings/selectors/__snapshots__/selectors.test.ts.snap +2 -0
- package/src/store/global/slices/settings/selectors/modelProvider.test.ts +127 -50
- package/src/store/global/slices/settings/selectors/modelProvider.ts +145 -43
- package/src/store/session/slices/agent/selectors.test.ts +0 -48
- package/src/store/session/slices/agent/selectors.ts +7 -12
- package/src/types/agent/index.ts +6 -2
- package/src/types/fetch.ts +4 -2
- package/src/types/files.ts +3 -3
- package/src/types/llm.ts +34 -0
- package/src/types/message/index.ts +7 -3
- package/src/types/openai/chat.ts +6 -11
- package/src/types/settings/index.ts +1 -0
- package/src/types/settings/modelProvider.ts +37 -1
- package/src/utils/__snapshots__/parseModels.test.ts.snap +63 -0
- package/src/utils/parseModels.test.ts +24 -0
- package/src/utils/parseModels.ts +37 -0
- package/src/app/api/openai/chat/createChatCompletion.test.ts +0 -166
- package/src/app/api/openai/chat/createChatCompletion.ts +0 -76
- package/src/app/api/openai/chat/route.ts +0 -19
- package/src/app/api/openai/models/route.ts +0 -17
- package/src/app/settings/llm/LLM/ModelList.tsx +0 -51
- package/src/app/settings/llm/LLM/getModelList.ts +0 -9
- package/src/app/settings/llm/index.tsx +0 -36
- package/src/const/llm.ts +0 -32
- package/src/features/Conversation/Error/ApiKeyForm.tsx +0 -87
- package/src/features/Conversation/Error/ErrorActionContainer.tsx +0 -23
- package/src/features/Conversation/Error/InvalidAccess.tsx +0 -90
- package/src/features/Conversation/Error/OpenAPIKey.tsx +0 -13
- package/src/features/Conversation/Error/Plugin/PluginError.tsx +0 -30
- package/src/services/modelList.ts +0 -15
- /package/src/app/chat/(desktop)/features/ChatInput/{Header → Footer}/DragUpload.tsx +0 -0
- /package/src/{app/api/openai/chat → libs/agent-runtime/utils}/desensitizeUrl.test.ts +0 -0
- /package/src/{app/api/openai/chat → libs/agent-runtime/utils}/desensitizeUrl.ts +0 -0
- /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,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
|
+
};
|