Feat/vertex ai support (#6416)

* WIP

* feat: integrate Vertex AI support and enhance service account configuration

- Added Vertex AI service integration with authentication via service accounts.
- Implemented IPC channels for Vertex AI authentication and cache management.
- Updated UI components to support service account configuration, including private key and client email fields.
- Enhanced localization for Vertex AI settings in multiple languages.
- Refactored AiProvider to support dynamic provider creation for Vertex AI.
- Updated Redux store to manage Vertex AI settings and service account information.

* chore: remove debug script from package.json and clean up console log in main process

* fix: ensure async handling in useKnowledge hook for base parameters

- Updated the useKnowledge hook to await the result of getKnowledgeBaseParams when removing items, ensuring proper asynchronous behavior.

* fix: ensure async handling in KnowledgeQueue for base parameters

* fix(i18n): add English prompt placeholder to Russian localization

* chore(yarn): update yarn.lock and patch for @google/genai

* fix(AihubmixPage): update AI provider instantiation to use async create method

* refactor: update VertexAPIClient import and class definition

- Changed import statement for VertexAPIClient to use named import.
- Updated VertexProvider class to VertexAPIClient for consistency with naming conventions.

* refactor: update AiProvider instantiation across components

- Replaced the use of AiProvider.create() with the new AiProvider() constructor in AddKnowledgePopup, AihubmixPage, SiliconPage, and KnowledgeService for consistency and improved clarity.

* refactor: simplify getKnowledgeBaseParams and update API key checks

- Changed getKnowledgeBaseParams to a synchronous function for improved performance.
- Updated API key validation logic to remove unnecessary checks for 'vertexai' provider type across multiple functions.

* feat: add Cephalon provider configuration with API and website links

- Introduced a new provider configuration for Cephalon, including API URL and various website links for official resources, API key, documentation, and models.

* refactor: streamline API call in AddKnowledgePopup component

- Removed unnecessary await from the create API call in the AddKnowledgePopup component, improving code clarity and performance.

* refactor: remove unnecessary await from getKnowledgeBaseParams call

- Simplified the searchKnowledgeBase function by removing the await from getKnowledgeBaseParams, enhancing performance and code clarity.

* refactor: remove externalLiveBindings option from Rollup output configuration in electron.vite.config.ts
This commit is contained in:
SuYao 2025-06-16 21:46:27 +08:00 committed by GitHub
parent f48e7aadb8
commit 502cce70c2
24 changed files with 7229 additions and 80 deletions

File diff suppressed because one or more lines are too long

View File

@ -94,7 +94,7 @@
"@emotion/is-prop-valid": "^1.3.1",
"@eslint-react/eslint-plugin": "^1.36.1",
"@eslint/js": "^9.22.0",
"@google/genai": "^1.0.1",
"@google/genai": "patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch",
"@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0",
"@langchain/community": "^0.3.36",
@ -163,6 +163,7 @@
"fast-xml-parser": "^5.2.0",
"franc-min": "^6.2.0",
"fs-extra": "^11.2.0",
"google-auth-library": "^9.15.1",
"html-to-image": "^1.11.13",
"husky": "^9.1.7",
"i18next": "^23.11.5",

View File

@ -86,6 +86,10 @@ export enum IpcChannel {
Gemini_ListFiles = 'gemini:list-files',
Gemini_DeleteFile = 'gemini:delete-file',
// VertexAI
VertexAI_GetAuthHeaders = 'vertexai:get-auth-headers',
VertexAI_ClearAuthCache = 'vertexai:clear-auth-cache',
Windows_ResetMinimumSize = 'window:reset-minimum-size',
Windows_SetMinimumSize = 'window:set-minimum-size',

View File

@ -29,6 +29,7 @@ import { SelectionService } from './services/SelectionService'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
import storeSyncService from './services/StoreSyncService'
import { themeService } from './services/ThemeService'
import VertexAIService from './services/VertexAIService'
import { setOpenLinkExternal } from './services/WebviewService'
import { windowService } from './services/WindowService'
import { calculateDirectorySize, getResourcePath } from './utils'
@ -40,6 +41,7 @@ const fileManager = new FileStorage()
const backupManager = new BackupManager()
const exportService = new ExportService(fileManager)
const obsidianVaultService = new ObsidianVaultService()
const vertexAIService = VertexAIService.getInstance()
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const appUpdater = new AppUpdater(mainWindow)
@ -274,6 +276,15 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
}
})
// VertexAI
ipcMain.handle(IpcChannel.VertexAI_GetAuthHeaders, async (_, params) => {
return vertexAIService.getAuthHeaders(params)
})
ipcMain.handle(IpcChannel.VertexAI_ClearAuthCache, async (_, projectId: string, clientEmail?: string) => {
vertexAIService.clearAuthCache(projectId, clientEmail)
})
// mini window
ipcMain.handle(IpcChannel.MiniWindow_Show, () => windowService.showMiniWindow())
ipcMain.handle(IpcChannel.MiniWindow_Hide, () => windowService.hideMiniWindow())

View File

