From 5656c7950dcda9e5fa9c01d7fd544799f7880bc4 Mon Sep 17 00:00:00 2001 From: InterestingDarkness Date: Mon, 26 May 2025 22:09:59 +0800 Subject: [PATCH 01/18] Implement Vertex AI authentication modes and configuration in UI - Updated index.html to include options for Vertex AI authentication modes (Express and Full). - Enhanced openai.js to manage Vertex AI settings, including project ID, region, and service account JSON. - Added validation and handling for service account JSON in the backend. - Modified API request handling in google.js to support both authentication modes for Vertex AI. - Updated secrets.js to include a key for storing Vertex AI service account JSON. - Improved error handling and user feedback for authentication issues. --- public/index.html | 145 +++++++++++-- public/scripts/extensions/shared.js | 30 ++- public/scripts/openai.js | 227 ++++++++++++++++++++- public/scripts/secrets.js | 1 + src/endpoints/backends/chat-completions.js | 167 ++++++++++++++- src/endpoints/google.js | 150 ++++++++++++-- src/endpoints/secrets.js | 1 + 7 files changed, 675 insertions(+), 46 deletions(-) diff --git a/public/index.html b/public/index.html index a7c62b44f..0a4f008f0 100644 --- a/public/index.html +++ b/public/index.html @@ -2779,7 +2779,7 @@ - + @@ -3225,34 +3225,147 @@
-

- - Google Vertex AI API Key - - - (Express mode keys only) - -

+

Google Vertex AI Configuration

+ +
- - + +
-
- For privacy reasons, your API key will be hidden after you reload the page. + + +
+

+ + Google Vertex AI API Key + + + (Express mode) + +

+
+ + +
+
+ For privacy reasons, your API key will be hidden after you reload the page. +
+ + + + +

Google Model

