@lobehub/chat 1.60.9 → 1.61.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/CHANGELOG.md +58 -0
- package/changelog/v1.json +21 -0
- package/locales/ar/error.json +4 -1
- package/locales/ar/modelProvider.json +7 -0
- package/locales/ar/models.json +3 -12
- package/locales/ar/providers.json +3 -0
- package/locales/bg-BG/error.json +4 -1
- package/locales/bg-BG/modelProvider.json +7 -0
- package/locales/bg-BG/models.json +3 -12
- package/locales/bg-BG/providers.json +3 -0
- package/locales/de-DE/error.json +4 -1
- package/locales/de-DE/modelProvider.json +7 -0
- package/locales/de-DE/models.json +3 -12
- package/locales/de-DE/providers.json +3 -0
- package/locales/en-US/error.json +4 -1
- package/locales/en-US/modelProvider.json +7 -0
- package/locales/en-US/models.json +3 -12
- package/locales/en-US/providers.json +3 -0
- package/locales/es-ES/error.json +4 -1
- package/locales/es-ES/modelProvider.json +7 -0
- package/locales/es-ES/models.json +3 -12
- package/locales/es-ES/providers.json +3 -0
- package/locales/fa-IR/error.json +4 -1
- package/locales/fa-IR/modelProvider.json +7 -0
- package/locales/fa-IR/models.json +3 -12
- package/locales/fa-IR/providers.json +3 -0
- package/locales/fr-FR/error.json +4 -1
- package/locales/fr-FR/modelProvider.json +7 -0
- package/locales/fr-FR/models.json +3 -12
- package/locales/fr-FR/providers.json +3 -0
- package/locales/it-IT/error.json +4 -1
- package/locales/it-IT/modelProvider.json +7 -0
- package/locales/it-IT/models.json +3 -12
- package/locales/it-IT/providers.json +3 -0
- package/locales/ja-JP/error.json +4 -1
- package/locales/ja-JP/modelProvider.json +7 -0
- package/locales/ja-JP/models.json +3 -12
- package/locales/ja-JP/providers.json +3 -0
- package/locales/ko-KR/error.json +4 -1
- package/locales/ko-KR/modelProvider.json +7 -0
- package/locales/ko-KR/models.json +3 -12
- package/locales/ko-KR/providers.json +3 -0
- package/locales/nl-NL/error.json +4 -1
- package/locales/nl-NL/modelProvider.json +7 -0
- package/locales/nl-NL/models.json +3 -12
- package/locales/nl-NL/providers.json +3 -0
- package/locales/pl-PL/error.json +4 -1
- package/locales/pl-PL/modelProvider.json +7 -0
- package/locales/pl-PL/models.json +3 -12
- package/locales/pl-PL/providers.json +3 -0
- package/locales/pt-BR/error.json +4 -1
- package/locales/pt-BR/modelProvider.json +7 -0
- package/locales/pt-BR/models.json +3 -12
- package/locales/pt-BR/providers.json +3 -0
- package/locales/ru-RU/error.json +4 -1
- package/locales/ru-RU/modelProvider.json +7 -0
- package/locales/ru-RU/models.json +3 -12
- package/locales/ru-RU/providers.json +3 -0
- package/locales/tr-TR/error.json +4 -1
- package/locales/tr-TR/modelProvider.json +7 -0
- package/locales/tr-TR/models.json +3 -12
- package/locales/tr-TR/providers.json +3 -0
- package/locales/vi-VN/error.json +4 -1
- package/locales/vi-VN/modelProvider.json +7 -0
- package/locales/vi-VN/models.json +3 -12
- package/locales/vi-VN/providers.json +3 -0
- package/locales/zh-CN/error.json +5 -2
- package/locales/zh-CN/modelProvider.json +7 -0
- package/locales/zh-CN/models.json +3 -12
- package/locales/zh-CN/providers.json +3 -0
- package/locales/zh-TW/error.json +4 -1
- package/locales/zh-TW/modelProvider.json +7 -0
- package/locales/zh-TW/models.json +3 -12
- package/locales/zh-TW/providers.json +3 -0
- package/package.json +2 -1
- package/src/app/(backend)/middleware/auth/index.ts +14 -1
- package/src/app/(backend)/webapi/chat/vertexai/route.ts +35 -0
- package/src/app/[variants]/(main)/settings/provider/(detail)/huggingface/page.tsx +3 -3
- package/src/app/[variants]/(main)/settings/provider/(detail)/vertexai/page.tsx +67 -0
- package/src/config/aiModels/index.ts +3 -0
- package/src/config/aiModels/vertexai.ts +200 -0
- package/src/config/modelProviders/index.ts +3 -0
- package/src/config/modelProviders/vertexai.ts +22 -0
- package/src/database/client/db.ts +2 -1
- package/src/features/Conversation/Error/index.tsx +3 -5
- package/src/features/Conversation/Messages/User/MarkdownRender/ContentPreview.tsx +6 -0
- package/src/libs/agent-runtime/error.ts +5 -4
- package/src/libs/agent-runtime/google/index.ts +22 -4
- package/src/libs/agent-runtime/types/type.ts +1 -0
- package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +22 -0
- package/src/libs/agent-runtime/utils/streams/vertex-ai.test.ts +236 -0
- package/src/libs/agent-runtime/utils/streams/vertex-ai.ts +75 -0
- package/src/libs/agent-runtime/vertexai/index.ts +23 -0
- package/src/locales/default/error.ts +5 -4
- package/src/locales/default/modelProvider.ts +7 -0
- package/src/types/fetch.ts +1 -0
- package/src/types/user/settings/keyVaults.ts +1 -0
- package/src/utils/errorResponse.test.ts +0 -12
- package/src/utils/errorResponse.ts +7 -2
- package/src/utils/safeParseJSON.ts +1 -1
- package/src/features/Conversation/Error/OpenAiBizError.tsx +0 -29
@@ -40,6 +40,7 @@ import TaichuProvider from './taichu';
|
|
40
40
|
import TencentcloudProvider from './tencentcloud';
|
41
41
|
import TogetherAIProvider from './togetherai';
|
42
42
|
import UpstageProvider from './upstage';
|
43
|
+
import VertexAIProvider from './vertexai';
|
43
44
|
import VLLMProvider from './vllm';
|
44
45
|
import VolcengineProvider from './volcengine';
|
45
46
|
import WenxinProvider from './wenxin';
|
@@ -102,6 +103,7 @@ export const DEFAULT_MODEL_PROVIDER_LIST = [
|
|
102
103
|
AnthropicProvider,
|
103
104
|
BedrockProvider,
|
104
105
|
GoogleProvider,
|
106
|
+
VertexAIProvider,
|
105
107
|
DeepSeekProvider,
|
106
108
|
HuggingFaceProvider,
|
107
109
|
OpenRouterProvider,
|
@@ -191,6 +193,7 @@ export { default as TaichuProviderCard } from './taichu';
|
|
191
193
|
export { default as TencentCloudProviderCard } from './tencentcloud';
|
192
194
|
export { default as TogetherAIProviderCard } from './togetherai';
|
193
195
|
export { default as UpstageProviderCard } from './upstage';
|
196
|
+
export { default as VertexAIProviderCard } from './vertexai';
|
194
197
|
export { default as VLLMProviderCard } from './vllm';
|
195
198
|
export { default as VolcengineProviderCard } from './volcengine';
|
196
199
|
export { default as WenxinProviderCard } from './wenxin';
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { ModelProviderCard } from '@/types/llm';
|
2
|
+
|
3
|
+
// ref: https://ai.google.dev/gemini-api/docs/models/gemini
|
4
|
+
const VertexAI: ModelProviderCard = {
|
5
|
+
chatModels: [],
|
6
|
+
checkModel: 'gemini-1.5-flash-001',
|
7
|
+
description:
|
8
|
+
'Google 的 Gemini 系列是其最先进、通用的 AI模型,由 Google DeepMind 打造,专为多模态设计,支持文本、代码、图像、音频和视频的无缝理解与处理。适用于从数据中心到移动设备的多种环境,极大提升了AI模型的效率与应用广泛性。',
|
9
|
+
id: 'vertexai',
|
10
|
+
modelsUrl: 'https://console.cloud.google.com/vertex-ai/model-garden',
|
11
|
+
name: 'VertexAI',
|
12
|
+
settings: {
|
13
|
+
disableBrowserRequest: true,
|
14
|
+
smoothing: {
|
15
|
+
speed: 2,
|
16
|
+
text: true,
|
17
|
+
},
|
18
|
+
},
|
19
|
+
url: 'https://cloud.google.com/vertex-ai',
|
20
|
+
};
|
21
|
+
|
22
|
+
export default VertexAI;
|
@@ -201,7 +201,8 @@ export class DatabaseManager {
|
|
201
201
|
const dbName = 'lobechat';
|
202
202
|
|
203
203
|
// make db as web worker if worker is available
|
204
|
-
|
204
|
+
// https://github.com/lobehub/lobe-chat/issues/5785
|
205
|
+
if (typeof Worker !== 'undefined' && typeof navigator.locks !== 'undefined') {
|
205
206
|
db = await initPgliteWorker({
|
206
207
|
dbName,
|
207
208
|
fsBundle: fsBundle as Blob,
|
@@ -14,7 +14,6 @@ import ClerkLogin from './ClerkLogin';
|
|
14
14
|
import ErrorJsonViewer from './ErrorJsonViewer';
|
15
15
|
import InvalidAPIKey from './InvalidAPIKey';
|
16
16
|
import InvalidAccessCode from './InvalidAccessCode';
|
17
|
-
import OpenAiBizError from './OpenAiBizError';
|
18
17
|
|
19
18
|
const loading = () => <Skeleton active />;
|
20
19
|
|
@@ -34,8 +33,11 @@ const getErrorAlertConfig = (
|
|
34
33
|
};
|
35
34
|
|
36
35
|
switch (errorType) {
|
36
|
+
case ChatErrorType.SystemTimeNotMatchError:
|
37
37
|
case AgentRuntimeErrorType.PermissionDenied:
|
38
|
+
case AgentRuntimeErrorType.InsufficientQuota:
|
38
39
|
case AgentRuntimeErrorType.QuotaLimitReached:
|
40
|
+
case AgentRuntimeErrorType.ExceededContextWindow:
|
39
41
|
case AgentRuntimeErrorType.LocationNotSupportError: {
|
40
42
|
return {
|
41
43
|
type: 'warning',
|
@@ -82,10 +84,6 @@ const ErrorMessageExtra = memo<{ data: ChatMessage }>(({ data }) => {
|
|
82
84
|
return <PluginSettings id={data.id} plugin={data.plugin} />;
|
83
85
|
}
|
84
86
|
|
85
|
-
case AgentRuntimeErrorType.OpenAIBizError: {
|
86
|
-
return <OpenAiBizError {...data} />;
|
87
|
-
}
|
88
|
-
|
89
87
|
case AgentRuntimeErrorType.OllamaBizError: {
|
90
88
|
return <OllamaBizError {...data} />;
|
91
89
|
}
|
@@ -15,9 +15,13 @@ const useStyles = createStyles(({ css, token, isDarkMode }, displayMode: 'chat'
|
|
15
15
|
|
16
16
|
return {
|
17
17
|
mask: css`
|
18
|
+
pointer-events: none;
|
19
|
+
|
18
20
|
position: absolute;
|
19
21
|
inset-block: 0 0;
|
22
|
+
|
20
23
|
width: 100%;
|
24
|
+
|
21
25
|
background: linear-gradient(0deg, ${maskBgColor} 0%, transparent 50%);
|
22
26
|
`,
|
23
27
|
};
|
@@ -44,10 +48,12 @@ const ContentPreview = ({ content, id, displayMode }: ContentPreviewProps) => {
|
|
44
48
|
<Flexbox padding={4}>
|
45
49
|
<Button
|
46
50
|
block
|
51
|
+
color={'default'}
|
47
52
|
onClick={() => {
|
48
53
|
openMessageDetail(id);
|
49
54
|
}}
|
50
55
|
size={'small'}
|
56
|
+
variant={'filled'}
|
51
57
|
>
|
52
58
|
{t('chatList.longMessageDetail')}
|
53
59
|
</Button>
|
@@ -3,8 +3,12 @@
|
|
3
3
|
export const AgentRuntimeErrorType = {
|
4
4
|
AgentRuntimeError: 'AgentRuntimeError', // Agent Runtime 模块运行时错误
|
5
5
|
LocationNotSupportError: 'LocationNotSupportError',
|
6
|
+
|
6
7
|
QuotaLimitReached: 'QuotaLimitReached',
|
8
|
+
InsufficientQuota: 'InsufficientQuota',
|
9
|
+
|
7
10
|
PermissionDenied: 'PermissionDenied',
|
11
|
+
ExceededContextWindow: 'ExceededContextWindow',
|
8
12
|
|
9
13
|
InvalidProviderAPIKey: 'InvalidProviderAPIKey',
|
10
14
|
ProviderBizError: 'ProviderBizError',
|
@@ -13,6 +17,7 @@ export const AgentRuntimeErrorType = {
|
|
13
17
|
OllamaBizError: 'OllamaBizError',
|
14
18
|
|
15
19
|
InvalidBedrockCredentials: 'InvalidBedrockCredentials',
|
20
|
+
InvalidVertexCredentials: 'InvalidVertexCredentials',
|
16
21
|
StreamChunkError: 'StreamChunkError',
|
17
22
|
|
18
23
|
InvalidGithubToken: 'InvalidGithubToken',
|
@@ -23,10 +28,6 @@ export const AgentRuntimeErrorType = {
|
|
23
28
|
* @deprecated
|
24
29
|
*/
|
25
30
|
NoOpenAIAPIKey: 'NoOpenAIAPIKey',
|
26
|
-
/**
|
27
|
-
* @deprecated
|
28
|
-
*/
|
29
|
-
OpenAIBizError: 'OpenAIBizError',
|
30
31
|
} as const;
|
31
32
|
|
32
33
|
export const AGENT_RUNTIME_ERROR_SET = new Set<string>(Object.values(AgentRuntimeErrorType));
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import type { VertexAI } from '@google-cloud/vertexai';
|
1
2
|
import {
|
2
3
|
Content,
|
3
4
|
FunctionCallPart,
|
@@ -9,6 +10,7 @@ import {
|
|
9
10
|
} from '@google/generative-ai';
|
10
11
|
|
11
12
|
import type { ChatModelCard } from '@/types/llm';
|
13
|
+
import { VertexAIStream } from '@/libs/agent-runtime/utils/streams/vertex-ai';
|
12
14
|
import { imageUrlToBase64 } from '@/utils/imageToBase64';
|
13
15
|
import { safeParseJSON } from '@/utils/safeParseJSON';
|
14
16
|
|
@@ -56,17 +58,27 @@ function getThreshold(model: string): HarmBlockThreshold {
|
|
56
58
|
|
57
59
|
const DEFAULT_BASE_URL = 'https://generativelanguage.googleapis.com';
|
58
60
|
|
61
|
+
interface LobeGoogleAIParams {
|
62
|
+
apiKey?: string;
|
63
|
+
baseURL?: string;
|
64
|
+
client?: GoogleGenerativeAI | VertexAI;
|
65
|
+
isVertexAi?: boolean;
|
66
|
+
}
|
67
|
+
|
59
68
|
export class LobeGoogleAI implements LobeRuntimeAI {
|
60
69
|
private client: GoogleGenerativeAI;
|
70
|
+
private isVertexAi: boolean;
|
61
71
|
baseURL?: string;
|
62
72
|
apiKey?: string;
|
63
73
|
|
64
|
-
constructor({ apiKey, baseURL }:
|
74
|
+
constructor({ apiKey, baseURL, client, isVertexAi }: LobeGoogleAIParams = {}) {
|
65
75
|
if (!apiKey) throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidProviderAPIKey);
|
66
76
|
|
67
77
|
this.client = new GoogleGenerativeAI(apiKey);
|
68
|
-
this.baseURL = baseURL || DEFAULT_BASE_URL;
|
69
78
|
this.apiKey = apiKey;
|
79
|
+
this.client = client ? (client as GoogleGenerativeAI) : new GoogleGenerativeAI(apiKey);
|
80
|
+
this.baseURL = client ? undefined : baseURL || DEFAULT_BASE_URL;
|
81
|
+
this.isVertexAi = isVertexAi || false;
|
70
82
|
}
|
71
83
|
|
72
84
|
async chat(rawPayload: ChatStreamPayload, options?: ChatCompetitionOptions) {
|
@@ -117,18 +129,24 @@ export class LobeGoogleAI implements LobeRuntimeAI {
|
|
117
129
|
const googleStream = convertIterableToStream(geminiStreamResult.stream);
|
118
130
|
const [prod, useForDebug] = googleStream.tee();
|
119
131
|
|
120
|
-
|
132
|
+
const key = this.isVertexAi
|
133
|
+
? 'DEBUG_VERTEX_AI_CHAT_COMPLETION'
|
134
|
+
: 'DEBUG_GOOGLE_CHAT_COMPLETION';
|
135
|
+
|
136
|
+
if (process.env[key] === '1') {
|
121
137
|
debugStream(useForDebug).catch();
|
122
138
|
}
|
123
139
|
|
124
140
|
// Convert the response into a friendly text-stream
|
125
|
-
const
|
141
|
+
const Stream = this.isVertexAi ? VertexAIStream : GoogleGenerativeAIStream;
|
142
|
+
const stream = Stream(prod, options?.callback);
|
126
143
|
|
127
144
|
// Respond with the stream
|
128
145
|
return StreamingResponse(stream, { headers: options?.headers });
|
129
146
|
} catch (e) {
|
130
147
|
const err = e as Error;
|
131
148
|
|
149
|
+
console.log(err);
|
132
150
|
const { errorType, error } = this.parseErrorMessage(err.message);
|
133
151
|
|
134
152
|
throw AgentRuntimeError.chat({ error, errorType, provider: ModelProvider.Google });
|
@@ -386,6 +386,28 @@ export const LobeOpenAICompatibleFactory = <T extends Record<string, any> = any>
|
|
386
386
|
|
387
387
|
const { errorResult, RuntimeError } = handleOpenAIError(error);
|
388
388
|
|
389
|
+
switch (errorResult.code) {
|
390
|
+
case 'insufficient_quota': {
|
391
|
+
return AgentRuntimeError.chat({
|
392
|
+
endpoint: desensitizedEndpoint,
|
393
|
+
error: errorResult,
|
394
|
+
errorType: AgentRuntimeErrorType.InsufficientQuota,
|
395
|
+
provider: provider as ModelProvider,
|
396
|
+
});
|
397
|
+
}
|
398
|
+
|
399
|
+
// content too long
|
400
|
+
case 'context_length_exceeded':
|
401
|
+
case 'string_above_max_length': {
|
402
|
+
return AgentRuntimeError.chat({
|
403
|
+
endpoint: desensitizedEndpoint,
|
404
|
+
error: errorResult,
|
405
|
+
errorType: AgentRuntimeErrorType.ExceededContextWindow,
|
406
|
+
provider: provider as ModelProvider,
|
407
|
+
});
|
408
|
+
}
|
409
|
+
}
|
410
|
+
|
389
411
|
return AgentRuntimeError.chat({
|
390
412
|
endpoint: desensitizedEndpoint,
|
391
413
|
error: errorResult,
|
@@ -0,0 +1,236 @@
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
2
|
+
|
3
|
+
import * as uuidModule from '@/utils/uuid';
|
4
|
+
|
5
|
+
import { VertexAIStream } from './vertex-ai';
|
6
|
+
|
7
|
+
describe('VertexAIStream', () => {
|
8
|
+
it('should transform Vertex AI stream to protocol stream', async () => {
|
9
|
+
vi.spyOn(uuidModule, 'nanoid').mockReturnValueOnce('1');
|
10
|
+
const rawChunks = [
|
11
|
+
{
|
12
|
+
candidates: [
|
13
|
+
{
|
14
|
+
content: { role: 'model', parts: [{ text: '你好' }] },
|
15
|
+
safetyRatings: [
|
16
|
+
{
|
17
|
+
category: 'HARM_CATEGORY_HATE_SPEECH',
|
18
|
+
probability: 'NEGLIGIBLE',
|
19
|
+
probabilityScore: 0.06298828,
|
20
|
+
severity: 'HARM_SEVERY_NEGLIGIBLE',
|
21
|
+
severityScore: 0.10986328,
|
22
|
+
},
|
23
|
+
{
|
24
|
+
category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
|
25
|
+
probability: 'NEGLIGIBLE',
|
26
|
+
probabilityScore: 0.05029297,
|
27
|
+
severity: 'HARM_SEVERITY_NEGLIGIBLE',
|
28
|
+
severityScore: 0.078125,
|
29
|
+
},
|
30
|
+
{
|
31
|
+
category: 'HARM_CATEGORY_HARASSMENT',
|
32
|
+
probability: 'NEGLIGIBLE',
|
33
|
+
probabilityScore: 0.19433594,
|
34
|
+
severity: 'HARM_SEVERITY_NEGLIGIBLE',
|
35
|
+
severityScore: 0.16015625,
|
36
|
+
},
|
37
|
+
{
|
38
|
+
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
|
39
|
+
probability: 'NEGLIGIBLE',
|
40
|
+
probabilityScore: 0.059326172,
|
41
|
+
severity: 'HARM_SEVERITY_NEGLIGIBLE',
|
42
|
+
severityScore: 0.064453125,
|
43
|
+
},
|
44
|
+
],
|
45
|
+
index: 0,
|
46
|
+
},
|
47
|
+
],
|
48
|
+
usageMetadata: {},
|
49
|
+
modelVersion: 'gemini-1.5-flash-001',
|
50
|
+
},
|
51
|
+
{
|
52
|
+
candidates: [
|
53
|
+
{
|
54
|
+
content: { role: 'model', parts: [{ text: '! 😊' }] },
|
55
|
+
safetyRatings: [
|
56
|
+
{
|
57
|
+
category: 'HARM_CATEGORY_HATE_SPEECH',
|
58
|
+
probability: 'NEGLIGIBLE',
|
59
|
+
probabilityScore: 0.052734375,
|
60
|
+
severity: 'HARM_SEVRITY_NEGLIGIBLE',
|
61
|
+
severityScore: 0.08642578,
|
62
|
+
},
|
63
|
+
{
|
64
|
+
category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
|
65
|
+
probability: 'NEGLIGIBLE',
|
66
|
+
probabilityScore: 0.071777344,
|
67
|
+
severity: 'HARM_SEVERITY_NEGLIGIBLE',
|
68
|
+
severityScore: 0.095214844,
|
69
|
+
},
|
70
|
+
{
|
71
|
+
category: 'HARM_CATEGORY_HARASSMENT',
|
72
|
+
probability: 'NEGLIGIBLE',
|
73
|
+
probabilityScore: 0.1640625,
|
74
|
+
severity: 'HARM_SEVERITY_NEGLIGIBLE',
|
75
|
+
severityScore: 0.10498047,
|
76
|
+
},
|
77
|
+
{
|
78
|
+
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
|
79
|
+
probability: 'NEGLIGIBLE',
|
80
|
+
probabilityScore: 0.075683594,
|
81
|
+
severity: 'HARM_SEVERITY_NEGLIGIBLE',
|
82
|
+
severityScore: 0.053466797,
|
83
|
+
},
|
84
|
+
],
|
85
|
+
index: 0,
|
86
|
+
},
|
87
|
+
],
|
88
|
+
modelVersion: 'gemini-1.5-flash-001',
|
89
|
+
},
|
90
|
+
];
|
91
|
+
|
92
|
+
const mockGoogleStream = new ReadableStream({
|
93
|
+
start(controller) {
|
94
|
+
rawChunks.forEach((chunk) => controller.enqueue(chunk));
|
95
|
+
|
96
|
+
controller.close();
|
97
|
+
},
|
98
|
+
});
|
99
|
+
|
100
|
+
const onStartMock = vi.fn();
|
101
|
+
const onTextMock = vi.fn();
|
102
|
+
const onTokenMock = vi.fn();
|
103
|
+
const onToolCallMock = vi.fn();
|
104
|
+
const onCompletionMock = vi.fn();
|
105
|
+
|
106
|
+
const protocolStream = VertexAIStream(mockGoogleStream, {
|
107
|
+
onStart: onStartMock,
|
108
|
+
onText: onTextMock,
|
109
|
+
onToken: onTokenMock,
|
110
|
+
onToolCall: onToolCallMock,
|
111
|
+
onCompletion: onCompletionMock,
|
112
|
+
});
|
113
|
+
|
114
|
+
const decoder = new TextDecoder();
|
115
|
+
const chunks = [];
|
116
|
+
|
117
|
+
// @ts-ignore
|
118
|
+
for await (const chunk of protocolStream) {
|
119
|
+
chunks.push(decoder.decode(chunk, { stream: true }));
|
120
|
+
}
|
121
|
+
|
122
|
+
expect(chunks).toEqual([
|
123
|
+
// text
|
124
|
+
'id: chat_1\n',
|
125
|
+
'event: text\n',
|
126
|
+
`data: "你好"\n\n`,
|
127
|
+
|
128
|
+
// text
|
129
|
+
'id: chat_1\n',
|
130
|
+
'event: text\n',
|
131
|
+
`data: "! 😊"\n\n`,
|
132
|
+
]);
|
133
|
+
|
134
|
+
expect(onStartMock).toHaveBeenCalledTimes(1);
|
135
|
+
expect(onTokenMock).toHaveBeenCalledTimes(2);
|
136
|
+
expect(onCompletionMock).toHaveBeenCalledTimes(1);
|
137
|
+
});
|
138
|
+
|
139
|
+
it('tool_calls', async () => {
|
140
|
+
vi.spyOn(uuidModule, 'nanoid').mockReturnValueOnce('1');
|
141
|
+
const rawChunks = [
|
142
|
+
{
|
143
|
+
candidates: [
|
144
|
+
{
|
145
|
+
content: {
|
146
|
+
role: 'model',
|
147
|
+
parts: [
|
148
|
+
{
|
149
|
+
functionCall: {
|
150
|
+
name: 'realtime-weather____fetchCurrentWeather',
|
151
|
+
args: { city: '杭州' },
|
152
|
+
},
|
153
|
+
},
|
154
|
+
],
|
155
|
+
},
|
156
|
+
finishReason: 'STOP',
|
157
|
+
safetyRatings: [
|
158
|
+
{
|
159
|
+
category: 'HARM_CATERY_HATE_SPEECH',
|
160
|
+
probability: 'NEGLIGIBLE',
|
161
|
+
probabilityScore: 0.09814453,
|
162
|
+
severity: 'HARM_SEVERITY_NEGLIGIBLE',
|
163
|
+
severityScore: 0.07470703,
|
164
|
+
},
|
165
|
+
{
|
166
|
+
category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
|
167
|
+
probability: 'NEGLIGIBLE',
|
168
|
+
probabilityScore: 0.1484375,
|
169
|
+
severity: 'HARM_SEVERITY_NEGLIGIBLE',
|
170
|
+
severityScore: 0.15136719,
|
171
|
+
},
|
172
|
+
{
|
173
|
+
category: 'HARM_CATEGORY_HARASSMENT',
|
174
|
+
probability: 'NEGLIGIBLE',
|
175
|
+
probabilityScore: 0.11279297,
|
176
|
+
severity: 'HARM_SEVERITY_NEGLIGIBLE',
|
177
|
+
severityScore: 0.10107422,
|
178
|
+
},
|
179
|
+
{
|
180
|
+
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
|
181
|
+
probability: 'NEGLIGIBLE',
|
182
|
+
probabilityScore: 0.048828125,
|
183
|
+
severity: 'HARM_SEVERITY_NEGLIGIBLE',
|
184
|
+
severityScore: 0.05493164,
|
185
|
+
},
|
186
|
+
],
|
187
|
+
index: 0,
|
188
|
+
},
|
189
|
+
],
|
190
|
+
usageMetadata: { promptTokenCount: 95, candidatesTokenCount: 9, totalTokenCount: 104 },
|
191
|
+
modelVersion: 'gemini-1.5-flash-001',
|
192
|
+
},
|
193
|
+
];
|
194
|
+
|
195
|
+
const mockGoogleStream = new ReadableStream({
|
196
|
+
start(controller) {
|
197
|
+
rawChunks.forEach((chunk) => controller.enqueue(chunk));
|
198
|
+
|
199
|
+
controller.close();
|
200
|
+
},
|
201
|
+
});
|
202
|
+
|
203
|
+
const onStartMock = vi.fn();
|
204
|
+
const onTextMock = vi.fn();
|
205
|
+
const onTokenMock = vi.fn();
|
206
|
+
const onToolCallMock = vi.fn();
|
207
|
+
const onCompletionMock = vi.fn();
|
208
|
+
|
209
|
+
const protocolStream = VertexAIStream(mockGoogleStream, {
|
210
|
+
onStart: onStartMock,
|
211
|
+
onText: onTextMock,
|
212
|
+
onToken: onTokenMock,
|
213
|
+
onToolCall: onToolCallMock,
|
214
|
+
onCompletion: onCompletionMock,
|
215
|
+
});
|
216
|
+
|
217
|
+
const decoder = new TextDecoder();
|
218
|
+
const chunks = [];
|
219
|
+
|
220
|
+
// @ts-ignore
|
221
|
+
for await (const chunk of protocolStream) {
|
222
|
+
chunks.push(decoder.decode(chunk, { stream: true }));
|
223
|
+
}
|
224
|
+
|
225
|
+
expect(chunks).toEqual([
|
226
|
+
// text
|
227
|
+
'id: chat_1\n',
|
228
|
+
'event: tool_calls\n',
|
229
|
+
`data: [{"function":{"arguments":"{\\"city\\":\\"杭州\\"}","name":"realtime-weather____fetchCurrentWeather"},"id":"realtime-weather____fetchCurrentWeather_0","index":0,"type":"function"}]\n\n`,
|
230
|
+
]);
|
231
|
+
|
232
|
+
expect(onStartMock).toHaveBeenCalledTimes(1);
|
233
|
+
expect(onToolCallMock).toHaveBeenCalledTimes(1);
|
234
|
+
expect(onCompletionMock).toHaveBeenCalledTimes(1);
|
235
|
+
});
|
236
|
+
});
|
@@ -0,0 +1,75 @@
|
|
1
|
+
import { EnhancedGenerateContentResponse, GenerateContentResponse } from '@google/generative-ai';
|
2
|
+
|
3
|
+
import { nanoid } from '@/utils/uuid';
|
4
|
+
|
5
|
+
import { ChatStreamCallbacks } from '../../types';
|
6
|
+
import {
|
7
|
+
StreamProtocolChunk,
|
8
|
+
StreamStack,
|
9
|
+
createCallbacksTransformer,
|
10
|
+
createSSEProtocolTransformer,
|
11
|
+
generateToolCallId,
|
12
|
+
} from './protocol';
|
13
|
+
|
14
|
+
const transformVertexAIStream = (
|
15
|
+
chunk: GenerateContentResponse,
|
16
|
+
stack: StreamStack,
|
17
|
+
): StreamProtocolChunk => {
|
18
|
+
// maybe need another structure to add support for multiple choices
|
19
|
+
const candidates = chunk.candidates;
|
20
|
+
|
21
|
+
if (!candidates)
|
22
|
+
return {
|
23
|
+
data: '',
|
24
|
+
id: stack?.id,
|
25
|
+
type: 'text',
|
26
|
+
};
|
27
|
+
|
28
|
+
const item = candidates[0];
|
29
|
+
if (item.content) {
|
30
|
+
const part = item.content.parts[0];
|
31
|
+
|
32
|
+
if (part.functionCall) {
|
33
|
+
const functionCall = part.functionCall;
|
34
|
+
|
35
|
+
return {
|
36
|
+
data: [
|
37
|
+
{
|
38
|
+
function: {
|
39
|
+
arguments: JSON.stringify(functionCall.args),
|
40
|
+
name: functionCall.name,
|
41
|
+
},
|
42
|
+
id: generateToolCallId(0, functionCall.name),
|
43
|
+
index: 0,
|
44
|
+
type: 'function',
|
45
|
+
},
|
46
|
+
],
|
47
|
+
id: stack?.id,
|
48
|
+
type: 'tool_calls',
|
49
|
+
};
|
50
|
+
}
|
51
|
+
|
52
|
+
return {
|
53
|
+
data: part.text,
|
54
|
+
id: stack?.id,
|
55
|
+
type: 'text',
|
56
|
+
};
|
57
|
+
}
|
58
|
+
|
59
|
+
return {
|
60
|
+
data: '',
|
61
|
+
id: stack?.id,
|
62
|
+
type: 'stop',
|
63
|
+
};
|
64
|
+
};
|
65
|
+
|
66
|
+
export const VertexAIStream = (
|
67
|
+
rawStream: ReadableStream<EnhancedGenerateContentResponse>,
|
68
|
+
callbacks?: ChatStreamCallbacks,
|
69
|
+
) => {
|
70
|
+
const streamStack: StreamStack = { id: 'chat_' + nanoid() };
|
71
|
+
|
72
|
+
return rawStream
|
73
|
+
.pipeThrough(createSSEProtocolTransformer(transformVertexAIStream, streamStack))
|
74
|
+
.pipeThrough(createCallbacksTransformer(callbacks));
|
75
|
+
};
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import { VertexAI, VertexInit } from '@google-cloud/vertexai';
|
2
|
+
|
3
|
+
import { AgentRuntimeError, AgentRuntimeErrorType, LobeGoogleAI } from '@/libs/agent-runtime';
|
4
|
+
|
5
|
+
export class LobeVertexAI extends LobeGoogleAI {
|
6
|
+
static initFromVertexAI(params?: VertexInit) {
|
7
|
+
try {
|
8
|
+
const client = new VertexAI({ ...params });
|
9
|
+
|
10
|
+
return new LobeGoogleAI({ apiKey: 'avoid-error', client, isVertexAi: true });
|
11
|
+
} catch (e) {
|
12
|
+
const err = e as Error;
|
13
|
+
|
14
|
+
if (err.name === 'IllegalArgumentError') {
|
15
|
+
throw AgentRuntimeError.createError(AgentRuntimeErrorType.InvalidVertexCredentials, {
|
16
|
+
message: err.message,
|
17
|
+
});
|
18
|
+
}
|
19
|
+
|
20
|
+
throw e;
|
21
|
+
}
|
22
|
+
}
|
23
|
+
}
|
@@ -90,8 +90,12 @@ export default {
|
|
90
90
|
|
91
91
|
InvalidAccessCode: '密码不正确或为空,请输入正确的访问密码,或者添加自定义 API Key',
|
92
92
|
InvalidClerkUser: '很抱歉,你当前尚未登录,请先登录或注册账号后继续操作',
|
93
|
+
SystemTimeNotMatchError: '很抱歉,您的系统时间和服务器不匹配,请检查您的系统时间后重试',
|
93
94
|
LocationNotSupportError:
|
94
95
|
'很抱歉,你的所在地区不支持此模型服务,可能是由于区域限制或服务未开通。请确认当前地区是否支持使用此服务,或尝试使用切换到其他地区后重试。',
|
96
|
+
InsufficientQuota:
|
97
|
+
'很抱歉,该密钥的配额(quota)已达上限,请检查账户余额是否充足,或增大密钥配额后再试',
|
98
|
+
ExceededContextWindow: '当前请求内容超出模型可处理的长度,请减少内容量后重试',
|
95
99
|
QuotaLimitReached:
|
96
100
|
'很抱歉,当前 Token 用量或请求次数已达该密钥的配额(quota)上限,请增加该密钥的配额或稍后再试',
|
97
101
|
PermissionDenied: '很抱歉,你没有权限访问该服务,请检查你的密钥是否有访问权限',
|
@@ -101,11 +105,8 @@ export default {
|
|
101
105
|
* @deprecated
|
102
106
|
*/
|
103
107
|
NoOpenAIAPIKey: 'OpenAI API Key 不正确或为空,请添加自定义 OpenAI API Key',
|
104
|
-
/**
|
105
|
-
* @deprecated
|
106
|
-
*/
|
107
|
-
OpenAIBizError: '请求 OpenAI 服务出错,请根据以下信息排查或重试',
|
108
108
|
|
109
|
+
InvalidVertexCredentials: 'Vertex 鉴权未通过,请检查鉴权凭证后重试',
|
109
110
|
InvalidBedrockCredentials: 'Bedrock 鉴权未通过,请检查 AccessKeyId/SecretAccessKey 后重试',
|
110
111
|
StreamChunkError:
|
111
112
|
'流式请求的消息块解析错误,请检查当前 API 接口是否符合标准规范,或联系你的 API 供应商咨询',
|
@@ -325,6 +325,13 @@ export default {
|
|
325
325
|
tooltip: '更新服务商基础配置',
|
326
326
|
updateSuccess: '更新成功',
|
327
327
|
},
|
328
|
+
vertexai: {
|
329
|
+
apiKey: {
|
330
|
+
desc: '填入你的 Vertex Ai Keys',
|
331
|
+
placeholder: `{ "type": "service_account", "project_id": "xxx", "private_key_id": ... }`,
|
332
|
+
title: 'Vertex AI Keys',
|
333
|
+
},
|
334
|
+
},
|
328
335
|
zeroone: {
|
329
336
|
title: '01.AI 零一万物',
|
330
337
|
},
|
package/src/types/fetch.ts
CHANGED
@@ -13,6 +13,7 @@ export const ChatErrorType = {
|
|
13
13
|
OllamaServiceUnavailable: 'OllamaServiceUnavailable', // 未启动/检测到 Ollama 服务
|
14
14
|
PluginFailToTransformArguments: 'PluginFailToTransformArguments',
|
15
15
|
UnknownChatFetchError: 'UnknownChatFetchError',
|
16
|
+
SystemTimeNotMatchError: 'SystemTimeNotMatchError',
|
16
17
|
|
17
18
|
// ******* 客户端错误 ******* //
|
18
19
|
BadRequest: 400,
|
@@ -68,6 +68,7 @@ export interface UserKeyVaults {
|
|
68
68
|
tencentcloud?: OpenAICompatibleKeyVault;
|
69
69
|
togetherai?: OpenAICompatibleKeyVault;
|
70
70
|
upstage?: OpenAICompatibleKeyVault;
|
71
|
+
vertexai?: OpenAICompatibleKeyVault;
|
71
72
|
vllm?: OpenAICompatibleKeyVault;
|
72
73
|
volcengine?: OpenAICompatibleKeyVault;
|
73
74
|
wenxin?: OpenAICompatibleKeyVault;
|
@@ -33,12 +33,6 @@ describe('createErrorResponse', () => {
|
|
33
33
|
});
|
34
34
|
|
35
35
|
describe('Provider Biz Error', () => {
|
36
|
-
it('returns a 471 status for OpenAIBizError error type', () => {
|
37
|
-
const errorType = AgentRuntimeErrorType.OpenAIBizError;
|
38
|
-
const response = createErrorResponse(errorType);
|
39
|
-
expect(response.status).toBe(471);
|
40
|
-
});
|
41
|
-
|
42
36
|
it('returns a 471 status for ProviderBizError error type', () => {
|
43
37
|
const errorType = AgentRuntimeErrorType.ProviderBizError;
|
44
38
|
const response = createErrorResponse(errorType);
|
@@ -50,12 +44,6 @@ describe('createErrorResponse', () => {
|
|
50
44
|
const response = createErrorResponse(errorType);
|
51
45
|
expect(response.status).toBe(470);
|
52
46
|
});
|
53
|
-
|
54
|
-
it('returns a 471 status for OpenAIBizError error type', () => {
|
55
|
-
const errorType = AgentRuntimeErrorType.OpenAIBizError;
|
56
|
-
const response = createErrorResponse(errorType as any);
|
57
|
-
expect(response.status).toBe(471);
|
58
|
-
});
|
59
47
|
});
|
60
48
|
|
61
49
|
// 测试状态码不在200-599范围内的情况
|