@ -0,0 +1,142 @@
import { GoogleAuth } from 'google-auth-library'
interface ServiceAccountCredentials {
privateKey: string
clientEmail: string
}
interface VertexAIAuthParams {
projectId: string
serviceAccount?: ServiceAccountCredentials
}
const REQUIRED_VERTEX_AI_SCOPE = 'https://www.googleapis.com/auth/cloud-platform'
class VertexAIService {
private static instance: VertexAIService
private authClients: Map<string, GoogleAuth> = new Map()
static getInstance(): VertexAIService {
if (!VertexAIService.instance) {
VertexAIService.instance = new VertexAIService()
}
return VertexAIService.instance
}
/**
* PEM头部和尾部
*/
private formatPrivateKey(privateKey: string): string {
if (!privateKey || typeof privateKey !== 'string') {
throw new Error('Private key must be a non-empty string')
}
// 处理JSON字符串中的转义换行符
let key = privateKey.replace(/\\n/g, '\n')
// 如果已经是正确格式的PEM直接返回
if (key.includes('-----BEGIN PRIVATE KEY-----') && key.includes('-----END PRIVATE KEY-----')) {
return key
}
// 移除所有换行符和空白字符(为了重新格式化)
key = key.replace(/\s+/g, '')
// 移除可能存在的头部和尾部
key = key.replace(/-----BEGIN[^-]*-----/g, '')
key = key.replace(/-----END[^-]*-----/g, '')
// 确保私钥不为空
if (!key) {
throw new Error('Private key is empty after formatting')
}
// 添加正确的PEM头部和尾部并格式化为64字符一行
const formattedKey = key.match(/.{1,64}/g)?.join('\n') || key
return `-----BEGIN PRIVATE KEY-----\n${formattedKey}\n-----END PRIVATE KEY-----`
}
/**
* Vertex AI
*/
async getAuthHeaders(params: VertexAIAuthParams): Promise<Record<string, string>> {
const { projectId, serviceAccount } = params
if (!serviceAccount?.privateKey || !serviceAccount?.clientEmail) {
throw new Error('Service account credentials are required')
}
// 创建缓存键
const cacheKey = `${projectId}-${serviceAccount.clientEmail}`
// 检查是否已有客户端实例
let auth = this.authClients.get(cacheKey)
if (!auth) {
try {
// 格式化私钥
const formattedPrivateKey = this.formatPrivateKey(serviceAccount.privateKey)
// 创建新的认证客户端
auth = new GoogleAuth({
credentials: {
private_key: formattedPrivateKey,
client_email: serviceAccount.clientEmail
},
projectId,
scopes: [REQUIRED_VERTEX_AI_SCOPE]
})
this.authClients.set(cacheKey, auth)
} catch (formatError: any) {
throw new Error(`Invalid private key format: ${formatError.message}`)
}
}
try {
// 获取认证头
const authHeaders = await auth.getRequestHeaders()
// 转换为普通对象
const headers: Record<string, string> = {}
for (const [key, value] of Object.entries(authHeaders)) {
if (typeof value === 'string') {
headers[key] = value
}
}
return headers
} catch (error: any) {
// 如果认证失败,清除缓存的客户端
this.authClients.delete(cacheKey)
throw new Error(`Failed to authenticate with service account: ${error.message}`)
}
}
/**
*
*/
clearAuthCache(projectId: string, clientEmail?: string): void {
if (clientEmail) {
const cacheKey = `${projectId}-${clientEmail}`
this.authClients.delete(cacheKey)
} else {
// 清理该项目的所有缓存
for (const [key] of this.authClients) {
if (key.startsWith(`${projectId}-`)) {
this.authClients.delete(key)
}
}
}
}
/**
*
*/
clearAllAuthCache(): void {
this.authClients.clear()
}
}
export default VertexAIService

View File