diff --git a/public/scripts/extensions/shared.js b/public/scripts/extensions/shared.js index 01a6e8219..b8508ba05 100644 --- a/public/scripts/extensions/shared.js +++ b/public/scripts/extensions/shared.js @@ -56,6 +56,13 @@ export async function getMultimodalCaption(base64Img, prompt) { model: extension_settings.caption.multimodal_model || 'gpt-4-turbo', }; + // Add Vertex AI specific parameters if using Vertex AI + if (extension_settings.caption.multimodal_api === 'vertexai') { + requestBody.vertexai_auth_mode = oai_settings.vertexai_auth_mode; + requestBody.vertexai_project_id = oai_settings.vertexai_project_id; + requestBody.vertexai_region = oai_settings.vertexai_region; + } + if (isOllama) { if (extension_settings.caption.multimodal_model === 'ollama_current') { requestBody.model = textgenerationwebui_settings.ollama_model; @@ -164,8 +171,27 @@ function throwIfInvalidModel(useReverseProxy) { throw new Error('Google AI Studio API key is not set.'); } - if (multimodalApi === 'vertexai' && !secret_state[SECRET_KEYS.VERTEXAI] && !useReverseProxy) { - throw new Error('Google Vertex AI API key is not set.'); + if (multimodalApi === 'vertexai' && !useReverseProxy) { + // Check based on authentication mode + const authMode = oai_settings.vertexai_auth_mode || 'express'; + + if (authMode === 'express') { + // Express mode requires API key + if (!secret_state[SECRET_KEYS.VERTEXAI]) { + throw new Error('Google Vertex AI API key is not set for Express mode.'); + } + } else if (authMode === 'full') { + // Full mode requires Service Account JSON and project settings + if (!secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) { + throw new Error('Service Account JSON is required for Vertex AI Full mode. Please validate and save your Service Account JSON.'); + } + if (!oai_settings.vertexai_project_id) { + throw new Error('Project ID is required for Vertex AI Full mode.'); + } + if (!oai_settings.vertexai_region) { + throw new Error('Region is required for Vertex AI Full mode.'); + } + } } if (multimodalApi === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] && !useReverseProxy) { diff --git a/public/scripts/openai.js b/public/scripts/openai.js index ce05244a7..56ae2ad9b 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -306,6 +306,10 @@ export const settingsToUpdate = { assistant_impersonation: ['#claude_assistant_impersonation', 'assistant_impersonation', false, false], claude_use_sysprompt: ['#claude_use_sysprompt', 'claude_use_sysprompt', true, false], use_makersuite_sysprompt: ['#use_makersuite_sysprompt', 'use_makersuite_sysprompt', true, false], + vertexai_auth_mode: ['#vertexai_auth_mode', 'vertexai_auth_mode', false, true], + vertexai_project_id: ['#vertexai_project_id', 'vertexai_project_id', false, true], + vertexai_region: ['#vertexai_region', 'vertexai_region', false, true], + vertexai_service_account_json: ['#vertexai_service_account_json', 'vertexai_service_account_json', false, true], use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true, true], squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true, false], image_inlining: ['#openai_image_inlining', 'image_inlining', true, false], @@ -387,6 +391,10 @@ const default_settings = { assistant_impersonation: '', claude_use_sysprompt: false, use_makersuite_sysprompt: true, + vertexai_auth_mode: 'express', + vertexai_project_id: '', + vertexai_region: 'us-central1', + vertexai_service_account_json: '', use_alt_scale: false, squash_system_messages: false, image_inlining: false, @@ -471,6 +479,10 @@ const oai_settings = { assistant_impersonation: '', claude_use_sysprompt: false, use_makersuite_sysprompt: true, + vertexai_auth_mode: 'express', + vertexai_project_id: '', + vertexai_region: 'us-central1', + vertexai_service_account_json: '', use_alt_scale: false, squash_system_messages: false, image_inlining: false, @@ -2188,6 +2200,11 @@ async function sendOpenAIRequest(type, messages, signal) { generate_data['top_k'] = Number(oai_settings.top_k_openai); generate_data['stop'] = getCustomStoppingStrings(stopStringsLimit).slice(0, stopStringsLimit).filter(x => x.length >= 1 && x.length <= 16); generate_data['use_makersuite_sysprompt'] = oai_settings.use_makersuite_sysprompt; + if (isVertexAI) { + generate_data['vertexai_auth_mode'] = oai_settings.vertexai_auth_mode; + generate_data['vertexai_project_id'] = oai_settings.vertexai_project_id; + generate_data['vertexai_region'] = oai_settings.vertexai_region; + } } if (isMistral) { @@ -3427,6 +3444,9 @@ function loadOpenAISettings(data, settings) { if (settings.openai_model !== undefined) oai_settings.openai_model = settings.openai_model; if (settings.claude_use_sysprompt !== undefined) oai_settings.claude_use_sysprompt = !!settings.claude_use_sysprompt; if (settings.use_makersuite_sysprompt !== undefined) oai_settings.use_makersuite_sysprompt = !!settings.use_makersuite_sysprompt; + if (settings.vertexai_auth_mode !== undefined) oai_settings.vertexai_auth_mode = settings.vertexai_auth_mode; + if (settings.vertexai_project_id !== undefined) oai_settings.vertexai_project_id = settings.vertexai_project_id; + if (settings.vertexai_region !== undefined) oai_settings.vertexai_region = settings.vertexai_region; if (settings.use_alt_scale !== undefined) { oai_settings.use_alt_scale = !!settings.use_alt_scale; updateScaleForm(); } $('#stream_toggle').prop('checked', oai_settings.stream_openai); $('#api_url_scale').val(oai_settings.api_url_scale); @@ -3482,6 +3502,12 @@ function loadOpenAISettings(data, settings) { $('#openai_external_category').toggle(oai_settings.show_external_models); $('#claude_use_sysprompt').prop('checked', oai_settings.claude_use_sysprompt); $('#use_makersuite_sysprompt').prop('checked', oai_settings.use_makersuite_sysprompt); + $('#vertexai_auth_mode').val(oai_settings.vertexai_auth_mode); + $('#vertexai_project_id').val(oai_settings.vertexai_project_id); + $('#vertexai_region').val(oai_settings.vertexai_region); + // Don't display Service Account JSON in textarea - it's stored in backend secrets + $('#vertexai_service_account_json').val(''); + updateVertexAIServiceAccountStatus(); $('#scale-alt').prop('checked', oai_settings.use_alt_scale); $('#openrouter_use_fallback').prop('checked', oai_settings.openrouter_use_fallback); $('#openrouter_group_models').prop('checked', oai_settings.openrouter_group_models); @@ -3804,6 +3830,10 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { assistant_impersonation: settings.assistant_impersonation, claude_use_sysprompt: settings.claude_use_sysprompt, use_makersuite_sysprompt: settings.use_makersuite_sysprompt, + vertexai_auth_mode: settings.vertexai_auth_mode, + vertexai_project_id: settings.vertexai_project_id, + vertexai_region: settings.vertexai_region, + vertexai_service_account_json: settings.vertexai_service_account_json, use_alt_scale: settings.use_alt_scale, squash_system_messages: settings.squash_system_messages, image_inlining: settings.image_inlining, @@ -4979,15 +5009,30 @@ async function onConnectButtonClick(e) { } if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) { - const api_key_vertexai = String($('#api_key_vertexai').val()).trim(); + if (oai_settings.vertexai_auth_mode === 'express') { + // Express mode - use API key + const api_key_vertexai = String($('#api_key_vertexai').val()).trim(); - if (api_key_vertexai.length) { - await writeSecret(SECRET_KEYS.VERTEXAI, api_key_vertexai); - } + if (api_key_vertexai.length) { + await writeSecret(SECRET_KEYS.VERTEXAI, api_key_vertexai); + } - if (!secret_state[SECRET_KEYS.VERTEXAI] && !oai_settings.reverse_proxy) { - console.log('No secret key saved for Vertex AI'); - return; + if (!secret_state[SECRET_KEYS.VERTEXAI] && !oai_settings.reverse_proxy) { + console.log('No secret key saved for Vertex AI Express mode'); + return; + } + } else { + // Full version - use service account + if (!oai_settings.vertexai_project_id.trim()) { + toastr.error('Project ID is required for Vertex AI full version'); + return; + } + + // Check if service account JSON is saved in backend + if (!secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) { + toastr.error('Service Account JSON is required for Vertex AI full version. Please validate and save your Service Account JSON.'); + return; + } } } @@ -5170,6 +5215,8 @@ function toggleChatCompletionForms() { } else if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) { $('#model_vertexai_select').trigger('change'); + // Update UI based on authentication mode + onVertexAIAuthModeChange.call($('#vertexai_auth_mode')[0]); } else if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) { $('#model_openrouter_select').trigger('change'); @@ -5480,6 +5527,160 @@ function runProxyCallback(_, value) { return foundName; } +/** + * Handle Vertex AI authentication mode change + */ +function onVertexAIAuthModeChange() { + const authMode = String($(this).val()); + oai_settings.vertexai_auth_mode = authMode; + + // Show/hide appropriate configuration sections + if (authMode === 'express') { + $('#vertexai_express_config').show(); + $('#vertexai_full_config').hide(); + $('#vertexai_express_models').show(); + // Hide all full version model groups + $('#vertexai_full_gemini_25').hide(); + $('#vertexai_full_gemini_20').hide(); + $('#vertexai_full_gemini_15').hide(); + $('#vertexai_full_gemma').hide(); + $('#vertexai_full_learnlm').hide(); + } else { + $('#vertexai_express_config').hide(); + $('#vertexai_full_config').show(); + $('#vertexai_express_models').hide(); + // Show all full version model groups + $('#vertexai_full_gemini_25').show(); + $('#vertexai_full_gemini_20').show(); + $('#vertexai_full_gemini_15').show(); + $('#vertexai_full_gemma').show(); + $('#vertexai_full_learnlm').show(); + } + + saveSettingsDebounced(); +} + +/** + * Validate Vertex AI service account JSON + */ +async function onVertexAIValidateServiceAccount() { + const jsonContent = String($('#vertexai_service_account_json').val()).trim(); + + if (!jsonContent) { + toastr.error('Please enter Service Account JSON content'); + return; + } + + try { + const serviceAccount = JSON.parse(jsonContent); + const requiredFields = ['type', 'project_id', 'private_key', 'client_email', 'client_id']; + const missingFields = requiredFields.filter(field => !serviceAccount[field]); + + if (missingFields.length > 0) { + toastr.error(`Missing required fields: ${missingFields.join(', ')}`); + updateVertexAIServiceAccountStatus(false, `Missing fields: ${missingFields.join(', ')}`); + return; + } + + if (serviceAccount.type !== 'service_account') { + toastr.error('Invalid service account type. Expected "service_account"'); + updateVertexAIServiceAccountStatus(false, 'Invalid service account type'); + return; + } + + // Save to backend secret storage + await writeSecret(SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT, jsonContent); + + // Clear the textarea and update settings + $('#vertexai_service_account_json').val(''); + oai_settings.vertexai_service_account_json = ''; + + // Show success status + updateVertexAIServiceAccountStatus(true, `Project: ${serviceAccount.project_id}, Email: ${serviceAccount.client_email}`); + + toastr.success('Service Account JSON is valid and saved securely'); + saveSettingsDebounced(); + } catch (error) { + console.error('JSON validation error:', error); + toastr.error('Invalid JSON format'); + updateVertexAIServiceAccountStatus(false, 'Invalid JSON format'); + } +} + +/** + * Clear Vertex AI service account JSON + */ +async function onVertexAIClearServiceAccount() { + $('#vertexai_service_account_json').val(''); + oai_settings.vertexai_service_account_json = ''; + + // Clear from backend secret storage + await writeSecret(SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT, ''); + + updateVertexAIServiceAccountStatus(false); + toastr.info('Service Account JSON cleared'); + saveSettingsDebounced(); +} + +/** + * Handle Vertex AI service account JSON input change + */ +function onVertexAIServiceAccountJsonChange() { + const jsonContent = String($(this).val()).trim(); + // Don't save to settings automatically - only save when validated + // oai_settings.vertexai_service_account_json = jsonContent; + + if (jsonContent) { + // Auto-validate when content is pasted + try { + const serviceAccount = JSON.parse(jsonContent); + const requiredFields = ['type', 'project_id', 'private_key', 'client_email']; + const hasAllFields = requiredFields.every(field => serviceAccount[field]); + + if (hasAllFields && serviceAccount.type === 'service_account') { + updateVertexAIServiceAccountStatus(false, 'JSON appears valid - click "Validate JSON" to save'); + } else { + updateVertexAIServiceAccountStatus(false, 'Incomplete or invalid JSON'); + } + } catch (error) { + updateVertexAIServiceAccountStatus(false, 'Invalid JSON format'); + } + } else { + updateVertexAIServiceAccountStatus(false); + } + + // Don't save settings automatically + // saveSettingsDebounced(); +} + +/** + * Update the Vertex AI service account status display + * @param {boolean} isValid - Whether the service account is valid + * @param {string} message - Status message to display + */ +function updateVertexAIServiceAccountStatus(isValid = false, message = '') { + const statusDiv = $('#vertexai_service_account_status'); + const infoSpan = $('#vertexai_service_account_info'); + + // If no explicit message provided, check if we have a saved service account + if (!message && secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) { + isValid = true; + message = 'Service Account JSON is saved and ready to use'; + } + + if (isValid && message) { + infoSpan.html(` ${message}`); + statusDiv.show(); + } else if (!isValid && message) { + infoSpan.html(` ${message}`); + statusDiv.show(); + } else { + statusDiv.hide(); + } +} + + + export function initOpenAI() { SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'proxy', @@ -5943,6 +6144,18 @@ export function initOpenAI() { $('#model_scale_select').on('change', onModelChange); $('#model_google_select').on('change', onModelChange); $('#model_vertexai_select').on('change', onModelChange); + $('#vertexai_auth_mode').on('change', onVertexAIAuthModeChange); + $('#vertexai_project_id').on('input', function () { + oai_settings.vertexai_project_id = String($(this).val()); + saveSettingsDebounced(); + }); + $('#vertexai_region').on('input', function () { + oai_settings.vertexai_region = String($(this).val()); + saveSettingsDebounced(); + }); + $('#vertexai_service_account_json').on('input', onVertexAIServiceAccountJsonChange); + $('#vertexai_validate_service_account').on('click', onVertexAIValidateServiceAccount); + $('#vertexai_clear_service_account').on('click', onVertexAIClearServiceAccount); $('#model_openrouter_select').on('change', onModelChange); $('#openrouter_group_models').on('change', onOpenrouterModelSortChange); $('#openrouter_sort_models').on('change', onOpenrouterModelSortChange); diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js index b0cc3d018..488acb22e 100644 --- a/public/scripts/secrets.js +++ b/public/scripts/secrets.js @@ -44,6 +44,7 @@ export const SECRET_KEYS = { SERPER: 'api_key_serper', FALAI: 'api_key_falai', XAI: 'api_key_xai', + VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json', }; const INPUT_MAP = { diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index 5e0205771..38d420c6b 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -1,5 +1,6 @@ import process from 'node:process'; import util from 'node:util'; +import crypto from 'node:crypto'; import express from 'express'; import fetch from 'node-fetch'; @@ -60,6 +61,124 @@ const API_DEEPSEEK = 'https://api.deepseek.com/beta'; const API_XAI = 'https://api.x.ai/v1'; const API_POLLINATIONS = 'https://text.pollinations.ai/openai'; +/** + * Generates a JWT token for Google Cloud authentication using service account credentials. + * @param {object} serviceAccount Service account JSON object + * @returns {Promise} JWT token + */ +async function generateJWTToken(serviceAccount) { + const now = Math.floor(Date.now() / 1000); + const expiry = now + 3600; // 1 hour + + const header = { + alg: 'RS256', + typ: 'JWT' + }; + + const payload = { + iss: serviceAccount.client_email, + scope: 'https://www.googleapis.com/auth/cloud-platform', + aud: 'https://oauth2.googleapis.com/token', + iat: now, + exp: expiry + }; + + const headerBase64 = Buffer.from(JSON.stringify(header)).toString('base64url'); + const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString('base64url'); + const signatureInput = `${headerBase64}.${payloadBase64}`; + + // Create signature using private key + const sign = crypto.createSign('RSA-SHA256'); + sign.update(signatureInput); + const signature = sign.sign(serviceAccount.private_key, 'base64url'); + + return `${signatureInput}.${signature}`; +} + +/** + * Gets an access token from Google OAuth2 using JWT assertion. + * @param {string} jwtToken JWT token + * @returns {Promise} Access token + */ +async function getAccessToken(jwtToken) { + const response = await fetch('https://oauth2.googleapis.com/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion: jwtToken + }) + }); + + if (!response.ok) { + throw new Error(`Failed to get access token: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + return data.access_token; +} + +/** + * Gets authentication for Vertex AI - either API key or service account token. + * @param {object} request Express request + * @returns {Promise<{authHeader: string, authType: string}>} Authentication header and type + */ +async function getVertexAIAuth(request) { + // Get the authentication mode from frontend + const authMode = request.body.vertexai_auth_mode || 'express'; + + // Check if using reverse proxy + if (request.body.reverse_proxy) { + return { + authHeader: `Bearer ${request.body.proxy_password}`, + authType: 'proxy' + }; + } + + if (authMode === 'express') { + // Express mode: use API key + const apiKey = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI); + if (apiKey) { + return { + authHeader: `Bearer ${apiKey}`, + authType: 'express' + }; + } + throw new Error('API key is required for Vertex AI Express mode'); + } else if (authMode === 'full') { + // Full mode: use service account JSON + // First try to read from backend secret storage + let serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT); + + // If not found in secrets, fall back to request body (for backward compatibility) + if (!serviceAccountJson) { + serviceAccountJson = request.body.vertexai_service_account_json; + } + + if (serviceAccountJson) { + try { + const serviceAccount = JSON.parse(serviceAccountJson); + + const jwtToken = await generateJWTToken(serviceAccount); + const accessToken = await getAccessToken(jwtToken); + + return { + authHeader: `Bearer ${accessToken}`, + authType: 'full' + }; + } catch (error) { + console.error('Failed to authenticate with service account:', error); + throw new Error(`Service account authentication failed: ${error.message}`); + } + } + throw new Error('Service Account JSON is required for Vertex AI Full mode'); + } + + throw new Error(`Unsupported Vertex AI authentication mode: ${authMode}`); +} + /** * Applies a post-processing step to the generated messages. * @param {object[]} messages Messages to post-process @@ -348,13 +467,20 @@ async function sendMakerSuiteRequest(request, response) { let apiUrl; let apiKey; + let authHeader; + let authType; + if (useVertexAi) { apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI); - apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.VERTEXAI); - if (!request.body.reverse_proxy && !apiKey) { - console.warn(`${apiName} API key is missing.`); - return response.status(400).send({ error: true }); + try { + const auth = await getVertexAIAuth(request); + authHeader = auth.authHeader; + authType = auth.authType; + console.debug(`Using Vertex AI authentication type: ${authType}`); + } catch (error) { + console.warn(`${apiName} authentication failed: ${error.message}`); + return response.status(400).send({ error: true, message: error.message }); } } else { apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE); @@ -364,6 +490,9 @@ async function sendMakerSuiteRequest(request, response) { console.warn(`${apiName} API key is missing.`); return response.status(400).send({ error: true }); } + + authHeader = `Bearer ${apiKey}`; + authType = 'api_key'; } const model = String(request.body.model); @@ -499,17 +628,39 @@ async function sendMakerSuiteRequest(request, response) { const responseType = (stream ? 'streamGenerateContent' : 'generateContent'); let url; + let headers = { + 'Content-Type': 'application/json', + }; + if (useVertexAi) { - url = `${apiUrl.toString().replace(/\/$/, '')}/v1/publishers/google/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`; + if (authType === 'express') { + // For Express mode (API key authentication), use the key parameter + const keyParam = authHeader.replace('Bearer ', ''); + url = `${apiUrl.toString().replace(/\/$/, '')}/v1/publishers/google/models/${model}:${responseType}?key=${keyParam}${stream ? '&alt=sse' : ''}`; + } else if (authType === 'full') { + // For Full mode (service account authentication), use project-specific URL + const projectId = request.body.vertexai_project_id || 'your-project-id'; + const region = request.body.vertexai_region || 'us-central1'; + // Handle global region differently - no region prefix in hostname + if (region === 'global') { + url = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${responseType}${stream ? '?alt=sse' : ''}`; + } else { + url = `https://${region}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${responseType}${stream ? '?alt=sse' : ''}`; + } + headers['Authorization'] = authHeader; + } else { + // For proxy mode, use the original URL with Authorization header + url = `${apiUrl.toString().replace(/\/$/, '')}/v1/publishers/google/models/${model}:${responseType}${stream ? '?alt=sse' : ''}`; + headers['Authorization'] = authHeader; + } } else { url = `${apiUrl.toString().replace(/\/$/, '')}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`; } + const generateResponse = await fetch(url, { body: JSON.stringify(body), method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: headers, signal: controller.signal, }); diff --git a/src/endpoints/google.js b/src/endpoints/google.js index 435a3d388..950abed44 100644 --- a/src/endpoints/google.js +++ b/src/endpoints/google.js @@ -2,6 +2,7 @@ import { Buffer } from 'node:buffer'; import fetch from 'node-fetch'; import express from 'express'; import { speak, languages } from 'google-translate-api-x'; +import crypto from 'node:crypto'; import { readSecret, SECRET_KEYS } from './secrets.js'; import { GEMINI_SAFETY } from '../constants.js'; @@ -9,6 +10,108 @@ import { GEMINI_SAFETY } from '../constants.js'; const API_MAKERSUITE = 'https://generativelanguage.googleapis.com'; const API_VERTEX_AI = 'https://us-central1-aiplatform.googleapis.com'; +// Vertex AI authentication helper functions +async function getVertexAIAuth(request) { + const authMode = request.body.vertexai_auth_mode || 'express'; + + if (request.body.reverse_proxy) { + return { + authHeader: `Bearer ${request.body.proxy_password}`, + authType: 'proxy' + }; + } + + if (authMode === 'express') { + const apiKey = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI); + if (apiKey) { + return { + authHeader: `Bearer ${apiKey}`, + authType: 'express' + }; + } + throw new Error('API key is required for Vertex AI Express mode'); + } else if (authMode === 'full') { + // Try to get service account JSON from backend storage first + let serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT); + + // If not found in backend storage, try from request body (for backward compatibility) + if (!serviceAccountJson) { + serviceAccountJson = request.body.vertexai_service_account_json; + } + + if (serviceAccountJson) { + try { + const serviceAccount = JSON.parse(serviceAccountJson); + const jwtToken = await generateJWTToken(serviceAccount); + const accessToken = await getAccessToken(jwtToken); + return { + authHeader: `Bearer ${accessToken}`, + authType: 'full' + }; + } catch (error) { + console.error('Failed to authenticate with service account:', error); + throw new Error(`Service account authentication failed: ${error.message}`); + } + } + throw new Error('Service Account JSON is required for Vertex AI Full mode'); + } + + throw new Error(`Unsupported Vertex AI authentication mode: ${authMode}`); +} + +/** + * Generates a JWT token for Google Cloud authentication using service account credentials. + * @param {object} serviceAccount Service account JSON object + * @returns {Promise} JWT token + */ +async function generateJWTToken(serviceAccount) { + const now = Math.floor(Date.now() / 1000); + const expiry = now + 3600; // 1 hour + + const header = { + alg: 'RS256', + typ: 'JWT' + }; + + const payload = { + iss: serviceAccount.client_email, + scope: 'https://www.googleapis.com/auth/cloud-platform', + aud: 'https://oauth2.googleapis.com/token', + iat: now, + exp: expiry + }; + + const headerBase64 = Buffer.from(JSON.stringify(header)).toString('base64url'); + const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString('base64url'); + const signatureInput = `${headerBase64}.${payloadBase64}`; + + // Create signature using private key + const sign = crypto.createSign('RSA-SHA256'); + sign.update(signatureInput); + const signature = sign.sign(serviceAccount.private_key, 'base64url'); + + return `${signatureInput}.${signature}`; +} + +async function getAccessToken(jwtToken) { + const response = await fetch('https://oauth2.googleapis.com/token', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', + assertion: jwtToken + }) + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to get access token: ${error}`); + } + + const data = await response.json(); + return data.access_token; +} + export const router = express.Router(); router.post('/caption-image', async (request, response) => { @@ -17,20 +120,43 @@ router.post('/caption-image', async (request, response) => { const base64Data = request.body.image.split(',')[1]; const useVertexAi = request.body.api === 'vertexai'; const apiName = useVertexAi ? 'Google Vertex AI' : 'Google AI Studio'; - let apiKey; - let apiUrl; - if (useVertexAi) { - apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.VERTEXAI); - apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI); - } else { - apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE); - apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE); - } const model = request.body.model || 'gemini-2.0-flash'; + let url; + let headers = { + 'Content-Type': 'application/json', + }; + if (useVertexAi) { - url = `${apiUrl.origin}/v1/publishers/google/models/${model}:generateContent?key=${apiKey}`; + // Get authentication for Vertex AI + const { authHeader, authType } = await getVertexAIAuth(request); + + if (authType === 'express') { + // Express mode: use API key parameter + const keyParam = authHeader.replace('Bearer ', ''); + const apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI); + url = `${apiUrl.origin}/v1/publishers/google/models/${model}:generateContent?key=${keyParam}`; + } else if (authType === 'full') { + // Full mode: use project-specific URL with Authorization header + const projectId = request.body.vertexai_project_id || 'your-project-id'; + const region = request.body.vertexai_region || 'us-central1'; + // Handle global region differently - no region prefix in hostname + if (region === 'global') { + url = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:generateContent`; + } else { + url = `https://${region}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:generateContent`; + } + headers['Authorization'] = authHeader; + } else { + // Proxy mode: use Authorization header + const apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI); + url = `${apiUrl.origin}/v1/publishers/google/models/${model}:generateContent`; + headers['Authorization'] = authHeader; + } } else { + // Google AI Studio + const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE); + const apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE); url = `${apiUrl.origin}/v1beta/models/${model}:generateContent?key=${apiKey}`; } const body = { @@ -53,9 +179,7 @@ router.post('/caption-image', async (request, response) => { const result = await fetch(url, { body: JSON.stringify(body), method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, + headers: headers, }); if (!result.ok) { diff --git a/src/endpoints/secrets.js b/src/endpoints/secrets.js index 0eed2eb7f..d1f015b42 100644 --- a/src/endpoints/secrets.js +++ b/src/endpoints/secrets.js @@ -54,6 +54,7 @@ export const SECRET_KEYS = { DEEPSEEK: 'api_key_deepseek', SERPER: 'api_key_serper', XAI: 'api_key_xai', + VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json', }; // These are the keys that are safe to expose, even if allowKeysExposure is false From 7fcd40c829b7499ca91b8d3fd83d5b83ff838fd1 Mon Sep 17 00:00:00 2001 From: InterestingDarkness Date: Mon, 26 May 2025 22:16:36 +0800 Subject: [PATCH 02/18] fix ESLint --- src/endpoints/backends/chat-completions.js | 16 ++++++++-------- src/endpoints/google.js | 14 +++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index 38d420c6b..7c340ace8 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -72,7 +72,7 @@ async function generateJWTToken(serviceAccount) { const header = { alg: 'RS256', - typ: 'JWT' + typ: 'JWT', }; const payload = { @@ -80,7 +80,7 @@ async function generateJWTToken(serviceAccount) { scope: 'https://www.googleapis.com/auth/cloud-platform', aud: 'https://oauth2.googleapis.com/token', iat: now, - exp: expiry + exp: expiry, }; const headerBase64 = Buffer.from(JSON.stringify(header)).toString('base64url'); @@ -104,12 +104,12 @@ async function getAccessToken(jwtToken) { const response = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { - 'Content-Type': 'application/x-www-form-urlencoded' + 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', - assertion: jwtToken - }) + assertion: jwtToken, + }), }); if (!response.ok) { @@ -133,7 +133,7 @@ async function getVertexAIAuth(request) { if (request.body.reverse_proxy) { return { authHeader: `Bearer ${request.body.proxy_password}`, - authType: 'proxy' + authType: 'proxy', }; } @@ -143,7 +143,7 @@ async function getVertexAIAuth(request) { if (apiKey) { return { authHeader: `Bearer ${apiKey}`, - authType: 'express' + authType: 'express', }; } throw new Error('API key is required for Vertex AI Express mode'); @@ -166,7 +166,7 @@ async function getVertexAIAuth(request) { return { authHeader: `Bearer ${accessToken}`, - authType: 'full' + authType: 'full', }; } catch (error) { console.error('Failed to authenticate with service account:', error); diff --git a/src/endpoints/google.js b/src/endpoints/google.js index 950abed44..a42e15af7 100644 --- a/src/endpoints/google.js +++ b/src/endpoints/google.js @@ -17,7 +17,7 @@ async function getVertexAIAuth(request) { if (request.body.reverse_proxy) { return { authHeader: `Bearer ${request.body.proxy_password}`, - authType: 'proxy' + authType: 'proxy', }; } @@ -26,7 +26,7 @@ async function getVertexAIAuth(request) { if (apiKey) { return { authHeader: `Bearer ${apiKey}`, - authType: 'express' + authType: 'express', }; } throw new Error('API key is required for Vertex AI Express mode'); @@ -46,7 +46,7 @@ async function getVertexAIAuth(request) { const accessToken = await getAccessToken(jwtToken); return { authHeader: `Bearer ${accessToken}`, - authType: 'full' + authType: 'full', }; } catch (error) { console.error('Failed to authenticate with service account:', error); @@ -70,7 +70,7 @@ async function generateJWTToken(serviceAccount) { const header = { alg: 'RS256', - typ: 'JWT' + typ: 'JWT', }; const payload = { @@ -78,7 +78,7 @@ async function generateJWTToken(serviceAccount) { scope: 'https://www.googleapis.com/auth/cloud-platform', aud: 'https://oauth2.googleapis.com/token', iat: now, - exp: expiry + exp: expiry, }; const headerBase64 = Buffer.from(JSON.stringify(header)).toString('base64url'); @@ -99,8 +99,8 @@ async function getAccessToken(jwtToken) { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', - assertion: jwtToken - }) + assertion: jwtToken, + }), }); if (!response.ok) { From 1c048a6c799a37090b2a3e6bb024f8ebc1b25d68 Mon Sep 17 00:00:00 2001 From: InterestingDarkness Date: Tue, 27 May 2025 00:31:43 +0800 Subject: [PATCH 03/18] Refactor Google authentication functions for Vertex AI - Exported `getVertexAIAuth`, `generateJWTToken`, and `getAccessToken` functions from google.js for better modularity. - Removed redundant JWT token generation and access token retrieval logic from chat-completions.js, utilizing the newly exported functions instead. - Improved code organization and maintainability by centralizing authentication logic. --- src/endpoints/backends/chat-completions.js | 114 +-------------------- src/endpoints/google.js | 6 +- 2 files changed, 4 insertions(+), 116 deletions(-) diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index 7c340ace8..98cdd8f00 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -1,6 +1,5 @@ import process from 'node:process'; import util from 'node:util'; -import crypto from 'node:crypto'; import express from 'express'; import fetch from 'node-fetch'; @@ -44,6 +43,7 @@ import { webTokenizers, getWebTokenizer, } from '../tokenizers.js'; +import { getVertexAIAuth } from '../google.js'; const API_OPENAI = 'https://api.openai.com/v1'; const API_CLAUDE = 'https://api.anthropic.com/v1'; @@ -61,123 +61,11 @@ const API_DEEPSEEK = 'https://api.deepseek.com/beta'; const API_XAI = 'https://api.x.ai/v1'; const API_POLLINATIONS = 'https://text.pollinations.ai/openai'; -/** - * Generates a JWT token for Google Cloud authentication using service account credentials. - * @param {object} serviceAccount Service account JSON object - * @returns {Promise} JWT token - */ -async function generateJWTToken(serviceAccount) { - const now = Math.floor(Date.now() / 1000); - const expiry = now + 3600; // 1 hour - const header = { - alg: 'RS256', - typ: 'JWT', - }; - const payload = { - iss: serviceAccount.client_email, - scope: 'https://www.googleapis.com/auth/cloud-platform', - aud: 'https://oauth2.googleapis.com/token', - iat: now, - exp: expiry, - }; - const headerBase64 = Buffer.from(JSON.stringify(header)).toString('base64url'); - const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString('base64url'); - const signatureInput = `${headerBase64}.${payloadBase64}`; - // Create signature using private key - const sign = crypto.createSign('RSA-SHA256'); - sign.update(signatureInput); - const signature = sign.sign(serviceAccount.private_key, 'base64url'); - return `${signatureInput}.${signature}`; -} - -/** - * Gets an access token from Google OAuth2 using JWT assertion. - * @param {string} jwtToken JWT token - * @returns {Promise} Access token - */ -async function getAccessToken(jwtToken) { - const response = await fetch('https://oauth2.googleapis.com/token', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: new URLSearchParams({ - grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', - assertion: jwtToken, - }), - }); - - if (!response.ok) { - throw new Error(`Failed to get access token: ${response.status} ${response.statusText}`); - } - - const data = await response.json(); - return data.access_token; -} - -/** - * Gets authentication for Vertex AI - either API key or service account token. - * @param {object} request Express request - * @returns {Promise<{authHeader: string, authType: string}>} Authentication header and type - */ -async function getVertexAIAuth(request) { - // Get the authentication mode from frontend - const authMode = request.body.vertexai_auth_mode || 'express'; - - // Check if using reverse proxy - if (request.body.reverse_proxy) { - return { - authHeader: `Bearer ${request.body.proxy_password}`, - authType: 'proxy', - }; - } - - if (authMode === 'express') { - // Express mode: use API key - const apiKey = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI); - if (apiKey) { - return { - authHeader: `Bearer ${apiKey}`, - authType: 'express', - }; - } - throw new Error('API key is required for Vertex AI Express mode'); - } else if (authMode === 'full') { - // Full mode: use service account JSON - // First try to read from backend secret storage - let serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT); - - // If not found in secrets, fall back to request body (for backward compatibility) - if (!serviceAccountJson) { - serviceAccountJson = request.body.vertexai_service_account_json; - } - - if (serviceAccountJson) { - try { - const serviceAccount = JSON.parse(serviceAccountJson); - - const jwtToken = await generateJWTToken(serviceAccount); - const accessToken = await getAccessToken(jwtToken); - - return { - authHeader: `Bearer ${accessToken}`, - authType: 'full', - }; - } catch (error) { - console.error('Failed to authenticate with service account:', error); - throw new Error(`Service account authentication failed: ${error.message}`); - } - } - throw new Error('Service Account JSON is required for Vertex AI Full mode'); - } - - throw new Error(`Unsupported Vertex AI authentication mode: ${authMode}`); -} /** * Applies a post-processing step to the generated messages. diff --git a/src/endpoints/google.js b/src/endpoints/google.js index a42e15af7..eb32faa61 100644 --- a/src/endpoints/google.js +++ b/src/endpoints/google.js @@ -11,7 +11,7 @@ const API_MAKERSUITE = 'https://generativelanguage.googleapis.com'; const API_VERTEX_AI = 'https://us-central1-aiplatform.googleapis.com'; // Vertex AI authentication helper functions -async function getVertexAIAuth(request) { +export async function getVertexAIAuth(request) { const authMode = request.body.vertexai_auth_mode || 'express'; if (request.body.reverse_proxy) { @@ -64,7 +64,7 @@ async function getVertexAIAuth(request) { * @param {object} serviceAccount Service account JSON object * @returns {Promise} JWT token */ -async function generateJWTToken(serviceAccount) { +export async function generateJWTToken(serviceAccount) { const now = Math.floor(Date.now() / 1000); const expiry = now + 3600; // 1 hour @@ -93,7 +93,7 @@ async function generateJWTToken(serviceAccount) { return `${signatureInput}.${signature}`; } -async function getAccessToken(jwtToken) { +export async function getAccessToken(jwtToken) { const response = await fetch('https://oauth2.googleapis.com/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, From 9e71b70f75f8cbebde7d713ad17acf59b1db52fc Mon Sep 17 00:00:00 2001 From: InterestingDarkness Date: Tue, 27 May 2025 00:38:06 +0800 Subject: [PATCH 04/18] Updated toastr messages in openai.js to use translation function for better internationalization support. --- public/scripts/openai.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 56ae2ad9b..f06eeb0f1 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -5567,7 +5567,7 @@ async function onVertexAIValidateServiceAccount() { const jsonContent = String($('#vertexai_service_account_json').val()).trim(); if (!jsonContent) { - toastr.error('Please enter Service Account JSON content'); + toastr.error(t`Please enter Service Account JSON content`); return; } @@ -5577,14 +5577,14 @@ async function onVertexAIValidateServiceAccount() { const missingFields = requiredFields.filter(field => !serviceAccount[field]); if (missingFields.length > 0) { - toastr.error(`Missing required fields: ${missingFields.join(', ')}`); - updateVertexAIServiceAccountStatus(false, `Missing fields: ${missingFields.join(', ')}`); + toastr.error(t`Missing required fields: ${missingFields.join(', ')}`); + updateVertexAIServiceAccountStatus(false, t`Missing fields: ${missingFields.join(', ')}`); return; } if (serviceAccount.type !== 'service_account') { - toastr.error('Invalid service account type. Expected "service_account"'); - updateVertexAIServiceAccountStatus(false, 'Invalid service account type'); + toastr.error(t`Invalid service account type. Expected "service_account"`); + updateVertexAIServiceAccountStatus(false, t`Invalid service account type`); return; } @@ -5598,12 +5598,12 @@ async function onVertexAIValidateServiceAccount() { // Show success status updateVertexAIServiceAccountStatus(true, `Project: ${serviceAccount.project_id}, Email: ${serviceAccount.client_email}`); - toastr.success('Service Account JSON is valid and saved securely'); + toastr.success(t`Service Account JSON is valid and saved securely`); saveSettingsDebounced(); } catch (error) { console.error('JSON validation error:', error); - toastr.error('Invalid JSON format'); - updateVertexAIServiceAccountStatus(false, 'Invalid JSON format'); + toastr.error(t`Invalid JSON format`); + updateVertexAIServiceAccountStatus(false, t`Invalid JSON format`); } } @@ -5618,7 +5618,7 @@ async function onVertexAIClearServiceAccount() { await writeSecret(SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT, ''); updateVertexAIServiceAccountStatus(false); - toastr.info('Service Account JSON cleared'); + toastr.info(t`Service Account JSON cleared`); saveSettingsDebounced(); } @@ -5638,12 +5638,12 @@ function onVertexAIServiceAccountJsonChange() { const hasAllFields = requiredFields.every(field => serviceAccount[field]); if (hasAllFields && serviceAccount.type === 'service_account') { - updateVertexAIServiceAccountStatus(false, 'JSON appears valid - click "Validate JSON" to save'); + updateVertexAIServiceAccountStatus(false, t`JSON appears valid - click "Validate JSON" to save`); } else { - updateVertexAIServiceAccountStatus(false, 'Incomplete or invalid JSON'); + updateVertexAIServiceAccountStatus(false, t`Incomplete or invalid JSON`); } } catch (error) { - updateVertexAIServiceAccountStatus(false, 'Invalid JSON format'); + updateVertexAIServiceAccountStatus(false, t`Invalid JSON format`); } } else { updateVertexAIServiceAccountStatus(false); @@ -5665,7 +5665,7 @@ function updateVertexAIServiceAccountStatus(isValid = false, message = '') { // If no explicit message provided, check if we have a saved service account if (!message && secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) { isValid = true; - message = 'Service Account JSON is saved and ready to use'; + message = t`Service Account JSON is saved and ready to use`; } if (isValid && message) { From 453c177a8ebeec77ef709b1a5bf1f2e5a8929ac3 Mon Sep 17 00:00:00 2001 From: InterestingDarkness Date: Tue, 27 May 2025 00:43:31 +0800 Subject: [PATCH 05/18] Enhance Vertex AI model selection by adding data-mode attributes - Updated index.html to include data-mode attributes for model optgroups, distinguishing between express and full modes. - Modified openai.js to show/hide model groups based on the selected authentication mode, improving user experience and functionality. --- public/index.html | 12 ++++++------ public/scripts/openai.js | 20 ++++++-------------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/public/index.html b/public/index.html index 0a4f008f0..3835913a8 100644 --- a/public/index.html +++ b/public/index.html @@ -3310,7 +3310,7 @@

