mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-20 23:22:05 +08:00
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:
parent
f48e7aadb8
commit
502cce70c2
6471
.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch
vendored
Normal file
6471
.yarn/patches/@google-genai-npm-1.0.1-e26f0f9af7.patch
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -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",
|
||||
|
||||
@ -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',
|
||||
|
||||
|
||||
@ -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())
|
||||
|
||||
142
src/main/services/VertexAIService.ts
Normal file
142
src/main/services/VertexAIService.ts
Normal 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
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
95
src/renderer/src/aiCore/clients/gemini/VertexAPIClient.ts
Normal file
95
src/renderer/src/aiCore/clients/gemini/VertexAPIClient.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/renderer/src/assets/images/providers/vertexai.svg
Normal file
1
src/renderer/src/assets/images/providers/vertexai.svg
Normal 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 |
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
src/renderer/src/hooks/useVertexAI.ts
Normal file
37
src/renderer/src/hooks/useVertexAI.ts
Normal 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
|
||||
}
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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,72 +336,76 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
)}
|
||||
{provider.id === 'openai' && <OpenAIAlert />}
|
||||
{isDmxapi && <DMXAPISettings provider={provider} setApiKey={setApiKey} />}
|
||||
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.api_key')}</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input.Password
|
||||
value={inputValue}
|
||||
placeholder={t('settings.provider.api_key')}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value)
|
||||
debouncedSetApiKey(e.target.value)
|
||||
}}
|
||||
onBlur={() => {
|
||||
const formattedValue = formatApiKeys(inputValue)
|
||||
setInputValue(formattedValue)
|
||||
setApiKey(formattedValue)
|
||||
onUpdateApiKey()
|
||||
}}
|
||||
spellCheck={false}
|
||||
autoFocus={provider.enabled && apiKey === '' && !isProviderSupportAuth(provider)}
|
||||
disabled={provider.id === 'copilot'}
|
||||
/>
|
||||
<Button
|
||||
type={apiValid ? 'primary' : 'default'}
|
||||
ghost={apiValid}
|
||||
onClick={onCheckApi}
|
||||
disabled={!apiHost || apiChecking}>
|
||||
{apiChecking ? <LoadingOutlined spin /> : apiValid ? <CheckOutlined /> : t('settings.provider.check')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
{apiKeyWebsite && (
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<HStack>
|
||||
{!isDmxapi && (
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.provider.get_api_key')}
|
||||
</SettingHelpLink>
|
||||
)}
|
||||
</HStack>
|
||||
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
)}
|
||||
{!isDmxapi && (
|
||||
{provider.id !== 'vertexai' && (
|
||||
<>
|
||||
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
|
||||
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.api_key')}</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input
|
||||
value={apiHost}
|
||||
placeholder={t('settings.provider.api_host')}
|
||||
onChange={(e) => setApiHost(e.target.value)}
|
||||
onBlur={onUpdateApiHost}
|
||||
<Input.Password
|
||||
value={inputValue}
|
||||
placeholder={t('settings.provider.api_key')}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value)
|
||||
debouncedSetApiKey(e.target.value)
|
||||
}}
|
||||
onBlur={() => {
|
||||
const formattedValue = formatApiKeys(inputValue)
|
||||
setInputValue(formattedValue)
|
||||
setApiKey(formattedValue)
|
||||
onUpdateApiKey()
|
||||
}}
|
||||
spellCheck={false}
|
||||
autoFocus={provider.enabled && apiKey === '' && !isProviderSupportAuth(provider)}
|
||||
disabled={provider.id === 'copilot'}
|
||||
/>
|
||||
{!isEmpty(configedApiHost) && apiHost !== configedApiHost && (
|
||||
<Button danger onClick={onReset}>
|
||||
{t('settings.provider.api.url.reset')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type={apiValid ? 'primary' : 'default'}
|
||||
ghost={apiValid}
|
||||
onClick={onCheckApi}
|
||||
disabled={!apiHost || apiChecking}>
|
||||
{apiChecking ? <LoadingOutlined spin /> : apiValid ? <CheckOutlined /> : t('settings.provider.check')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
{isOpenAIProvider(provider) && (
|
||||
{apiKeyWebsite && (
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<SettingHelpText
|
||||
style={{ marginLeft: 6, marginRight: '1em', whiteSpace: 'break-spaces', wordBreak: 'break-all' }}>
|
||||
{hostPreview()}
|
||||
</SettingHelpText>
|
||||
<SettingHelpText style={{ minWidth: 'fit-content' }}>
|
||||
{t('settings.provider.api.url.tip')}
|
||||
</SettingHelpText>
|
||||
<HStack>
|
||||
{!isDmxapi && (
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.provider.get_api_key')}
|
||||
</SettingHelpLink>
|
||||
)}
|
||||
</HStack>
|
||||
<SettingHelpText>{t('settings.provider.api_key.tip')}</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
)}
|
||||
{!isDmxapi && (
|
||||
<>
|
||||
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
|
||||
<Space.Compact style={{ width: '100%', marginTop: 5 }}>
|
||||
<Input
|
||||
value={apiHost}
|
||||
placeholder={t('settings.provider.api_host')}
|
||||
onChange={(e) => setApiHost(e.target.value)}
|
||||
onBlur={onUpdateApiHost}
|
||||
/>
|
||||
{!isEmpty(configedApiHost) && apiHost !== configedApiHost && (
|
||||
<Button danger onClick={onReset}>
|
||||
{t('settings.provider.api.url.reset')}
|
||||
</Button>
|
||||
)}
|
||||
</Space.Compact>
|
||||
{isOpenAIProvider(provider) && (
|
||||
<SettingHelpTextRow style={{ justifyContent: 'space-between' }}>
|
||||
<SettingHelpText
|
||||
style={{ marginLeft: 6, marginRight: '1em', whiteSpace: 'break-spaces', wordBreak: 'break-all' }}>
|
||||
{hostPreview()}
|
||||
</SettingHelpText>
|
||||
<SettingHelpText style={{ minWidth: 'fit-content' }}>
|
||||
{t('settings.provider.api.url.tip')}
|
||||
</SettingHelpText>
|
||||
</SettingHelpTextRow>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isAzureOpenAI && (
|
||||
@ -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}>
|
||||
|
||||
@ -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
|
||||
@ -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'))
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
21
yarn.lock
21
yarn.lock
@ -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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user