@ -129,6 +129,13 @@ const api = {
listFiles: (apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_ListFiles, apiKey),
deleteFile: (fileId: string, apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_DeleteFile, fileId, apiKey)
},
vertexAI: {
getAuthHeaders: (params: { projectId: string; serviceAccount?: { privateKey: string; clientEmail: string } }) =>
ipcRenderer.invoke(IpcChannel.VertexAI_GetAuthHeaders, params),
clearAuthCache: (projectId: string, clientEmail?: string) =>
ipcRenderer.invoke(IpcChannel.VertexAI_ClearAuthCache, projectId, clientEmail)
},
config: {
set: (key: string, value: any, isNotify: boolean = false) =>
ipcRenderer.invoke(IpcChannel.Config_Set, key, value, isNotify),

View File

@ -4,6 +4,7 @@ import { AihubmixAPIClient } from './AihubmixAPIClient'
import { AnthropicAPIClient } from './anthropic/AnthropicAPIClient'
import { BaseApiClient } from './BaseApiClient'
import { GeminiAPIClient } from './gemini/GeminiAPIClient'
import { VertexAPIClient } from './gemini/VertexAPIClient'
import { OpenAIAPIClient } from './openai/OpenAIApiClient'
import { OpenAIResponseAPIClient } from './openai/OpenAIResponseAPIClient'
@ -44,6 +45,9 @@ export class ApiClientFactory {
case 'gemini':
instance = new GeminiAPIClient(provider) as BaseApiClient
break
case 'vertexai':
instance = new VertexAPIClient(provider) as BaseApiClient
break
case 'anthropic':
instance = new AnthropicAPIClient(provider) as BaseApiClient
break

View File

@ -176,12 +176,23 @@ export class GeminiAPIClient extends BaseApiClient<
this.sdkInstance = new GoogleGenAI({
vertexai: false,
apiKey: this.apiKey,
httpOptions: { baseUrl: this.getBaseURL() }
apiVersion: this.getApiVersion(),
httpOptions: {
baseUrl: this.getBaseURL(),
apiVersion: this.getApiVersion()
}
})
return this.sdkInstance
}
protected getApiVersion(): string {
if (this.provider.isVertex) {
return 'v1'
}
return 'v1beta'
}
/**
* Handle a PDF file
* @param file - The file

View File

@ -0,0 +1,95 @@
import { GoogleGenAI } from '@google/genai'
import { getVertexAILocation, getVertexAIProjectId, getVertexAIServiceAccount } from '@renderer/hooks/useVertexAI'
import { Provider } from '@renderer/types'
import { GeminiAPIClient } from './GeminiAPIClient'
export class VertexAPIClient extends GeminiAPIClient {
private authHeaders?: Record<string, string>
private authHeadersExpiry?: number
constructor(provider: Provider) {
super(provider)
}
override async getSdkInstance() {
if (this.sdkInstance) {
return this.sdkInstance
}
const serviceAccount = getVertexAIServiceAccount()
const projectId = getVertexAIProjectId()
const location = getVertexAILocation()
if (!serviceAccount.privateKey || !serviceAccount.clientEmail || !projectId || !location) {
throw new Error('Vertex AI settings are not configured')
}
const authHeaders = await this.getServiceAccountAuthHeaders()
this.sdkInstance = new GoogleGenAI({
vertexai: true,
project: projectId,
location: location,
httpOptions: {
apiVersion: this.getApiVersion(),
headers: authHeaders
}
})
return this.sdkInstance
}
/**
* service account
*/
private async getServiceAccountAuthHeaders(): Promise<Record<string, string> | undefined> {
const serviceAccount = getVertexAIServiceAccount()
const projectId = getVertexAIProjectId()
// 检查是否配置了 service account
if (!serviceAccount.privateKey || !serviceAccount.clientEmail || !projectId) {
return undefined
}
// 检查是否已有有效的认证头(提前 5 分钟过期)
const now = Date.now()
if (this.authHeaders && this.authHeadersExpiry && this.authHeadersExpiry - now > 5 * 60 * 1000) {
return this.authHeaders
}
try {
// 从主进程获取认证头
this.authHeaders = await window.api.vertexAI.getAuthHeaders({
projectId,
serviceAccount: {
privateKey: serviceAccount.privateKey,
clientEmail: serviceAccount.clientEmail
}
})
// 设置过期时间(通常认证头有效期为 1 小时)
this.authHeadersExpiry = now + 60 * 60 * 1000
return this.authHeaders
} catch (error: any) {
console.error('Failed to get auth headers:', error)
throw new Error(`Service Account authentication failed: ${error.message}`)
}
}
/**
*
*/
clearAuthCache(): void {
this.authHeaders = undefined
this.authHeadersExpiry = undefined
const serviceAccount = getVertexAIServiceAccount()
const projectId = getVertexAIProjectId()
if (projectId && serviceAccount.clientEmail) {
window.api.vertexAI.clearAuthCache(projectId, serviceAccount.clientEmail)
}
}
}

View File

@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>VertexAI</title><path d="M11.995 20.216a1.892 1.892 0 100 3.785 1.892 1.892 0 000-3.785zm0 2.806a.927.927 0 11.927-.914.914.914 0 01-.927.914z" fill="#4285F4"></path><path clip-rule="evenodd" d="M21.687 14.144c.237.038.452.16.605.344a.978.978 0 01-.18 1.3l-8.24 6.082a1.892 1.892 0 00-1.147-1.508l8.28-6.08a.991.991 0 01.682-.138z" fill="#669DF6" fill-rule="evenodd"></path><path clip-rule="evenodd" d="M10.122 21.842l-8.217-6.066a.952.952 0 01-.206-1.287.978.978 0 011.287-.206l8.28 6.08a1.893 1.893 0 00-1.144 1.479z" fill="#AECBFA" fill-rule="evenodd"></path><path d="M4.273 4.475a.978.978 0 01-.965-.965V1.09a.978.978 0 111.943 0v2.42a.978.978 0 01-.978.965zM4.247 13.034a.978.978 0 100-1.956.978.978 0 000 1.956zM4.247 10.19a.978.978 0 100-1.956.978.978 0 000 1.956zM4.247 7.332a.978.978 0 100-1.956.978.978 0 000 1.956z" fill="#AECBFA"></path><path d="M19.718 7.307a.978.978 0 01-.965-.979v-2.42a.965.965 0 011.93 0v2.42a.964.964 0 01-.965.979zM19.743 13.047a.978.978 0 100-1.956.978.978 0 000 1.956zM19.743 10.151a.978.978 0 100-1.956.978.978 0 000 1.956zM19.743 2.068a.978.978 0 100-1.956.978.978 0 000 1.956z" fill="#4285F4"></path><path d="M11.995 15.917a.978.978 0 01-.965-.965v-2.459a.978.978 0 011.943 0v2.433a.976.976 0 01-.978.991zM11.995 18.762a.978.978 0 100-1.956.978.978 0 000 1.956zM11.995 10.64a.978.978 0 100-1.956.978.978 0 000 1.956zM11.995 7.783a.978.978 0 100-1.956.978.978 0 000 1.956z" fill="#669DF6"></path><path d="M15.856 10.177a.978.978 0 01-.965-.965v-2.42a.977.977 0 011.702-.763.979.979 0 01.241.763v2.42a.978.978 0 01-.978.965zM15.869 4.913a.978.978 0 100-1.956.978.978 0 000 1.956zM15.869 15.853a.978.978 0 100-1.956.978.978 0 000 1.956zM15.869 12.996a.978.978 0 100-1.956.978.978 0 000 1.956z" fill="#4285F4"></path><path d="M8.121 15.853a.978.978 0 100-1.956.978.978 0 000 1.956zM8.121 7.783a.978.978 0 100-1.956.978.978 0 000 1.956zM8.121 4.913a.978.978 0 100-1.957.978.978 0 000 1.957zM8.134 12.996a.978.978 0 01-.978-.94V9.611a.965.965 0 011.93 0v2.445a.966.966 0 01-.952.94z" fill="#AECBFA"></path></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -42,6 +42,7 @@ import StepProviderLogo from '@renderer/assets/images/providers/step.png'
import TencentCloudProviderLogo from '@renderer/assets/images/providers/tencent-cloud-ti.png'
import TogetherProviderLogo from '@renderer/assets/images/providers/together.png'
import TokenFluxProviderLogo from '@renderer/assets/images/providers/tokenflux.png'
import VertexAIProviderLogo from '@renderer/assets/images/providers/vertexai.svg'
import BytedanceProviderLogo from '@renderer/assets/images/providers/volcengine.png'
import VoyageAIProviderLogo from '@renderer/assets/images/providers/voyageai.png'
import XirangProviderLogo from '@renderer/assets/images/providers/xirang.png'
@ -100,7 +101,8 @@ const PROVIDER_LOGO_MAP = {
qiniu: QiniuProviderLogo,
tokenflux: TokenFluxProviderLogo,
cephalon: CephalonProviderLogo,
lanyun: LanyunProviderLogo
lanyun: LanyunProviderLogo,
vertexai: VertexAIProviderLogo
} as const
export function getProviderLogo(providerId: string) {
@ -651,5 +653,16 @@ export const PROVIDER_CONFIG = {
docs: 'https://archive.lanyun.net/maas/doc/',
models: 'https://maas.lanyun.net/api/#/model/modelSquare'
}
},
vertexai: {
api: {
url: 'https://console.cloud.google.com/apis/api/aiplatform.googleapis.com/overview'
},
websites: {
official: 'https://cloud.google.com/vertex-ai',
apiKey: 'https://console.cloud.google.com/apis/credentials',
docs: 'https://cloud.google.com/vertex-ai/generative-ai/docs',
models: 'https://cloud.google.com/vertex-ai/generative-ai/docs/learn/models'
}
}
}