Google Model

diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index ec7a6d6c7..e7b2979f8 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -408,6 +408,7 @@ async function sendMakerSuiteRequest(request, response) { const imageGenerationModels = [ 'gemini-2.0-flash-exp', 'gemini-2.0-flash-exp-image-generation', + 'gemini-2.0-flash-preview-image-generation', ]; // These models do not support setting the threshold to OFF at all. From 55af19e70b774df1012693314cd9e2ed68af2ac2 Mon Sep 17 00:00:00 2001 From: InterestingDarkness Date: Wed, 28 May 2025 21:26:10 +0800 Subject: [PATCH 10/18] Reduced the number of rows in the textarea for Service Account JSON input in index.html --- public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 74f88ef3e..013f8702d 100644 --- a/public/index.html +++ b/public/index.html @@ -3281,7 +3281,7 @@
- -
- Paste the complete JSON content of your Google Cloud Service Account key file. The content will be stored securely. +
+ For privacy reasons, your Service Account JSON content will be hidden after you reload the page.
diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js index 488acb22e..279a28351 100644 --- a/public/scripts/secrets.js +++ b/public/scripts/secrets.js @@ -81,8 +81,13 @@ const INPUT_MAP = { [SECRET_KEYS.GENERIC]: '#api_key_generic', [SECRET_KEYS.DEEPSEEK]: '#api_key_deepseek', [SECRET_KEYS.XAI]: '#api_key_xai', + [SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]: '#vertexai_service_account_json', }; +const STATIC_PLACEHOLDER_KEYS = [ + SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT, +]; + async function clearSecret() { const key = $(this).data('key'); await writeSecret(key, ''); @@ -94,6 +99,9 @@ async function clearSecret() { export function updateSecretDisplay() { for (const [secret_key, input_selector] of Object.entries(INPUT_MAP)) { + if (STATIC_PLACEHOLDER_KEYS.includes(secret_key)) { + continue; + } const validSecret = !!secret_state[secret_key]; const placeholder = $('#viewSecrets').attr(validSecret ? 'key_saved_text' : 'missing_key_text');