@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/changelog/v1.json +21 -0
  3. package/locales/ar/error.json +4 -1
  4. package/locales/ar/modelProvider.json +7 -0
  5. package/locales/ar/models.json +3 -12
  6. package/locales/ar/providers.json +3 -0
  7. package/locales/bg-BG/error.json +4 -1
  8. package/locales/bg-BG/modelProvider.json +7 -0
  9. package/locales/bg-BG/models.json +3 -12
  10. package/locales/bg-BG/providers.json +3 -0
  11. package/locales/de-DE/error.json +4 -1
  12. package/locales/de-DE/modelProvider.json +7 -0
  13. package/locales/de-DE/models.json +3 -12
  14. package/locales/de-DE/providers.json +3 -0
  15. package/locales/en-US/error.json +4 -1
  16. package/locales/en-US/modelProvider.json +7 -0
  17. package/locales/en-US/models.json +3 -12
  18. package/locales/en-US/providers.json +3 -0
  19. package/locales/es-ES/error.json +4 -1
  20. package/locales/es-ES/modelProvider.json +7 -0
  21. package/locales/es-ES/models.json +3 -12
  22. package/locales/es-ES/providers.json +3 -0
  23. package/locales/fa-IR/error.json +4 -1
  24. package/locales/fa-IR/modelProvider.json +7 -0
  25. package/locales/fa-IR/models.json +3 -12
  26. package/locales/fa-IR/providers.json +3 -0
  27. package/locales/fr-FR/error.json +4 -1
  28. package/locales/fr-FR/modelProvider.json +7 -0
  29. package/locales/fr-FR/models.json +3 -12
  30. package/locales/fr-FR/providers.json +3 -0
  31. package/locales/it-IT/error.json +4 -1
  32. package/locales/it-IT/modelProvider.json +7 -0
  33. package/locales/it-IT/models.json +3 -12
  34. package/locales/it-IT/providers.json +3 -0
  35. package/locales/ja-JP/error.json +4 -1
  36. package/locales/ja-JP/modelProvider.json +7 -0
  37. package/locales/ja-JP/models.json +3 -12
  38. package/locales/ja-JP/providers.json +3 -0
  39. package/locales/ko-KR/error.json +4 -1
  40. package/locales/ko-KR/modelProvider.json +7 -0
  41. package/locales/ko-KR/models.json +3 -12
  42. package/locales/ko-KR/providers.json +3 -0
  43. package/locales/nl-NL/error.json +4 -1
  44. package/locales/nl-NL/modelProvider.json +7 -0
  45. package/locales/nl-NL/models.json +3 -12
  46. package/locales/nl-NL/providers.json +3 -0
  47. package/locales/pl-PL/error.json +4 -1
  48. package/locales/pl-PL/modelProvider.json +7 -0
  49. package/locales/pl-PL/models.json +3 -12
  50. package/locales/pl-PL/providers.json +3 -0
  51. package/locales/pt-BR/error.json +4 -1
  52. package/locales/pt-BR/modelProvider.json +7 -0
  53. package/locales/pt-BR/models.json +3 -12
  54. package/locales/pt-BR/providers.json +3 -0
  55. package/locales/ru-RU/error.json +4 -1
  56. package/locales/ru-RU/modelProvider.json +7 -0
  57. package/locales/ru-RU/models.json +3 -12
  58. package/locales/ru-RU/providers.json +3 -0
  59. package/locales/tr-TR/error.json +4 -1
  60. package/locales/tr-TR/modelProvider.json +7 -0
  61. package/locales/tr-TR/models.json +3 -12
  62. package/locales/tr-TR/providers.json +3 -0
  63. package/locales/vi-VN/error.json +4 -1
  64. package/locales/vi-VN/modelProvider.json +7 -0
  65. package/locales/vi-VN/models.json +3 -12
  66. package/locales/vi-VN/providers.json +3 -0
  67. package/locales/zh-CN/error.json +5 -2
  68. package/locales/zh-CN/modelProvider.json +7 -0
  69. package/locales/zh-CN/models.json +3 -12
  70. package/locales/zh-CN/providers.json +3 -0
  71. package/locales/zh-TW/error.json +4 -1
  72. package/locales/zh-TW/modelProvider.json +7 -0
  73. package/locales/zh-TW/models.json +3 -12
  74. package/locales/zh-TW/providers.json +3 -0
  75. package/package.json +2 -1
  76. package/src/app/(backend)/middleware/auth/index.ts +14 -1
  77. package/src/app/(backend)/webapi/chat/vertexai/route.ts +35 -0
  78. package/src/app/[variants]/(main)/settings/provider/(detail)/huggingface/page.tsx +3 -3
  79. package/src/app/[variants]/(main)/settings/provider/(detail)/vertexai/page.tsx +67 -0
  80. package/src/config/aiModels/index.ts +3 -0
  81. package/src/config/aiModels/vertexai.ts +200 -0
  82. package/src/config/modelProviders/index.ts +3 -0
  83. package/src/config/modelProviders/vertexai.ts +22 -0
  84. package/src/database/client/db.ts +2 -1
  85. package/src/features/Conversation/Error/index.tsx +3 -5
  86. package/src/features/Conversation/Messages/User/MarkdownRender/ContentPreview.tsx +6 -0
  87. package/src/libs/agent-runtime/error.ts +5 -4
  88. package/src/libs/agent-runtime/google/index.ts +22 -4
  89. package/src/libs/agent-runtime/types/type.ts +1 -0
  90. package/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +22 -0
  91. package/src/libs/agent-runtime/utils/streams/vertex-ai.test.ts +236 -0
  92. package/src/libs/agent-runtime/utils/streams/vertex-ai.ts +75 -0
  93. package/src/libs/agent-runtime/vertexai/index.ts +23 -0
  94. package/src/locales/default/error.ts +5 -4
  95. package/src/locales/default/modelProvider.ts +7 -0
  96. package/src/types/fetch.ts +1 -0
  97. package/src/types/user/settings/keyVaults.ts +1 -0
  98. package/src/utils/errorResponse.test.ts +0 -12
  99. package/src/utils/errorResponse.ts +7 -2
  100. package/src/utils/safeParseJSON.ts +1 -1
  101. 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
- if (typeof Worker !== 'undefined') {
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 }: { apiKey?: string; baseURL?: string } = {}) {
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
- if (process.env.DEBUG_GOOGLE_CHAT_COMPLETION === '1') {
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 stream = GoogleGenerativeAIStream(prod, options?.callback);
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 });
@@ -66,6 +66,7 @@ export enum ModelProvider {
66
66
  TogetherAI = 'togetherai',
67
67
  Upstage = 'upstage',
68
68
  VLLM = 'vllm',
69
+ VertexAI = 'vertexai',
69
70
  Volcengine = 'volcengine',
70
71
  Wenxin = 'wenxin',
71
72
  XAI = 'xai',
@@ -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
  },
@@ -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范围内的情况