View File

@ -0,0 +1,37 @@
import store, { useAppSelector } from '@renderer/store'
import {
setVertexAILocation,
setVertexAIProjectId,
setVertexAIServiceAccountClientEmail,
setVertexAIServiceAccountPrivateKey
} from '@renderer/store/llm'
import { useDispatch } from 'react-redux'
export function useVertexAISettings() {
const settings = useAppSelector((state) => state.llm.settings.vertexai)
const dispatch = useDispatch()
return {
...settings,
setProjectId: (projectId: string) => dispatch(setVertexAIProjectId(projectId)),
setLocation: (location: string) => dispatch(setVertexAILocation(location)),
setServiceAccountPrivateKey: (privateKey: string) => dispatch(setVertexAIServiceAccountPrivateKey(privateKey)),
setServiceAccountClientEmail: (clientEmail: string) => dispatch(setVertexAIServiceAccountClientEmail(clientEmail))
}
}
export function getVertexAISettings() {
return store.getState().llm.settings.vertexai
}
export function getVertexAILocation() {
return store.getState().llm.settings.vertexai.location
}
export function getVertexAIProjectId() {
return store.getState().llm.settings.vertexai.projectId
}
export function getVertexAIServiceAccount() {
return store.getState().llm.settings.vertexai.serviceAccount
}

View File

