diff --git a/package.json b/package.json index 92618084d..cd59398ca 100644 --- a/package.json +++ b/package.json @@ -66,8 +66,8 @@ "@e2b/code-interpreter": "^1.5.1", "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", - "@google/genai": "^1.29.0", "node-pty": "^1.1.0-beta39", + "@google/genai": "^1.30.0", "@jxa/run": "^1.4.0", "@modelcontextprotocol/sdk": "^1.22.0", "axios": "^1.13.2", diff --git a/src/main/presenter/configPresenter/modelCapabilities.ts b/src/main/presenter/configPresenter/modelCapabilities.ts index 63607d5aa..47afdbb35 100644 --- a/src/main/presenter/configPresenter/modelCapabilities.ts +++ b/src/main/presenter/configPresenter/modelCapabilities.ts @@ -19,7 +19,8 @@ export class ModelCapabilities { private index: Map> = new Map() private static readonly PROVIDER_ID_ALIASES: Record = { dashscope: 'alibaba-cn', - gemini: 'google' + gemini: 'google', + vertex: 'google-vertex' } constructor() { diff --git a/src/main/presenter/configPresenter/providers.ts b/src/main/presenter/configPresenter/providers.ts index 68e112424..e0b17f86f 100644 --- a/src/main/presenter/configPresenter/providers.ts +++ b/src/main/presenter/configPresenter/providers.ts @@ -232,6 +232,21 @@ export const DEFAULT_PROVIDERS: LLM_PROVIDER_BASE[] = [ defaultBaseUrl: 'https://generativelanguage.googleapis.com' } }, + { + id: 'vertex', + name: 'Vertex AI', + apiType: 'vertex', + apiKey: '', + baseUrl: '', + enable: false, + 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/model-reference/gemini', + defaultBaseUrl: 'https://aiplatform.googleapis.com' + } + }, { id: 'anthropic', name: 'Anthropic', diff --git a/src/main/presenter/llmProviderPresenter/managers/providerInstanceManager.ts b/src/main/presenter/llmProviderPresenter/managers/providerInstanceManager.ts index 390f0af31..b624646bc 100644 --- a/src/main/presenter/llmProviderPresenter/managers/providerInstanceManager.ts +++ b/src/main/presenter/llmProviderPresenter/managers/providerInstanceManager.ts @@ -23,6 +23,7 @@ import { ZhipuProvider } from '../providers/zhipuProvider' import { LMStudioProvider } from '../providers/lmstudioProvider' import { OpenAIResponsesProvider } from '../providers/openAIResponsesProvider' import { CherryInProvider } from '../providers/cherryInProvider' +import { VertexProvider } from '../providers/vertexProvider' import { OpenRouterProvider } from '../providers/openRouterProvider' import { MinimaxProvider } from '../providers/minimaxProvider' import { AihubmixProvider } from '../providers/aihubmixProvider' @@ -77,6 +78,7 @@ export class ProviderInstanceManager { ['dashscope', DashscopeProvider], ['gemini', GeminiProvider], ['zhipu', ZhipuProvider], + ['vertex', VertexProvider], ['github', GithubProvider], ['github-copilot', GithubCopilotProvider], ['ollama', OllamaProvider], @@ -106,6 +108,7 @@ export class ProviderInstanceManager { ['dashscope', DashscopeProvider], ['ppio', PPIOProvider], ['gemini', GeminiProvider], + ['vertex', VertexProvider], ['zhipu', ZhipuProvider], ['github', GithubProvider], ['github-copilot', GithubCopilotProvider], diff --git a/src/main/presenter/llmProviderPresenter/providers/vertexProvider.ts b/src/main/presenter/llmProviderPresenter/providers/vertexProvider.ts new file mode 100644 index 000000000..e09670735 --- /dev/null +++ b/src/main/presenter/llmProviderPresenter/providers/vertexProvider.ts @@ -0,0 +1,1193 @@ +import { presenter } from '@/presenter' +import { + Content, + FunctionCallingConfigMode, + GenerateContentParameters, + GenerateContentResponseUsageMetadata, + GoogleGenAI, + HarmBlockThreshold, + HarmCategory, + Modality, + Part, + SafetySetting, + Tool, + GoogleSearch, + GenerateContentConfig +} from '@google/genai' +import { ModelType } from '@shared/model' +import { + ChatMessage, + IConfigPresenter, + LLM_PROVIDER, + LLMCoreStreamEvent, + LLMResponse, + MCPToolDefinition, + MODEL_META, + ModelConfig +} from '@shared/presenter' +import { VERTEX_PROVIDER } from '@shared/types/presenters/llmprovider.presenter' +import { createStreamEvent } from '@shared/types/core/llm-events' +import { BaseLLMProvider, SUMMARY_TITLES_PROMPT } from '../baseProvider' +import { modelCapabilities } from '../../configPresenter/modelCapabilities' +import { eventBus, SendTarget } from '@/eventbus' +import { CONFIG_EVENTS } from '@/events' + +// Mapping from simple keys to API HarmCategory constants +const keyToHarmCategoryMap: Record = { + harassment: HarmCategory.HARM_CATEGORY_HARASSMENT, + hateSpeech: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + sexuallyExplicit: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, + dangerousContent: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT +} + +// Value mapping from config storage to API HarmBlockThreshold constants +// Assuming config stores 'BLOCK_NONE', 'BLOCK_LOW_AND_ABOVE', etc. directly +const valueToHarmBlockThresholdMap: Record = { + BLOCK_NONE: HarmBlockThreshold.BLOCK_NONE, + BLOCK_LOW_AND_ABOVE: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE, + BLOCK_MEDIUM_AND_ABOVE: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE, + BLOCK_ONLY_HIGH: HarmBlockThreshold.BLOCK_ONLY_HIGH, + HARM_BLOCK_THRESHOLD_UNSPECIFIED: HarmBlockThreshold.HARM_BLOCK_THRESHOLD_UNSPECIFIED +} +const safetySettingKeys = Object.keys(keyToHarmCategoryMap) + +export class VertexProvider extends BaseLLMProvider { + private genAI: GoogleGenAI + + constructor(provider: LLM_PROVIDER, configPresenter: IConfigPresenter) { + super(provider, configPresenter) + this.genAI = this.createGenAIClient() + this.init() + } + + public onProxyResolved(): void { + this.genAI = this.createGenAIClient() + this.init() + } + + private get vertexProvider(): VERTEX_PROVIDER { + return this.provider as VERTEX_PROVIDER + } + + private resolveProjectId(): string | undefined { + return this.vertexProvider.projectId || process.env.GOOGLE_CLOUD_PROJECT + } + + private resolveLocation(): string | undefined { + return this.vertexProvider.location || process.env.GOOGLE_CLOUD_LOCATION + } + + private buildGoogleAuthOptions() { + const privateKey = this.vertexProvider.accountPrivateKey + const clientEmail = this.vertexProvider.accountClientEmail + if (privateKey && clientEmail) { + return { + projectId: this.resolveProjectId(), + credentials: { + client_email: clientEmail, + private_key: privateKey.replace(/\\n/g, '\n') + }, + scopes: ['https://www.googleapis.com/auth/cloud-platform'] + } + } + return undefined + } + + private buildBaseUrl(): string { + const customBaseUrl = this.vertexProvider.baseUrl?.trim() + if (customBaseUrl) return customBaseUrl + + const location = this.resolveLocation() || 'us-central1' + if (this.vertexProvider.endpointMode === 'express') { + return 'https://aiplatform.googleapis.com/' + } + return `https://${location}-aiplatform.googleapis.com/` + } + + private getApiVersion(): 'v1' | 'v1beta1' { + return (this.vertexProvider.apiVersion as 'v1' | 'v1beta1') || 'v1' + } + + private createGenAIClient(): GoogleGenAI { + const project = this.resolveProjectId() + const location = this.resolveLocation() + const apiVersion = this.getApiVersion() + + return new GoogleGenAI({ + vertexai: true, + project, + location, + apiVersion, + googleAuthOptions: this.buildGoogleAuthOptions(), + httpOptions: { + baseUrl: this.buildBaseUrl(), + apiVersion + } + }) + } + + // 确保带有 Vertex 格式的模型路径 + private ensureVertexModelName(modelId: string): string { + if (!modelId) return modelId + if (modelId.startsWith('projects/')) return modelId + const normalized = modelId + .replace(/^models\//i, '') + .replace(/^publishers\/google\/models\//i, '') + return `publishers/google/models/${normalized}` + } + + // Implement abstract method fetchProviderModels from BaseLLMProvider + protected async fetchProviderModels(): Promise { + try { + const modelsResponse = await this.genAI.models.list() + // console.log('gemini models response:', modelsResponse) + + // 将 pager 转换为数组 + const models: any[] = [] + for await (const model of modelsResponse) { + models.push(model) + } + + if (models.length === 0) { + console.warn('No models found in Vertex AI response, using Provider DB models') + const dbModels = this.configPresenter.getDbProviderModels(this.provider.id).map((m) => ({ + id: m.id, + name: m.name, + group: m.group || 'default', + providerId: this.provider.id, + isCustom: false, + contextLength: m.contextLength, + maxTokens: m.maxTokens, + vision: m.vision || false, + functionCall: m.functionCall || false, + reasoning: m.reasoning || false, + ...(m.type ? { type: m.type } : {}) + })) + return dbModels + } + + // 映射 API 返回的模型数据(能力统一读 Provider DB) + const normalizeModelId = (mid: string): string => + String(mid || '') + .replace(/^models\//i, '') + .replace(/^publishers\/google\/models\//i, '') + const apiModels: MODEL_META[] = models + .filter((model: any) => { + const name = String(model.name || '').toLowerCase() + return ( + !name.includes('embedding') && + !name.includes('aqa') && + !name.includes('text-embedding') && + !name.includes('gemma-3n-e4b-it') + ) + }) + .map((model: any) => { + const apiModelId: string = model.name + const displayName: string = model.displayName || apiModelId + + const normalizedId = normalizeModelId(apiModelId) + + const vision = modelCapabilities.supportsVision(this.provider.id, normalizedId) + const functionCall = modelCapabilities.supportsToolCall(this.provider.id, normalizedId) + const reasoning = modelCapabilities.supportsReasoning(this.provider.id, normalizedId) + const isImageOutput = modelCapabilities.supportsImageOutput( + this.provider.id, + normalizedId + ) + const modelType = isImageOutput ? ModelType.ImageGeneration : ModelType.Chat + + let group = 'default' + if (/\b(exp|preview)\b/i.test(apiModelId)) group = 'experimental' + else if (/\bgemma\b/i.test(apiModelId)) group = 'gemma' + + return { + id: apiModelId, + name: displayName, + group, + providerId: this.provider.id, + isCustom: false, + contextLength: model.inputTokenLimit, + maxTokens: model.outputTokenLimit, + vision, + functionCall, + reasoning, + ...(modelType !== ModelType.Chat && { type: modelType }) + } as MODEL_META + }) + + // console.log('Mapped Vertex models:', apiModels) + return apiModels + } catch (error) { + console.warn('Failed to fetch models from Vertex AI:', error) + // If API call fails, fallback to Provider DB mapping + const dbModels = this.configPresenter.getDbProviderModels(this.provider.id).map((m) => ({ + id: m.id, + name: m.name, + group: m.group || 'default', + providerId: this.provider.id, + isCustom: false, + contextLength: m.contextLength, + maxTokens: m.maxTokens, + vision: m.vision || false, + functionCall: m.functionCall || false, + reasoning: m.reasoning || false, + ...(m.type ? { type: m.type } : {}) + })) + return dbModels + } + } + + // Implement summaryTitles abstract method from BaseLLMProvider + public async summaryTitles( + messages: { role: 'system' | 'user' | 'assistant'; content: string }[], + modelId: string + ): Promise { + console.log('vertex summary check, ignore modelId', modelId) + // Use Vertex AI to generate conversation titles + try { + const conversationText = messages.map((m) => `${m.role}: ${m.content}`).join('\n') + const prompt = `${SUMMARY_TITLES_PROMPT}\n\n${conversationText}` + + const result = await this.genAI.models.generateContent({ + model: this.ensureVertexModelName(modelId), + contents: [{ role: 'user', parts: [{ text: prompt }] }], + config: this.getGenerateContentConfig(0.4, undefined, modelId, false) + }) + + return result.text?.trim() || 'New Conversation' + } catch (error) { + console.error('Failed to generate conversation title:', error) + return 'New Conversation' + } + } + + // Override fetchModels method since Vertex AI will reuse the cached model list + async fetchModels(): Promise { + // Vertex AI will reuse the cached model list + return this.models + } + + // Override check method to use the first default model for testing + async check(): Promise<{ isOk: boolean; errorMsg: string | null }> { + try { + const projectId = this.resolveProjectId() + const location = this.resolveLocation() + if (!projectId || !location) { + return { isOk: false, errorMsg: 'projectId and location are required for Vertex AI' } + } + if (this.vertexProvider.accountPrivateKey && !this.vertexProvider.accountClientEmail) { + return { + isOk: false, + errorMsg: 'accountClientEmail is required when accountPrivateKey is provided' + } + } + this.genAI = this.createGenAIClient() + + // Use the first model for simple testing + const testModelId = + this.models.find((m) => m.type !== ModelType.ImageGeneration)?.id || + this.models[0]?.id || + 'gemini-1.5-flash-001' + + const result = await this.genAI.models.generateContent({ + model: this.ensureVertexModelName(testModelId), + contents: [{ role: 'user', parts: [{ text: 'Hello from Vertex AI' }] }] + }) + return { + isOk: Boolean(result && (result.text || result.candidates?.length)), + errorMsg: null + } + } catch (error) { + console.error('Vertex provider check failed:', this.provider.name, error) + return { isOk: false, errorMsg: error instanceof Error ? error.message : String(error) } + } + } + + protected async init() { + if (this.provider.enable) { + try { + const projectId = this.resolveProjectId() + const location = this.resolveLocation() + if (!projectId || !location) { + console.warn('Vertex provider missing projectId or location, skip initialization') + return + } + this.genAI = this.createGenAIClient() + this.isInitialized = true + // Use API to get model list, fallback to static list if failed + this.models = await this.fetchProviderModels() + await this.autoEnableModelsIfNeeded() + // Vertex AI is relatively slow, special compensation + eventBus.sendToRenderer( + CONFIG_EVENTS.MODEL_LIST_CHANGED, + SendTarget.ALL_WINDOWS, + this.provider.id + ) + console.info('Provider initialized successfully:', this.provider.name) + } catch (error) { + console.warn('Provider initialization failed:', this.provider.name, error) + } + } + } + + /** + * 重写 autoEnableModelsIfNeeded 方法 + * 不自动启用模型,交由用户手动选择。 + */ + protected async autoEnableModelsIfNeeded() { + if (!this.models || this.models.length === 0) return + const providerId = this.provider.id + + // 检查是否有自定义模型 + const customModels = this.configPresenter.getCustomModels(providerId) + if (customModels && customModels.length > 0) return + + // 检查是否有任何模型的状态被手动修改过 + const hasManuallyModifiedModels = this.models.some((model) => + this.configPresenter.getModelStatus(providerId, model.id) + ) + if (hasManuallyModifiedModels) return + + // 检查是否有任何已启用的模型 + const hasEnabledModels = this.models.some((model) => + this.configPresenter.getModelStatus(providerId, model.id) + ) + + // 不再自动启用模型,让用户手动选择启用需要的模型 + if (!hasEnabledModels) { + console.info( + `Provider ${this.provider.name} models loaded, please manually enable the models you need` + ) + } + } + + // Helper function to get and format safety settings + private async getFormattedSafetySettings(): Promise { + const safetySettings: SafetySetting[] = [] + + for (const key of safetySettingKeys) { + try { + // Use configPresenter to get the setting value for the 'gemini' provider + // Assuming getSetting returns the string value like 'BLOCK_MEDIUM_AND_ABOVE' + const settingValue = + (await this.configPresenter.getSetting( + `geminiSafety_${key}` // Match the key used in settings store + )) || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED' // Default if not set + + const threshold = valueToHarmBlockThresholdMap[settingValue] + const category = keyToHarmCategoryMap[key] + + // Only add if threshold is defined, category is defined, and threshold is not BLOCK_NONE + if ( + threshold && + category && + threshold !== 'BLOCK_NONE' && + threshold !== 'HARM_BLOCK_THRESHOLD_UNSPECIFIED' + ) { + safetySettings.push({ category, threshold }) + } + } catch (error) { + console.warn(`Failed to retrieve or map safety setting for ${key}:`, error) + } + } + + return safetySettings.length > 0 ? safetySettings : undefined + } + + // 判断模型是否支持 thinkingBudget + private supportsThinkingBudget(modelId: string): boolean { + const normalized = modelId.replace(/^models\//i, '') + const range = modelCapabilities.getThinkingBudgetRange(this.provider.id, normalized) + return ( + typeof range.default === 'number' || + typeof range.min === 'number' || + typeof range.max === 'number' + ) + } + + // 获取生成配置,不再创建模型实例 + private getGenerateContentConfig( + temperature?: number, + maxTokens?: number, + modelId?: string, + reasoning?: boolean, + thinkingBudget?: number + ): GenerateContentConfig { + const config: GenerateContentConfig = { + temperature, + maxOutputTokens: maxTokens, + topP: 1 // topP默认为1.0 + } + + // 从当前模型列表中查找指定的模型 + if (modelId && this.models) { + const model = this.models.find((m) => m.id === modelId) + if (model && model.type === ModelType.ImageGeneration) { + config.responseModalities = [Modality.TEXT, Modality.IMAGE] + } + } + + // 正确配置思考功能 + if (reasoning) { + config.thinkingConfig = { + includeThoughts: true + } + + // 仅对支持 thinkingBudget 的 Gemini 2.5 系列模型添加 thinkingBudget 参数 + if (modelId && this.supportsThinkingBudget(modelId) && thinkingBudget !== undefined) { + config.thinkingConfig.thinkingBudget = thinkingBudget + } + } + + return config + } + + // 将 ChatMessage 转换为 Gemini 格式的消息 + private formatVertexMessages(messages: ChatMessage[]): { + systemInstruction: string + contents: Content[] + } { + // 提取系统消息 + const systemMessages = messages.filter((msg) => msg.role === 'system') + let systemContent = '' + if (systemMessages.length > 0) { + systemContent = systemMessages.map((msg) => msg.content).join('\n') + } + + // 创建Gemini内容数组 + const formattedContents: Content[] = [] + + // 处理非系统消息 + const nonSystemMessages = messages.filter((msg) => msg.role !== 'system') + for (let i = 0; i < nonSystemMessages.length; i++) { + const message = nonSystemMessages[i] + + // 检查是否是带有tool_calls的assistant消息 + if (message.role === 'assistant' && 'tool_calls' in message) { + // 处理tool_calls消息 + for (const toolCall of message.tool_calls || []) { + // 添加模型发出的函数调用 + formattedContents.push({ + role: 'model', + parts: [ + { + functionCall: { + name: toolCall.function.name, + args: JSON.parse(toolCall.function.arguments || '{}') + } + } + ] + }) + + // 查找对应的工具响应消息 + const nextMessage = i + 1 < nonSystemMessages.length ? nonSystemMessages[i + 1] : null + if ( + nextMessage && + nextMessage.role === 'tool' && + 'tool_call_id' in nextMessage && + nextMessage.tool_call_id === toolCall.id + ) { + // 添加用户角色的函数响应 + formattedContents.push({ + role: 'user', + parts: [ + { + functionResponse: { + name: toolCall.function.name, + response: { + result: + typeof nextMessage.content === 'string' + ? nextMessage.content + : JSON.stringify(nextMessage.content) + } + } + } + ] + }) + + // 跳过下一条消息,因为已经处理过了 + i++ + } + } + continue + } + + // 为每条消息创建parts数组 + const parts: Part[] = [] + + // 检查消息是否包含工具调用或工具响应 + if (message.role === 'tool' && Array.isArray(message.content)) { + // 处理工具消息 + for (const part of message.content) { + // @ts-ignore - 处理类型兼容性 + if (part.type === 'function_call' && part.function_call) { + // 处理函数调用 + parts.push({ + // @ts-ignore - 处理类型兼容性 + functionCall: { + // @ts-ignore - 处理类型兼容性 + name: part.function_call.name || '', + // @ts-ignore - 处理类型兼容性 + args: part.function_call.arguments ? JSON.parse(part.function_call.arguments) : {} + } + }) + // @ts-ignore - 处理类型兼容性 + } else if (part.type === 'function_response') { + // 处理函数响应 + // @ts-ignore - 处理类型兼容性 + parts.push({ text: part.function_response || '' }) + } + } + } else if (typeof message.content === 'string') { + // 处理消息内容 - 可能是字符串或包含图片的数组 + // 处理纯文本消息 + // 只添加非空文本 + if (message.content.trim() !== '') { + parts.push({ text: message.content }) + } + } else if (Array.isArray(message.content)) { + // 处理多模态消息(带图片等) + for (const part of message.content) { + if (part.type === 'text') { + // 只添加非空文本 + if (part.text && part.text.trim() !== '') { + parts.push({ text: part.text }) + } + } else if (part.type === 'image_url' && part.image_url) { + // 处理图片(假设是 base64 格式) + const matches = part.image_url.url.match(/^data:([^;]+);base64,(.+)$/) + if (matches && matches.length === 3) { + const mimeType = matches[1] + const base64Data = matches[2] + parts.push({ + inlineData: { + data: base64Data, + mimeType: mimeType + } + }) + } + } + } + } + + // 只有当parts不为空时,才添加到formattedContents中 + if (parts.length > 0) { + // 将消息角色转换为Gemini支持的角色 + let role: 'user' | 'model' = 'user' + if (message.role === 'assistant') { + role = 'model' + } else if (message.role === 'tool') { + // 工具消息作为用户消息处理 + role = 'user' + } + + formattedContents.push({ + role: role, + parts: parts + }) + } + } + + return { systemInstruction: systemContent, contents: formattedContents } + } + + // 处理 Vertex API 响应,支持新旧格式的思考内容 + private processVertexResponse(result: any): LLMResponse { + const resultResp: LLMResponse = { + content: '' + } + + let textContent = '' + let thoughtContent = '' + + // 检查是否有候选响应和 parts + if (result.candidates && result.candidates[0]?.content?.parts) { + for (const part of result.candidates[0].content.parts) { + // 检查是否是思考内容 (新格式) + if ((part as any).thought === true && part.text) { + thoughtContent += part.text + } else if (part.text) { + textContent += part.text + } + } + } else { + // 回退到使用 result.text + textContent = result.text || '' + } + + // 如果没有检测到新格式的思考内容,检查旧格式的 标签 + if (!thoughtContent && textContent.includes('')) { + const thinkStart = textContent.indexOf('') + const thinkEnd = textContent.indexOf('') + + if (thinkEnd > thinkStart) { + // 提取reasoning_content + thoughtContent = textContent.substring(thinkStart + 7, thinkEnd).trim() + + // 合并前后的普通内容 + const beforeThink = textContent.substring(0, thinkStart).trim() + const afterThink = textContent.substring(thinkEnd + 8).trim() + textContent = [beforeThink, afterThink].filter(Boolean).join('\n') + } + } + + resultResp.content = textContent + if (thoughtContent) { + resultResp.reasoning_content = thoughtContent + } + + return resultResp + } + + // 实现抽象方法 + async completions( + messages: { role: 'system' | 'user' | 'assistant'; content: string }[], + modelId: string, + temperature?: number, + maxTokens?: number + ): Promise { + try { + if (!this.genAI) { + throw new Error('Google Generative AI client is not initialized') + } + + const { systemInstruction, contents } = this.formatVertexMessages(messages) + + // 创建 GenerateContentConfig + const generateContentConfig: GenerateContentConfig = this.getGenerateContentConfig( + temperature ?? 0.7, + maxTokens, + modelId, + false // completions 方法中不处理 reasoning + ) + + if (systemInstruction) { + generateContentConfig.systemInstruction = systemInstruction + } + + // 一次性创建 requestParams + const requestParams: GenerateContentParameters = { + model: modelId, + contents, + config: generateContentConfig + } + + const result = await this.genAI.models.generateContent({ + ...requestParams, + model: this.ensureVertexModelName(requestParams.model as string) + }) + + const resultResp: LLMResponse = { + content: '' + } + + // 尝试获取tokens信息 - 使用新SDK的usageMetadata结构 + try { + if (result.usageMetadata) { + const usage = result.usageMetadata + resultResp.totalUsage = { + prompt_tokens: usage.promptTokenCount || 0, + completion_tokens: usage.candidatesTokenCount || 0, + total_tokens: usage.totalTokenCount || 0 + } + } else { + // 估算token数量 - 简单方法,可以根据实际需要调整 + const promptText = messages.map((m) => m.content).join(' ') + const responseText = result.text || '' + + // 简单估算: 英文约1个token/4个字符,中文约1个token/1.5个字符 + const estimateTokens = (text: string): number => { + const chineseCharCount = (text.match(/[\u4e00-\u9fa5]/g) || []).length + const otherCharCount = text.length - chineseCharCount + return Math.ceil(chineseCharCount / 1.5 + otherCharCount / 4) + } + + const promptTokens = estimateTokens(promptText) + const completionTokens = estimateTokens(responseText) + + resultResp.totalUsage = { + prompt_tokens: promptTokens, + completion_tokens: completionTokens, + total_tokens: promptTokens + completionTokens + } + } + } catch (e) { + console.warn('Failed to estimate token count for Vertex response', e) + } + + // 处理响应内容,支持新格式的思考内容 + let textContent = '' + let thoughtContent = '' + + // 检查是否有候选响应和 parts + if (result.candidates && result.candidates[0]?.content?.parts) { + for (const part of result.candidates[0].content.parts) { + // 检查是否是思考内容 (新格式) + if ((part as any).thought === true && part.text) { + thoughtContent += part.text + } else if (part.text) { + textContent += part.text + } + } + } else { + // 回退到使用 result.text + textContent = result.text || '' + } + + // 如果没有检测到新格式的思考内容,检查旧格式的 标签 + if (!thoughtContent && textContent.includes('')) { + const thinkStart = textContent.indexOf('') + const thinkEnd = textContent.indexOf('') + + if (thinkEnd > thinkStart) { + // 提取reasoning_content + thoughtContent = textContent.substring(thinkStart + 7, thinkEnd).trim() + + // 合并前后的普通内容 + const beforeThink = textContent.substring(0, thinkStart).trim() + const afterThink = textContent.substring(thinkEnd + 8).trim() + textContent = [beforeThink, afterThink].filter(Boolean).join('\n') + } + } + + resultResp.content = textContent + if (thoughtContent) { + resultResp.reasoning_content = thoughtContent + } + + return resultResp + } catch (error) { + console.error('Vertex completions error:', error) + throw error + } + } + + async summaries( + text: string, + modelId: string, + temperature?: number, + maxTokens?: number + ): Promise { + if (!this.isInitialized) { + throw new Error('Provider not initialized') + } + + if (!modelId) { + throw new Error('Model ID is required') + } + + try { + const prompt = `Please generate a concise summary for the following content:\n\n${text}` + + const result = await this.genAI.models.generateContent({ + model: this.ensureVertexModelName(modelId), + contents: [{ role: 'user', parts: [{ text: prompt }] }], + config: this.getGenerateContentConfig(temperature, maxTokens, modelId, false) + }) + + return this.processVertexResponse(result) + } catch (error) { + console.error('Vertex summaries error:', error) + throw error + } + } + + async generateText( + prompt: string, + modelId: string, + temperature?: number, + maxTokens?: number + ): Promise { + if (!this.isInitialized) { + throw new Error('Provider not initialized') + } + + if (!modelId) { + throw new Error('Model ID is required') + } + + try { + const result = await this.genAI.models.generateContent({ + model: this.ensureVertexModelName(modelId), + contents: [{ role: 'user', parts: [{ text: prompt }] }], + config: this.getGenerateContentConfig(temperature, maxTokens, modelId, false) + }) + + return this.processVertexResponse(result) + } catch (error) { + console.error('Vertex generateText error:', error) + throw error + } + } + + async suggestions( + context: string, + modelId: string, + temperature?: number, + maxTokens?: number + ): Promise { + if (!this.isInitialized) { + throw new Error('Provider not initialized') + } + + if (!modelId) { + throw new Error('Model ID is required') + } + + try { + const prompt = `Based on the following context, please provide up to 5 reasonable suggestion options, each not exceeding 100 characters. Please return in JSON array format without other explanations:\n\n${context}` + + const result = await this.genAI.models.generateContent({ + model: this.ensureVertexModelName(modelId), + contents: [{ role: 'user', parts: [{ text: prompt }] }], + config: this.getGenerateContentConfig(temperature, maxTokens, modelId, false) + }) + + const responseText = result.text || '' + + // 尝试从响应中解析出JSON数组 + try { + const cleanedText = responseText.replace(/```json|```/g, '').trim() + const suggestions = JSON.parse(cleanedText) + if (Array.isArray(suggestions)) { + return suggestions.map((item) => item.toString()) + } + } catch (parseError) { + console.error('Vertex suggestions parseError:', parseError) + // 如果解析失败,尝试分行处理 + const lines = responseText + .split('\n') + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('```') && !line.includes(':')) + .map((line) => line.replace(/^[0-9]+\.\s*/, '').replace(/^-\s*/, '')) + + if (lines.length > 0) { + return lines.slice(0, 5) + } + } + + // If all fail, return a default prompt + return ['Unable to generate suggestions'] + } catch (error) { + console.error('Vertex suggestions error:', error) + return ['Error occurred, unable to get suggestions'] + } + } + /** + * 核心流式处理方法 + * 实现BaseLLMProvider中的抽象方法 + */ + async *coreStream( + messages: ChatMessage[], + modelId: string, + modelConfig: ModelConfig, + temperature: number, + maxTokens: number, + mcpTools: MCPToolDefinition[] + ): AsyncGenerator { + if (!this.isInitialized) throw new Error('Provider not initialized') + if (!modelId) throw new Error('Model ID is required') + console.log('modelConfig', modelConfig, modelId) + + // 检查是否是图片生成模型 + const isImageGenerationModel = modelConfig?.type === ModelType.ImageGeneration + + // 如果是图片生成模型,使用特殊处理 + if (isImageGenerationModel) { + yield* this.handleImageGenerationStream(messages, modelId, temperature, maxTokens) + return + } + + const safetySettings = await this.getFormattedSafetySettings() + console.log('safetySettings', safetySettings) + + // 添加Gemini工具调用 + let geminiTools: Tool[] = [] + + // 注意:googleSearch内置工具与外部工具是互斥的 + if (modelConfig.enableSearch) { + geminiTools.push({ googleSearch: {} as GoogleSearch }) + } else { + if (mcpTools.length > 0) + geminiTools = await presenter.mcpPresenter.mcpToolsToGeminiTools(mcpTools, this.provider.id) + } + + // 格式化消息为Gemini格式 + const formattedParts = this.formatVertexMessages(messages) + + // 1. 获取基础 config + const generateContentConfig: GenerateContentConfig = this.getGenerateContentConfig( + temperature, + maxTokens, + modelId, + modelConfig.reasoning, + modelConfig.thinkingBudget + ) + + // 2. 在本地变量上添加其他属性 + if (formattedParts.systemInstruction) { + generateContentConfig.systemInstruction = formattedParts.systemInstruction + } + + if (geminiTools.length > 0) { + generateContentConfig.tools = geminiTools + // 仅当存在 functionDeclarations 时才配置 functionCallingConfig + const hasFunctionDeclarations = geminiTools.some((t: any) => { + const fns = t?.functionDeclarations + return Array.isArray(fns) && fns.length > 0 + }) + if (hasFunctionDeclarations) { + generateContentConfig.toolConfig = { + functionCallingConfig: { + mode: FunctionCallingConfigMode.AUTO // 允许模型自动决定是否调用工具 + } + } + } + } + + if (safetySettings) { + generateContentConfig.safetySettings = safetySettings + } + + // 3. 一次性创建完整的 requestParams + const requestParams: GenerateContentParameters = { + model: modelId, + contents: formattedParts.contents, + config: generateContentConfig + } + + // 发送流式请求 + const result = await this.genAI.models.generateContentStream({ + ...requestParams, + model: this.ensureVertexModelName(requestParams.model as string) + }) + + // 状态变量 + let buffer = '' + let isInThinkTag = false + let toolUseDetected = false + let usageMetadata: GenerateContentResponseUsageMetadata | undefined + let isNewThoughtFormatDetected = modelConfig.reasoning === true + + // 流处理循环 + for await (const chunk of result) { + // 处理用量统计 + if (chunk.usageMetadata) { + usageMetadata = chunk.usageMetadata + } + + // 检查是否包含函数调用 + if (chunk.candidates && chunk.candidates[0]?.content?.parts?.[0]?.functionCall) { + const functionCall = chunk.candidates[0].content.parts[0].functionCall + const functionName = functionCall.name + const functionArgs = functionCall.args || {} + const toolCallId = `gemini-tool-${Date.now()}` + + toolUseDetected = true + + // 发送工具调用开始事件 + yield createStreamEvent.toolCallStart(toolCallId, functionName || '') + + // 发送工具调用参数 + const argsString = JSON.stringify(functionArgs) + yield createStreamEvent.toolCallChunk(toolCallId, argsString) + + // 发送工具调用结束事件 + yield createStreamEvent.toolCallEnd(toolCallId, argsString) + + // 设置停止原因为工具使用 + break + } + + // 处理内容块 + let content = '' + let thoughtContent = '' + + // 处理文本和图像内容 + if (chunk.candidates && chunk.candidates[0]?.content?.parts) { + for (const part of chunk.candidates[0].content.parts) { + // 检查是否是思考内容 (新格式) + if ((part as any).thought === true && part.text) { + isNewThoughtFormatDetected = true + thoughtContent += part.text + } else if (part.text) { + content += part.text + } else if (part.inlineData && part.inlineData.data && part.inlineData.mimeType) { + // 处理图像数据 + yield createStreamEvent.imageData({ + data: part.inlineData.data, + mimeType: part.inlineData.mimeType + }) + } + } + } else { + // 兼容处理 + content = chunk.text || '' + } + + // 如果检测到思考内容,直接发送 + if (thoughtContent) { + yield createStreamEvent.reasoning(thoughtContent) + } + + if (!content) continue + + if (isNewThoughtFormatDetected) { + yield createStreamEvent.text(content) + } else { + buffer += content + + if (buffer.includes('') && !isInThinkTag) { + const thinkStart = buffer.indexOf('') + if (thinkStart > 0) { + yield createStreamEvent.text(buffer.substring(0, thinkStart)) + } + buffer = buffer.substring(thinkStart + 7) + isInThinkTag = true + } + + if (isInThinkTag && buffer.includes('')) { + const thinkEnd = buffer.indexOf('') + const reasoningContent = buffer.substring(0, thinkEnd) + if (reasoningContent) { + yield createStreamEvent.reasoning(reasoningContent) + } + buffer = buffer.substring(thinkEnd + 8) + isInThinkTag = false + } + + if (!isInThinkTag && buffer) { + yield createStreamEvent.text(buffer) + buffer = '' + } + } + } + + if (usageMetadata) { + yield createStreamEvent.usage({ + prompt_tokens: usageMetadata.promptTokenCount || 0, + completion_tokens: usageMetadata.candidatesTokenCount || 0, + total_tokens: usageMetadata.totalTokenCount || 0 + }) + } + + // 处理剩余缓冲区内容 + if (!isNewThoughtFormatDetected && buffer) { + if (isInThinkTag) { + yield createStreamEvent.reasoning(buffer) + } else { + yield createStreamEvent.text(buffer) + } + } + + // 发送停止事件 + yield createStreamEvent.stop(toolUseDetected ? 'tool_use' : 'complete') + } + + /** + * 处理图片生成模型的流式输出 + */ + private async *handleImageGenerationStream( + messages: ChatMessage[], + modelId: string, + temperature?: number, + maxTokens?: number + ): AsyncGenerator { + try { + // 提取用户消息并构建parts数组 + const userMessage = messages.findLast((msg) => msg.role === 'user') + if (!userMessage) { + throw new Error('No user message found for image generation') + } + + // 构建包含文本和图片的parts数组,参考formatVertexMessages的逻辑 + const parts: Part[] = [] + + if (typeof userMessage.content === 'string') { + // 处理纯文本消息 + if (userMessage.content.trim() !== '') { + parts.push({ text: userMessage.content }) + } + } else if (Array.isArray(userMessage.content)) { + // 处理多模态消息(带图片等) + for (const part of userMessage.content) { + if (part.type === 'text') { + // 只添加非空文本 + if (part.text && part.text.trim() !== '') { + parts.push({ text: part.text }) + } + } else if (part.type === 'image_url' && part.image_url) { + // 处理图片(假设是 base64 格式) + const matches = part.image_url.url.match(/^data:([^;]+);base64,(.+)$/) + if (matches && matches.length === 3) { + const mimeType = matches[1] + const base64Data = matches[2] + parts.push({ + inlineData: { + data: base64Data, + mimeType: mimeType + } + }) + } + } + } + } + + // 如果没有有效的parts,抛出错误 + if (parts.length === 0) { + throw new Error('No valid content found for image generation') + } + + // 发送生成请求 + const result = await this.genAI.models.generateContentStream({ + model: this.ensureVertexModelName(modelId), + contents: [{ role: 'user', parts }], + config: this.getGenerateContentConfig(temperature, maxTokens, modelId, false) // 图像生成不需要reasoning + }) + + // 处理流式响应 + for await (const chunk of result) { + if (chunk.candidates && chunk.candidates[0]?.content?.parts) { + for (const part of chunk.candidates[0].content.parts) { + if (part.text) { + // 输出文本内容 + yield createStreamEvent.text(part.text) + } else if (part.inlineData) { + // 输出图像数据 + yield createStreamEvent.imageData({ + data: part.inlineData.data || '', + mimeType: part.inlineData.mimeType || '' + }) + } + } + } + } + + // 发送停止事件 + yield createStreamEvent.stop('complete') + } catch (error) { + console.error('Image generation stream error:', error) + yield createStreamEvent.error( + error instanceof Error ? error.message : 'Image generation failed' + ) + yield createStreamEvent.stop('error') + } + } + + async getEmbeddings(modelId: string, texts: string[]): Promise { + if (!this.genAI) throw new Error('Google Generative AI client is not initialized') + // Vertex embedContent 支持批量输入 + const resp = await this.genAI.models.embedContent({ + model: this.ensureVertexModelName(modelId), + contents: texts.map((text) => ({ + parts: [{ text }] + })) + }) + // resp.embeddings?: ContentEmbedding[] + if (resp && Array.isArray(resp.embeddings)) { + return resp.embeddings.map((e) => (Array.isArray(e.values) ? e.values : [])) + } + // 若无返回,抛出异常 + throw new Error('Vertex AI embedding API did not return embeddings') + } +} diff --git a/src/renderer/settings/components/ModelProviderSettingsDetail.vue b/src/renderer/settings/components/ModelProviderSettingsDetail.vue index e6f1d4545..cf8038f2d 100644 --- a/src/renderer/settings/components/ModelProviderSettingsDetail.vue +++ b/src/renderer/settings/components/ModelProviderSettingsDetail.vue @@ -16,6 +16,15 @@ + + + + +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+ +
+
+ + + diff --git a/src/renderer/src/components/icons/ModelIcon.vue b/src/renderer/src/components/icons/ModelIcon.vue index 32596890f..1f85ccd79 100644 --- a/src/renderer/src/components/icons/ModelIcon.vue +++ b/src/renderer/src/components/icons/ModelIcon.vue @@ -106,6 +106,7 @@ const icons = { upstage: upstageColorIcon, vercel: vercelColorIcon, vertexai: vertexaiColorIcon, + vertex: vertexaiColorIcon, vidu: viduColorIcon, viggle: viggleColorIcon, tiangong: tiangongColorIcon, diff --git a/src/renderer/src/i18n/da-DK/settings.json b/src/renderer/src/i18n/da-DK/settings.json index f8a42dcc3..ca030a06e 100644 --- a/src/renderer/src/i18n/da-DK/settings.json +++ b/src/renderer/src/i18n/da-DK/settings.json @@ -489,7 +489,19 @@ "operationSuccess": "Handling gennemført", "settingsApplied": "Indstillinger anvendt", "bedrockLimitTip": "* Kun Anthropic Claude (Opus, Sonnet, Haiku) understøttes", - "bedrockVerifyTip": "DeepChat bruger Claude 3.5 Sonnet til verifikation. Hvis du ikke har adgang, mislykkes testen – dette påvirker ikke andre modeller." + "bedrockVerifyTip": "DeepChat bruger Claude 3.5 Sonnet til verifikation. Hvis du ikke har adgang, mislykkes testen – dette påvirker ikke andre modeller.", + "vertexApiVersion": "API version", + "vertexEndpointExpress": "Express (globalt slutpunkt)", + "vertexEndpointMode": "slutpunktstilstand", + "vertexEndpointStandard": "Standard (områdeslutpunkt)", + "vertexLocation": "areal", + "vertexLocationPlaceholder": "Indtast venligst en region (f.eks. us-central1)", + "vertexPrivateKey": "Tjenestekonto privat nøgle", + "vertexPrivateKeyPlaceholder": "Indsæt den private nøgle (understøtter enkelt linje \\n)", + "vertexProjectId": "Projekt ID", + "vertexProjectIdPlaceholder": "Indtast dit Google Cloud-projekt-id", + "vertexServiceEmailPlaceholder": "Indtast venligst tjenestekontoens e-mail", + "vertexServiceEmail": "Servicekonto-e-mail" }, "knowledgeBase": { "title": "Indstillinger for vidensbase", diff --git a/src/renderer/src/i18n/en-US/settings.json b/src/renderer/src/i18n/en-US/settings.json index 1d06f9205..f7cb503be 100644 --- a/src/renderer/src/i18n/en-US/settings.json +++ b/src/renderer/src/i18n/en-US/settings.json @@ -274,6 +274,18 @@ "accessKeyIdPlaceholder": "Please enter AWS Access Key ID", "secretAccessKeyPlaceholder": "Please enter AWS Secret Access Key", "regionPlaceholder": "Please enter AWS region", + "vertexProjectId": "Project ID", + "vertexProjectIdPlaceholder": "Please enter your Google Cloud project ID", + "vertexLocation": "area", + "vertexLocationPlaceholder": "Please enter a region (e.g. us-central1)", + "vertexServiceEmail": "Service account email", + "vertexServiceEmailPlaceholder": "Please enter the service account email", + "vertexPrivateKey": "Service account private key", + "vertexPrivateKeyPlaceholder": "Paste the private key (supports single line \\n)", + "vertexApiVersion": "API version", + "vertexEndpointMode": "endpoint mode", + "vertexEndpointStandard": "Standard (region endpoint)", + "vertexEndpointExpress": "Express (global endpoint)", "verifyKey": "Verify Key", "howToGet": "How to get", "getKeyTip": "Please visit", diff --git a/src/renderer/src/i18n/fa-IR/settings.json b/src/renderer/src/i18n/fa-IR/settings.json index 5afb30c5c..0fc4f9afd 100644 --- a/src/renderer/src/i18n/fa-IR/settings.json +++ b/src/renderer/src/i18n/fa-IR/settings.json @@ -274,6 +274,18 @@ "accessKeyIdPlaceholder": "لطفاً AWS Access Key ID را وارد کنید", "secretAccessKeyPlaceholder": "لطفاً AWS Secret Access Key را وارد کنید", "regionPlaceholder": "لطفاً منطقه AWS را وارد کنید", + "vertexProjectId": "شناسه پروژه", + "vertexProjectIdPlaceholder": "لطفا شناسه پروژه Google Cloud خود را وارد کنید", + "vertexLocation": "منطقه", + "vertexLocationPlaceholder": "لطفاً یک منطقه (به عنوان مثال us-central1) را وارد کنید", + "vertexServiceEmail": "ایمیل حساب سرویس", + "vertexServiceEmailPlaceholder": "لطفا ایمیل حساب سرویس را وارد کنید", + "vertexPrivateKey": "کلید خصوصی حساب سرویس", + "vertexPrivateKeyPlaceholder": "کلید خصوصی را جایگذاری کنید (از یک خط پشتیبانی می کند \\n)", + "vertexApiVersion": "نسخه API", + "vertexEndpointMode": "حالت نقطه پایانی", + "vertexEndpointStandard": "استاندارد (نقطه پایانی منطقه)", + "vertexEndpointExpress": "اکسپرس (نقطه پایانی جهانی)", "verifyKey": "پذیرش کلید", "howToGet": "چگونه دریافت کنیم", "getKeyTip": "لطفاً به", diff --git a/src/renderer/src/i18n/fr-FR/settings.json b/src/renderer/src/i18n/fr-FR/settings.json index 09873a00e..ebfd842e1 100644 --- a/src/renderer/src/i18n/fr-FR/settings.json +++ b/src/renderer/src/i18n/fr-FR/settings.json @@ -274,6 +274,18 @@ "accessKeyIdPlaceholder": "Veuillez entrer l'AWS Access Key ID", "secretAccessKeyPlaceholder": "Veuillez entrer l'AWS Secret Access Key", "regionPlaceholder": "Veuillez entrer la région AWS", + "vertexProjectId": "ID du projet", + "vertexProjectIdPlaceholder": "Veuillez saisir l'ID de votre projet Google Cloud", + "vertexLocation": "zone", + "vertexLocationPlaceholder": "Veuillez entrer une région (par exemple us-central1)", + "vertexServiceEmail": "E-mail du compte de service", + "vertexServiceEmailPlaceholder": "Veuillez saisir l'e-mail du compte de service", + "vertexPrivateKey": "Clé privée du compte de service", + "vertexPrivateKeyPlaceholder": "Collez la clé privée (prend en charge une seule ligne \\n)", + "vertexApiVersion": "Version API", + "vertexEndpointMode": "mode point de terminaison", + "vertexEndpointStandard": "Standard (point de terminaison de la région)", + "vertexEndpointExpress": "Express (point de terminaison mondial)", "verifyKey": "Vérifier la clé", "howToGet": "Comment obtenir", "getKeyTip": "Veuillez visiter", diff --git a/src/renderer/src/i18n/ja-JP/settings.json b/src/renderer/src/i18n/ja-JP/settings.json index ad179db46..4a9728c75 100644 --- a/src/renderer/src/i18n/ja-JP/settings.json +++ b/src/renderer/src/i18n/ja-JP/settings.json @@ -274,6 +274,18 @@ "accessKeyIdPlaceholder": "AWS Access Key IDを入力してください", "secretAccessKeyPlaceholder": "AWS Secret Access Keyを入力してください", "regionPlaceholder": "AWSリージョンを入力してください", + "vertexProjectId": "プロジェクトID", + "vertexProjectIdPlaceholder": "Google Cloud プロジェクト ID を入力してください", + "vertexLocation": "エリア", + "vertexLocationPlaceholder": "地域を入力してください (例: us-central1)", + "vertexServiceEmail": "サービスアカウントのメールアドレス", + "vertexServiceEmailPlaceholder": "サービスアカウントのメールアドレスを入力してください", + "vertexPrivateKey": "サービスアカウントの秘密キー", + "vertexPrivateKeyPlaceholder": "秘密キーを貼り付けます (単一行をサポート\\n)", + "vertexApiVersion": "API バージョン", + "vertexEndpointMode": "エンドポイントモード", + "vertexEndpointStandard": "標準(リージョンエンドポイント)", + "vertexEndpointExpress": "Express(グローバルエンドポイント)", "verifyKey": "キーを検証", "howToGet": "取得方法", "getKeyTip": "以下へアクセスしてください", diff --git a/src/renderer/src/i18n/ko-KR/settings.json b/src/renderer/src/i18n/ko-KR/settings.json index 8d4546c05..cdcf336f2 100644 --- a/src/renderer/src/i18n/ko-KR/settings.json +++ b/src/renderer/src/i18n/ko-KR/settings.json @@ -274,6 +274,18 @@ "accessKeyIdPlaceholder": "AWS Access Key ID를 입력하세요", "secretAccessKeyPlaceholder": "AWS Secret Access Key를 입력하세요", "regionPlaceholder": "AWS 리전을 입력하세요", + "vertexProjectId": "프로젝트 ID", + "vertexProjectIdPlaceholder": "Google Cloud 프로젝트 ID를 입력하세요.", + "vertexLocation": "영역", + "vertexLocationPlaceholder": "지역을 입력하세요(예: us-central1).", + "vertexServiceEmail": "서비스 계정 이메일", + "vertexServiceEmailPlaceholder": "서비스 계정 이메일을 입력하세요.", + "vertexPrivateKey": "서비스 계정 비공개 키", + "vertexPrivateKeyPlaceholder": "개인 키 붙여넣기(한 줄 지원\\n)", + "vertexApiVersion": "API 버전", + "vertexEndpointMode": "엔드포인트 모드", + "vertexEndpointStandard": "표준(지역 엔드포인트)", + "vertexEndpointExpress": "Express(글로벌 엔드포인트)", "verifyKey": "키 확인", "howToGet": "얻는 방법", "getKeyTip": "다음으로 이동하세요", diff --git a/src/renderer/src/i18n/pt-BR/settings.json b/src/renderer/src/i18n/pt-BR/settings.json index bf34eade9..862d63c4a 100644 --- a/src/renderer/src/i18n/pt-BR/settings.json +++ b/src/renderer/src/i18n/pt-BR/settings.json @@ -274,6 +274,18 @@ "accessKeyIdPlaceholder": "Por favor, insira o ID da Chave de Acesso da AWS", "secretAccessKeyPlaceholder": "Por favor, insira a Chave de Acesso Secreta da AWS", "regionPlaceholder": "Por favor, insira a região da AWS", + "vertexProjectId": "ID do projeto", + "vertexProjectIdPlaceholder": "Insira o ID do projeto do Google Cloud", + "vertexLocation": "área", + "vertexLocationPlaceholder": "Insira uma região (por exemplo, us-central1)", + "vertexServiceEmail": "E-mail da conta de serviço", + "vertexServiceEmailPlaceholder": "Insira o e-mail da conta de serviço", + "vertexPrivateKey": "Chave privada da conta de serviço", + "vertexPrivateKeyPlaceholder": "Cole a chave privada (suporta linha única \\n)", + "vertexApiVersion": "Versão da API", + "vertexEndpointMode": "modo de ponto final", + "vertexEndpointStandard": "Padrão (ponto final da região)", + "vertexEndpointExpress": "Expresso (endpoint global)", "verifyKey": "Verificar Chave", "howToGet": "Como obter", "getKeyTip": "Por favor, visite", diff --git a/src/renderer/src/i18n/ru-RU/settings.json b/src/renderer/src/i18n/ru-RU/settings.json index f24e12464..26f7dc715 100644 --- a/src/renderer/src/i18n/ru-RU/settings.json +++ b/src/renderer/src/i18n/ru-RU/settings.json @@ -274,6 +274,18 @@ "accessKeyIdPlaceholder": "Введите AWS Access Key ID", "secretAccessKeyPlaceholder": "Введите AWS Secret Access Key", "regionPlaceholder": "Введите регион AWS", + "vertexProjectId": "Идентификатор проекта", + "vertexProjectIdPlaceholder": "Введите идентификатор проекта Google Cloud.", + "vertexLocation": "область", + "vertexLocationPlaceholder": "Пожалуйста, введите регион (например, us-central1)", + "vertexServiceEmail": "Адрес электронной почты сервисного аккаунта", + "vertexServiceEmailPlaceholder": "Пожалуйста, введите адрес электронной почты сервисного аккаунта", + "vertexPrivateKey": "Закрытый ключ сервисного аккаунта", + "vertexPrivateKeyPlaceholder": "Вставьте закрытый ключ (поддерживается одна строка \\n)", + "vertexApiVersion": "версия API", + "vertexEndpointMode": "режим конечной точки", + "vertexEndpointStandard": "Стандартный (конечная точка региона)", + "vertexEndpointExpress": "Экспресс (глобальная конечная точка)", "verifyKey": "Проверить ключ", "howToGet": "Как получить", "getKeyTip": "Перейдите по следующему адресу", diff --git a/src/renderer/src/i18n/zh-CN/settings.json b/src/renderer/src/i18n/zh-CN/settings.json index 357b3e6d7..3b299947f 100644 --- a/src/renderer/src/i18n/zh-CN/settings.json +++ b/src/renderer/src/i18n/zh-CN/settings.json @@ -274,6 +274,18 @@ "accessKeyIdPlaceholder": "请输入AWS Access Key ID", "secretAccessKeyPlaceholder": "请输入AWS Secret Access Key", "regionPlaceholder": "请输入AWS区域", + "vertexProjectId": "项目 ID", + "vertexProjectIdPlaceholder": "请输入 Google Cloud 项目 ID", + "vertexLocation": "区域", + "vertexLocationPlaceholder": "请输入区域(例如 us-central1)", + "vertexServiceEmail": "服务账号邮箱", + "vertexServiceEmailPlaceholder": "请输入服务账号邮箱", + "vertexPrivateKey": "服务账号私钥", + "vertexPrivateKeyPlaceholder": "粘贴私钥(支持单行 \\n)", + "vertexApiVersion": "API 版本", + "vertexEndpointMode": "端点模式", + "vertexEndpointStandard": "标准(区域端点)", + "vertexEndpointExpress": "Express(全局端点)", "verifyKey": "验证密钥", "howToGet": "如何获取", "refreshingModels": "刷新中...", diff --git a/src/renderer/src/i18n/zh-HK/settings.json b/src/renderer/src/i18n/zh-HK/settings.json index ba3ec949a..c5a0b83ea 100644 --- a/src/renderer/src/i18n/zh-HK/settings.json +++ b/src/renderer/src/i18n/zh-HK/settings.json @@ -274,6 +274,18 @@ "accessKeyIdPlaceholder": "請輸入AWS Access Key ID", "secretAccessKeyPlaceholder": "請輸入AWS Secret Access Key", "regionPlaceholder": "請輸入AWS區域", + "vertexProjectId": "項目 ID", + "vertexProjectIdPlaceholder": "請輸入 Google Cloud 項目 ID", + "vertexLocation": "區域", + "vertexLocationPlaceholder": "請輸入區域(例如 us-central1)", + "vertexServiceEmail": "服務賬號郵箱", + "vertexServiceEmailPlaceholder": "請輸入服務賬號郵箱", + "vertexPrivateKey": "服務賬號私鑰", + "vertexPrivateKeyPlaceholder": "粘貼私鑰(支持單行 \\n)", + "vertexApiVersion": "API 版本", + "vertexEndpointMode": "端點模式", + "vertexEndpointStandard": "標準(區域端點)", + "vertexEndpointExpress": "Express(全局端點)", "verifyKey": "驗證密鑰", "howToGet": "如何獲取", "getKeyTip": "請前往", diff --git a/src/renderer/src/i18n/zh-TW/settings.json b/src/renderer/src/i18n/zh-TW/settings.json index 5bc30e1ab..f887800e6 100644 --- a/src/renderer/src/i18n/zh-TW/settings.json +++ b/src/renderer/src/i18n/zh-TW/settings.json @@ -274,6 +274,18 @@ "accessKeyIdPlaceholder": "請輸入 AWS Access Key ID", "secretAccessKeyPlaceholder": "請輸入 AWS Secret Access Key", "regionPlaceholder": "請輸入 AWS 區域", + "vertexProjectId": "項目 ID", + "vertexProjectIdPlaceholder": "請輸入 Google Cloud 項目 ID", + "vertexLocation": "區域", + "vertexLocationPlaceholder": "請輸入區域(例如 us-central1)", + "vertexServiceEmail": "服務賬號郵箱", + "vertexServiceEmailPlaceholder": "請輸入服務賬號郵箱", + "vertexPrivateKey": "服務賬號私鑰", + "vertexPrivateKeyPlaceholder": "粘貼私鑰(支持單行 \\n)", + "vertexApiVersion": "API 版本", + "vertexEndpointMode": "端點模式", + "vertexEndpointStandard": "標準(區域端點)", + "vertexEndpointExpress": "Express(全局端點)", "verifyKey": "驗證金鑰", "howToGet": "如何取得", "getKeyTip": "請前往", diff --git a/src/renderer/src/stores/providerStore.ts b/src/renderer/src/stores/providerStore.ts index a5f92bf98..428fb6067 100644 --- a/src/renderer/src/stores/providerStore.ts +++ b/src/renderer/src/stores/providerStore.ts @@ -3,7 +3,7 @@ import { defineStore } from 'pinia' import { usePresenter } from '@/composables/usePresenter' import { useIpcQuery } from '@/composables/useIpcQuery' import { CONFIG_EVENTS, PROVIDER_DB_EVENTS } from '@/events' -import type { AWS_BEDROCK_PROVIDER, LLM_PROVIDER } from '@shared/presenter' +import type { AWS_BEDROCK_PROVIDER, LLM_PROVIDER, VERTEX_PROVIDER } from '@shared/presenter' const PROVIDER_ORDER_KEY = 'providerOrder' const PROVIDER_TIMESTAMP_KEY = 'providerTimestamps' @@ -293,6 +293,13 @@ export const useProviderStore = defineStore('provider', () => { return updateProviderConfig(providerId, updates) } + const updateVertexProviderConfig = async ( + providerId: string, + updates: Partial + ) => { + return updateProviderConfig(providerId, updates) + } + const checkProvider = async (providerId: string, modelId?: string) => { return llmP.check(providerId, modelId) } @@ -400,6 +407,7 @@ export const useProviderStore = defineStore('provider', () => { addCustomProvider, removeProvider, updateAwsBedrockProviderConfig, + updateVertexProviderConfig, checkProvider, setAzureApiVersion, getAzureApiVersion, diff --git a/src/shared/provider-operations.ts b/src/shared/provider-operations.ts index dbdc08193..0187ddb28 100644 --- a/src/shared/provider-operations.ts +++ b/src/shared/provider-operations.ts @@ -46,7 +46,13 @@ export const REBUILD_REQUIRED_FIELDS = [ 'secretAccessKey', // AWS Bedrock 'region', // AWS Bedrock 'azureResourceName', // Azure - 'azureApiVersion' // Azure + 'azureApiVersion', // Azure + 'projectId', // Vertex AI + 'location', // Vertex AI + 'accountPrivateKey', // Vertex AI + 'accountClientEmail', // Vertex AI + 'apiVersion', // Vertex AI + 'endpointMode' // Vertex AI ] as const /** diff --git a/src/shared/types/presenters/legacy.presenters.d.ts b/src/shared/types/presenters/legacy.presenters.d.ts index 0191c7316..046599751 100644 --- a/src/shared/types/presenters/legacy.presenters.d.ts +++ b/src/shared/types/presenters/legacy.presenters.d.ts @@ -767,6 +767,15 @@ export type AWS_BEDROCK_PROVIDER = LLM_PROVIDER & { credential?: AwsBedrockCredential } +export type VERTEX_PROVIDER = LLM_PROVIDER & { + projectId?: string + location?: string + accountPrivateKey?: string + accountClientEmail?: string + apiVersion?: 'v1' | 'v1beta1' + endpointMode?: 'standard' | 'express' +} + export interface AwsBedrockCredential { accessKeyId: string secretAccessKey: string diff --git a/src/shared/types/presenters/llmprovider.presenter.d.ts b/src/shared/types/presenters/llmprovider.presenter.d.ts index 476653e67..12eb25d89 100644 --- a/src/shared/types/presenters/llmprovider.presenter.d.ts +++ b/src/shared/types/presenters/llmprovider.presenter.d.ts @@ -91,6 +91,15 @@ export type AWS_BEDROCK_PROVIDER = LLM_PROVIDER & { credential?: AwsBedrockCredential } +export type VERTEX_PROVIDER = LLM_PROVIDER & { + projectId?: string + location?: string + accountPrivateKey?: string + accountClientEmail?: string + apiVersion?: 'v1' | 'v1beta1' + endpointMode?: 'standard' | 'express' +} + export interface OllamaModel { name: string size: number