@ -1020,7 +1020,8 @@
"qiniu": "Qiniu AI",
"tokenflux": "TokenFlux",
"302ai": "302.AI",
"lanyun": "LANYUN"
"lanyun": "LANYUN",
"vertexai": "Vertex AI"
},
"restore": {
"confirm": "Are you sure you want to restore data?",
@ -1681,6 +1682,27 @@
"title": "Model Notes",
"placeholder": "Enter Markdown content...",
"markdown_editor_default_value": "Preview area"
},
"vertex_ai": {
"project_id": "Project ID",
"project_id_placeholder": "your-google-cloud-project-id",
"project_id_help": "Your Google Cloud project ID",
"location": "Location",
"location_help": "Vertex AI service location, e.g., us-central1",
"service_account": {
"title": "Service Account Configuration",
"private_key": "Private Key",
"private_key_placeholder": "Enter Service Account private key",
"private_key_help": "The private_key field from the JSON key file downloaded from Google Cloud Console",
"client_email": "Client Email",
"client_email_placeholder": "Enter Service Account client email",
"client_email_help": "The client_email field from the JSON key file downloaded from Google Cloud Console",
"description": "Use Service Account for authentication, suitable for environments where ADC is not available",
"auth_success": "Service Account authenticated successfully",
"incomplete_config": "Please complete Service Account configuration first"
},
"documentation": "View official documentation for more configuration details:",
"learn_more": "Learn More"
}
},
"proxy": {

View File

@ -1020,7 +1020,8 @@
"tokenflux": "TokenFlux",
"302ai": "302.AI",
"cephalon": "Cephalon",
"lanyun": "LANYUN"
"lanyun": "LANYUN",
"vertexai": "Vertex AI"
},
"restore": {
"confirm": "データを復元しますか?",
@ -1669,6 +1670,27 @@
},
"openai": {
"alert": "OpenAIプロバイダーは旧式の呼び出し方法をサポートしなくなりました。サードパーティのAPIを使用している場合は、新しいサービスプロバイダーを作成してください。"
},
"vertex_ai": {
"project_id": "プロジェクトID",
"project_id_placeholder": "your-google-cloud-project-id",
"project_id_help": "Google CloudプロジェクトID",
"location": "場所",
"location_help": "Vertex AIサービスの場所、例us-central1",
"service_account": {
"title": "サービスアカウント設定",
"private_key": "秘密鍵",
"private_key_placeholder": "サービスアカウントの秘密鍵を入力してください",
"private_key_help": "Google Cloud ConsoleからダウンロードしたJSONキーファイルのprivate_keyフィールド",
"client_email": "クライアントメール",
"client_email_placeholder": "サービスアカウントのクライアントメールを入力してください",
"client_email_help": "Google Cloud ConsoleからダウンロードしたJSONキーファイルのclient_emailフィールド",
"description": "ADCが利用できない環境での認証に適しています",
"auth_success": "サービスアカウントの認証が成功しました",
"incomplete_config": "まずサービスアカウントの設定を完了してください"
},
"documentation": "詳細な設定については、公式ドキュメントを参照してください:",
"learn_more": "詳細を確認"
}
},
"proxy": {

View File

@ -849,9 +849,8 @@
"rendering_speed": "Скорость рендеринга",
"learn_more": "Узнать больше",
"prompt_placeholder_edit": "Введите ваше описание изображения, текстовая отрисовка использует двойные кавычки для обертки",
"prompt_placeholder_en": "Введите” английский “описание изображения, текстовая отрисовка использует двойные кавычки для обертки",
"paint_course": "Руководство / Учебник",
"proxy_required": "Открыть прокси и включить “TUN режим” для просмотра сгенерированных изображений или скопировать их в браузер для открытия. В будущем будет поддерживаться прямое соединение",
"proxy_required": "Сейчас необходимо открыть прокси для просмотра сгенерированных изображений, в будущем будет поддерживаться прямое соединение",
"image_file_required": "Пожалуйста, сначала загрузите изображение",
"image_file_retry": "Пожалуйста, сначала загрузите изображение",
"image_placeholder": "Изображение недоступно",
@ -1020,7 +1019,8 @@
"qiniu": "Qiniu AI",
"tokenflux": "TokenFlux",
"302ai": "302.AI",
"lanyun": "LANYUN"
"lanyun": "LANYUN",
"vertexai": "Vertex AI"
},
"restore": {
"confirm": "Вы уверены, что хотите восстановить данные?",
@ -1669,6 +1669,27 @@
},
"openai": {
"alert": "Поставщик OpenAI больше не поддерживает старые методы вызова. Если вы используете сторонний API, создайте нового поставщика услуг."
},
"vertex_ai": {
"project_id": "ID проекта",
"project_id_placeholder": "your-google-cloud-project-id",
"project_id_help": "Ваш ID проекта Google Cloud",
"location": "Местоположение",
"location_help": "Местоположение службы Vertex AI, например, us-central1",
"service_account": {
"title": "Конфигурация Service Account",
"private_key": "Приватный ключ",
"private_key_placeholder": "Введите приватный ключ Service Account",
"private_key_help": "Поле private_key из файла ключа JSON, загруженного из Google Cloud Console",
"client_email": "Email клиента",
"client_email_placeholder": "Введите email клиента Service Account",
"client_email_help": "Поле client_email из файла ключа JSON, загруженного из Google Cloud Console",
"description": "Используйте Service Account для аутентификации, подходит для сред, где ADC недоступен",
"auth_success": "Service Account успешно аутентифицирован",
"incomplete_config": "Пожалуйста, сначала завершите конфигурацию Service Account"
},
"documentation": "Смотрите официальную документацию для получения более подробной информации о конфигурации:",
"learn_more": "Узнать больше"
}
},
"proxy": {

View File

@ -1020,7 +1020,8 @@
"qiniu": "七牛云 AI 推理",
"tokenflux": "TokenFlux",
"302ai": "302.AI",
"lanyun": "蓝耘科技"
"lanyun": "蓝耘科技",
"vertexai": "Vertex AI"
},
"restore": {
"confirm": "确定要恢复数据吗?",
@ -1681,6 +1682,27 @@
"title": "模型备注",
"placeholder": "请输入Markdown格式内容...",
"markdown_editor_default_value": "预览区域"
},
"vertex_ai": {
"project_id": "项目 ID",
"project_id_placeholder": "your-google-cloud-project-id",
"project_id_help": "您的 Google Cloud 项目 ID",
"location": "地区",
"location_help": "Vertex AI 服务的地区,例如 us-central1",
"service_account": {
"title": "Service Account 配置",
"private_key": "私钥",
"private_key_placeholder": "请输入 Service Account 私钥",
"private_key_help": "从 Google Cloud Console 下载的 JSON 密钥文件中的 private_key 字段",
"client_email": "客户端邮箱",
"client_email_placeholder": "请输入 Service Account 客户端邮箱",
"client_email_help": "从 Google Cloud Console 下载的 JSON 密钥文件中的 client_email 字段",
"description": "使用 Service Account 进行身份验证,适用于无法使用 ADC 的环境",
"auth_success": "Service Account 认证成功",
"incomplete_config": "请先完整配置 Service Account 信息"
},
"documentation": "查看官方文档了解更多配置详情:",
"learn_more": "了解更多"
}
},
"proxy": {

View File

@ -1020,7 +1020,8 @@
"qiniu": "七牛雲 AI 推理",
"tokenflux": "TokenFlux",
"302ai": "302.AI",
"lanyun": "藍耘"
"lanyun": "藍耘",
"vertexai": "Vertex AI"
},
"restore": {
"confirm": "確定要復原資料嗎?",
@ -1672,6 +1673,27 @@
},
"openai": {
"alert": "OpenAI Provider 不再支援舊的呼叫方法。如果使用第三方 API請建立新的服務供應商"
},
"vertex_ai": {
"project_id": "專案ID",
"project_id_placeholder": "your-google-cloud-project-id",
"project_id_help": "您的 Google Cloud 專案 ID",
"location": "地區",
"location_help": "Vertex AI 服務地區例如us-central1",
"service_account": {
"title": "服務帳戶設定",
"private_key": "私密金鑰",
"private_key_placeholder": "輸入服務帳戶私密金鑰",
"private_key_help": "從 Google Cloud Console 下載的 JSON 金鑰檔案中的 private_key 欄位",
"client_email": "Client Email",
"client_email_placeholder": "輸入服務帳戶 client email",
"client_email_help": "從 Google Cloud Console 下載的 JSON 金鑰檔案中的 client_email 欄位",
"description": "使用服務帳戶進行身份驗證,適用於 ADC 不可用的環境",
"auth_success": "服務帳戶驗證成功",
"incomplete_config": "請先完成服務帳戶設定"
},
"documentation": "檢視官方文件以取得更多設定詳細資訊:",
"learn_more": "瞭解更多"
}
},
"proxy": {

View File

@ -42,6 +42,7 @@ import ModelListSearchBar from './ModelListSearchBar'
import ProviderOAuth from './ProviderOAuth'
import ProviderSettingsPopup from './ProviderSettingsPopup'
import SelectProviderModelPopup from './SelectProviderModelPopup'
import VertexAISettings from './VertexAISettings'
interface Props {
provider: Provider
@ -335,6 +336,8 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
)}
{provider.id === 'openai' && <OpenAIAlert />}
{isDmxapi && <DMXAPISettings provider={provider} setApiKey={setApiKey} />}
{provider.id !== 'vertexai' && (
<>
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.api_key')}</SettingSubtitle>
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input.Password
@ -403,6 +406,8 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
)}
</>
)}
</>
)}
{isAzureOpenAI && (
<>
<SettingSubtitle>{t('settings.provider.api_version')}</SettingSubtitle>
@ -419,6 +424,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
{provider.id === 'lmstudio' && <LMStudioSettings />}
{provider.id === 'gpustack' && <GPUStackSettings />}
{provider.id === 'copilot' && <GithubCopilotSettings provider={provider} setApiKey={setApiKey} />}
{provider.id === 'vertexai' && <VertexAISettings />}
<SettingSubtitle style={{ marginBottom: 5 }}>
<Space align="center" style={{ width: '100%', justifyContent: 'space-between' }}>
<HStack alignItems="center" gap={8} mb={5}>

View File

@ -0,0 +1,138 @@
import { HStack } from '@renderer/components/Layout'
import { PROVIDER_CONFIG } from '@renderer/config/providers'
import { useVertexAISettings } from '@renderer/hooks/useVertexAI'
import { Alert, Input } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SettingHelpLink, SettingHelpText, SettingHelpTextRow, SettingSubtitle } from '..'
const VertexAISettings: FC = () => {
const { t } = useTranslation()
const {
projectId,
location,
serviceAccount,
setProjectId,
setLocation,
setServiceAccountPrivateKey,
setServiceAccountClientEmail
} = useVertexAISettings()
const providerConfig = PROVIDER_CONFIG['vertexai']
const apiKeyWebsite = providerConfig?.websites?.apiKey
const [localProjectId, setLocalProjectId] = useState(projectId)
const [localLocation, setLocalLocation] = useState(location)
const handleProjectIdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setLocalProjectId(e.target.value)
}
const handleLocationChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newLocation = e.target.value
setLocalLocation(newLocation)
}
const handleServiceAccountPrivateKeyChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setServiceAccountPrivateKey(e.target.value)
}
const handleServiceAccountPrivateKeyBlur = () => {
setServiceAccountPrivateKey(serviceAccount.privateKey)
}
const handleServiceAccountClientEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setServiceAccountClientEmail(e.target.value)
}
const handleServiceAccountClientEmailBlur = () => {
setServiceAccountClientEmail(serviceAccount.clientEmail)
}
const handleProjectIdBlur = () => {
setProjectId(localProjectId)
}
const handleLocationBlur = () => {
setLocation(localLocation)
}
return (
<>
<SettingSubtitle style={{ marginTop: 5 }}>
{t('settings.provider.vertex_ai.service_account.title')}
</SettingSubtitle>
<Alert
type="info"
style={{ marginTop: 5 }}
message={t('settings.provider.vertex_ai.service_account.description')}
showIcon
/>
<SettingSubtitle style={{ marginTop: 5 }}>
{t('settings.provider.vertex_ai.service_account.client_email')}
</SettingSubtitle>
<Input.Password
value={serviceAccount.clientEmail}
placeholder={t('settings.provider.vertex_ai.service_account.client_email_placeholder')}
onChange={handleServiceAccountClientEmailChange}
onBlur={handleServiceAccountClientEmailBlur}
style={{ marginTop: 5 }}
/>
<SettingHelpTextRow>
<SettingHelpText>{t('settings.provider.vertex_ai.service_account.client_email_help')}</SettingHelpText>
</SettingHelpTextRow>
<SettingSubtitle style={{ marginTop: 5 }}>
{t('settings.provider.vertex_ai.service_account.private_key')}
</SettingSubtitle>
<Input.TextArea
value={serviceAccount.privateKey}
placeholder={t('settings.provider.vertex_ai.service_account.private_key_placeholder')}
onChange={handleServiceAccountPrivateKeyChange}
onBlur={handleServiceAccountPrivateKeyBlur}
style={{ marginTop: 5 }}
spellCheck={false}
autoSize={{ minRows: 4, maxRows: 4 }}
/>
{apiKeyWebsite && (
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
<HStack>
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
{t('settings.provider.get_api_key')}
</SettingHelpLink>
</HStack>
<SettingHelpText>{t('settings.provider.vertex_ai.service_account.private_key_help')}</SettingHelpText>
</SettingHelpTextRow>
)}
<>
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.vertex_ai.project_id')}</SettingSubtitle>
<Input.Password
value={localProjectId}
placeholder={t('settings.provider.vertex_ai.project_id_placeholder')}
onChange={handleProjectIdChange}
onBlur={handleProjectIdBlur}
style={{ marginTop: 5 }}
/>
<SettingHelpTextRow>
<SettingHelpText>{t('settings.provider.vertex_ai.project_id_help')}</SettingHelpText>
</SettingHelpTextRow>
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.vertex_ai.location')}</SettingSubtitle>
<Input
value={localLocation}
placeholder="us-central1"
onChange={handleLocationChange}
onBlur={handleLocationBlur}
style={{ marginTop: 5 }}
/>
<SettingHelpTextRow>
<SettingHelpText>{t('settings.provider.vertex_ai.location_help')}</SettingHelpText>
</SettingHelpTextRow>
</>
</>
)
}
export default VertexAISettings

View File

@ -516,7 +516,7 @@ export async function fetchGenerate({ prompt, content }: { prompt: string; conte
function hasApiKey(provider: Provider) {
if (!provider) return false
if (provider.id === 'ollama' || provider.id === 'lmstudio') return true
if (provider.id === 'ollama' || provider.id === 'lmstudio' || provider.type === 'vertexai') return true
return !isEmpty(provider.apiKey)
}
@ -538,14 +538,19 @@ export function checkApiProvider(provider: Provider): void {
const key = 'api-check'
const style = { marginTop: '3vh' }
if (provider.id !== 'ollama' && provider.id !== 'lmstudio') {
if (
provider.id !== 'ollama' &&
provider.id !== 'lmstudio' &&
provider.type !== 'vertexai' &&
provider.id !== 'copilot'
) {
if (!provider.apiKey) {
window.message.error({ content: i18n.t('message.error.enter.api.key'), key, style })
throw new Error(i18n.t('message.error.enter.api.key'))
}
}
if (!provider.apiHost) {
if (!provider.apiHost && provider.type !== 'vertexai') {
window.message.error({ content: i18n.t('message.error.enter.api.host'), key, style })
throw new Error(i18n.t('message.error.enter.api.host'))
}

View File

@ -14,6 +14,14 @@ type LlmSettings = {
gpustack: {
keepAliveTime: number
}
vertexai: {
serviceAccount: {
privateKey: string
clientEmail: string
}
projectId: string
location: string
}
}
export interface LlmState {
@ -225,7 +233,8 @@ export const INITIAL_PROVIDERS: Provider[] = [
apiHost: 'https://generativelanguage.googleapis.com',
models: SYSTEM_MODELS.gemini,
isSystem: true,
enabled: false
enabled: false,
isVertex: false
},
{
id: 'zhipu',
@ -507,10 +516,21 @@ export const INITIAL_PROVIDERS: Provider[] = [
models: SYSTEM_MODELS.voyageai,
isSystem: true,
enabled: false
},
{
id: 'vertexai',
name: 'VertexAI',
type: 'vertexai',
apiKey: '',
apiHost: 'https://aiplatform.googleapis.com',
models: [],
isSystem: true,
enabled: false,
isVertex: true
}
]
const initialState: LlmState = {
export const initialState: LlmState = {
defaultModel: SYSTEM_MODELS.defaultModel[0],
topicNamingModel: SYSTEM_MODELS.defaultModel[1],
translateModel: SYSTEM_MODELS.defaultModel[2],
@ -525,6 +545,14 @@ const initialState: LlmState = {
},
gpustack: {
keepAliveTime: 0
},
vertexai: {
serviceAccount: {
privateKey: '',
clientEmail: ''
},
projectId: '',
location: ''
}
}
}
@ -634,6 +662,18 @@ const llmSlice = createSlice({
setGPUStackKeepAliveTime: (state, action: PayloadAction<number>) => {
state.settings.gpustack.keepAliveTime = action.payload
},
setVertexAIProjectId: (state, action: PayloadAction<string>) => {
state.settings.vertexai.projectId = action.payload
},
setVertexAILocation: (state, action: PayloadAction<string>) => {
state.settings.vertexai.location = action.payload
},
setVertexAIServiceAccountPrivateKey: (state, action: PayloadAction<string>) => {
state.settings.vertexai.serviceAccount.privateKey = action.payload
},
setVertexAIServiceAccountClientEmail: (state, action: PayloadAction<string>) => {
state.settings.vertexai.serviceAccount.clientEmail = action.payload
},
updateModel: (
state,
action: PayloadAction<{
@ -666,6 +706,10 @@ export const {
setOllamaKeepAliveTime,
setLMStudioKeepAliveTime,
setGPUStackKeepAliveTime,
setVertexAIProjectId,
setVertexAILocation,
setVertexAIServiceAccountPrivateKey,
setVertexAIServiceAccountClientEmail,
updateModel
} = llmSlice.actions

View File

@ -5,14 +5,14 @@ import { SYSTEM_MODELS } from '@renderer/config/models'
import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
import db from '@renderer/databases'
import i18n from '@renderer/i18n'
import { Assistant, WebSearchProvider } from '@renderer/types'
import { Assistant, Provider, WebSearchProvider } from '@renderer/types'
import { getDefaultGroupName, getLeadingEmoji, runAsyncFunction, uuid } from '@renderer/utils'
import { isEmpty } from 'lodash'
import { createMigrate } from 'redux-persist'
import { RootState } from '.'
import { DEFAULT_TOOL_ORDER } from './inputTools'
import { INITIAL_PROVIDERS, moveProvider } from './llm'
import { INITIAL_PROVIDERS, initialState as llmInitialState, moveProvider } from './llm'
import { mcpSlice } from './mcp'
import { defaultActionItems } from './selectionStore'
import { DEFAULT_SIDEBAR_ICONS, initialState as settingsInitialState } from './settings'
@ -56,6 +56,15 @@ function addProvider(state: RootState, id: string) {
}
}
function updateProvider(state: RootState, id: string, provider: Partial<Provider>) {
if (state.llm.providers) {
const index = state.llm.providers.findIndex((p) => p.id === id)
if (index !== -1) {
state.llm.providers[index] = { ...state.llm.providers[index], ...provider }
}
}
}
function addWebSearchProvider(state: RootState, id: string) {
if (state.websearch && state.websearch.providers) {
if (!state.websearch.providers.find((p) => p.id === id)) {
@ -1569,6 +1578,24 @@ const migrateConfig = {
} catch (error) {
return state
}
},
'113': (state: RootState) => {
try {
addProvider(state, 'vertexai')
state.llm.providers = moveProvider(state.llm.providers, 'vertexai', 10)
if (!state.llm.settings.vertexai) {
state.llm.settings.vertexai = llmInitialState.settings.vertexai
}
updateProvider(state, 'gemini', {
isVertex: false
})
updateProvider(state, 'vertexai', {
isVertex: true
})
return state
} catch (error) {
return state
}
}
}

View File

@ -159,10 +159,18 @@ export type Provider = {
isAuthed?: boolean
rateLimit?: number
isNotSupportArrayContent?: boolean
isVertex?: boolean
notes?: string
}
export type ProviderType = 'openai' | 'openai-response' | 'anthropic' | 'gemini' | 'qwenlm' | 'azure-openai'
export type ProviderType =
| 'openai'
| 'openai-response'
| 'anthropic'
| 'gemini'
| 'qwenlm'
| 'azure-openai'
| 'vertexai'
export type ModelType = 'text' | 'vision' | 'embedding' | 'reasoning' | 'function_calling' | 'web_search'

View File

@ -2021,7 +2021,7 @@ __metadata:
languageName: node
linkType: hard
"@google/genai@npm:^1.0.1":
"@google/genai@npm:1.0.1":
version: 1.0.1
resolution: "@google/genai@npm:1.0.1"
dependencies:
@ -2035,6 +2035,20 @@ __metadata:
languageName: node
linkType: hard
"@google/genai@patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch":
version: 1.0.1
resolution: "@google/genai@patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch::version=1.0.1&hash=b1e680"
dependencies:
google-auth-library: "npm:^9.14.2"
ws: "npm:^8.18.0"
zod: "npm:^3.22.4"
zod-to-json-schema: "npm:^3.22.4"
peerDependencies:
"@modelcontextprotocol/sdk": ^1.11.0
checksum: 10c0/aa38b73de3d84944f51c1f45a3945ea7578b6660276ea748f2349ed42106edc5c81c08872f7fb62cd6e158fc0517283cfe9cdbcce806eee3b62439f60b82496a
languageName: node
linkType: hard
"@hello-pangea/dnd@npm:^16.6.0":
version: 16.6.0
resolution: "@hello-pangea/dnd@npm:16.6.0"
@ -5596,7 +5610,7 @@ __metadata:
"@emotion/is-prop-valid": "npm:^1.3.1"
"@eslint-react/eslint-plugin": "npm:^1.36.1"
"@eslint/js": "npm:^9.22.0"
"@google/genai": "npm:^1.0.1"
"@google/genai": "patch:@google/genai@npm%3A1.0.1#~/.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch"
"@hello-pangea/dnd": "npm:^16.6.0"
"@kangfenmao/keyv-storage": "npm:^0.1.0"
"@langchain/community": "npm:^0.3.36"
@ -5668,6 +5682,7 @@ __metadata:
fast-xml-parser: "npm:^5.2.0"
franc-min: "npm:^6.2.0"
fs-extra: "npm:^11.2.0"
google-auth-library: "npm:^9.15.1"
html-to-image: "npm:^1.11.13"
husky: "npm:^9.1.7"
i18next: "npm:^23.11.5"
@ -10258,7 +10273,7 @@ __metadata:
languageName: node
linkType: hard
"google-auth-library@npm:^9.14.2":
"google-auth-library@npm:^9.14.2, google-auth-library@npm:^9.15.1":
version: 9.15.1
resolution: "google-auth-library@npm:9.15.1"
dependencies: