From 75e3f599e6e808d7858176f382af2baabafa7b0c Mon Sep 17 00:00:00 2001 From: InterestingDarkness Date: Wed, 28 May 2025 21:57:17 +0800 Subject: [PATCH] Derive Vertex AI Project ID from Service Account JSON This commit refactors the Vertex AI integration to automatically derive the Project ID from the provided Service Account JSON. This simplifies the configuration process for users in "Full" (service account) authentication mode by removing the need to specify the Project ID separately. --- public/index.html | 6 ---- public/scripts/extensions/shared.js | 5 +--- public/scripts/openai.js | 16 +--------- public/scripts/secrets.js | 1 - src/endpoints/backends/chat-completions.js | 19 ++++++++---- src/endpoints/google.js | 35 ++++++++++++++++++++-- src/endpoints/secrets.js | 1 - 7 files changed, 48 insertions(+), 35 deletions(-) diff --git a/public/index.html b/public/index.html index 9e086407c..f4eed7b28 100644 --- a/public/index.html +++ b/public/index.html @@ -3266,12 +3266,6 @@ - -
- - -
-
diff --git a/public/scripts/extensions/shared.js b/public/scripts/extensions/shared.js index 6a73d1cf3..c68d6c575 100644 --- a/public/scripts/extensions/shared.js +++ b/public/scripts/extensions/shared.js @@ -180,13 +180,10 @@ function throwIfInvalidModel(useReverseProxy) { 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 + // Full mode requires Service Account JSON and region 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 (!secret_state[SECRET_KEYS.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.'); } diff --git a/public/scripts/openai.js b/public/scripts/openai.js index d3c3edab0..23fbdeadd 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -3496,8 +3496,6 @@ function loadOpenAISettings(data, settings) { $('#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); - // Don't display Project ID in input - it's stored in backend secrets - $('#vertexai_project_id').val(''); $('#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(''); @@ -5015,19 +5013,7 @@ async function onConnectButtonClick(e) { } } else { // Full version - use service account - // Check if we have a saved project ID, otherwise use the input value - const savedProjectId = secret_state[SECRET_KEYS.VERTEXAI_PROJECT_ID]; - const inputProjectId = String($('#vertexai_project_id').val()).trim(); - - if (!savedProjectId && !inputProjectId) { - toastr.error(t`Project ID is required for Vertex AI full version`); - return; - } - - // Save project ID to secrets if we have an input value - if (inputProjectId.length) { - await writeSecret(SECRET_KEYS.VERTEXAI_PROJECT_ID, inputProjectId); - } + // Project ID will be extracted from the Service Account JSON // Check if service account JSON is saved in backend if (!secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) { diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js index b72beb46b..488acb22e 100644 --- a/public/scripts/secrets.js +++ b/public/scripts/secrets.js @@ -44,7 +44,6 @@ export const SECRET_KEYS = { SERPER: 'api_key_serper', FALAI: 'api_key_falai', XAI: 'api_key_xai', - VERTEXAI_PROJECT_ID: 'vertexai_project_id', VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json', }; diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index e7b2979f8..767125af3 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -43,7 +43,7 @@ import { webTokenizers, getWebTokenizer, } from '../tokenizers.js'; -import { getVertexAIAuth } from '../google.js'; +import { getVertexAIAuth, getProjectIdFromServiceAccount } from '../google.js'; const API_OPENAI = 'https://api.openai.com/v1'; const API_CLAUDE = 'https://api.anthropic.com/v1'; @@ -528,10 +528,19 @@ async function sendMakerSuiteRequest(request, response) { 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 - // Only use project ID from secrets - const projectId = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_PROJECT_ID); - if (!projectId) { - console.warn('Vertex AI project ID is missing.'); + // Get project ID from Service Account JSON + const serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT); + if (!serviceAccountJson) { + console.warn('Vertex AI Service Account JSON is missing.'); + return response.status(400).send({ error: true }); + } + + let projectId; + try { + const serviceAccount = JSON.parse(serviceAccountJson); + projectId = getProjectIdFromServiceAccount(serviceAccount); + } catch (error) { + console.error('Failed to extract project ID from Service Account JSON:', error); return response.status(400).send({ error: true }); } const region = request.body.vertexai_region || 'us-central1'; diff --git a/src/endpoints/google.js b/src/endpoints/google.js index 9d06683c0..b0c24557f 100644 --- a/src/endpoints/google.js +++ b/src/endpoints/google.js @@ -107,6 +107,25 @@ export async function getAccessToken(jwtToken) { return data.access_token; } +/** + * Extracts the project ID from a Service Account JSON object. + * @param {object} serviceAccount Service account JSON object + * @returns {string} Project ID + * @throws {Error} If project ID is not found in the service account + */ +export function getProjectIdFromServiceAccount(serviceAccount) { + if (!serviceAccount || typeof serviceAccount !== 'object') { + throw new Error('Invalid service account object'); + } + + const projectId = serviceAccount.project_id; + if (!projectId || typeof projectId !== 'string') { + throw new Error('Project ID not found in service account JSON'); + } + + return projectId; +} + export const router = express.Router(); router.post('/caption-image', async (request, response) => { @@ -133,9 +152,19 @@ router.post('/caption-image', async (request, response) => { 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 = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_PROJECT_ID); - if (!projectId) { - console.warn('Vertex AI project ID is missing.'); + // Get project ID from Service Account JSON + const serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT); + if (!serviceAccountJson) { + console.warn('Vertex AI Service Account JSON is missing.'); + return response.status(400).send({ error: true }); + } + + let projectId; + try { + const serviceAccount = JSON.parse(serviceAccountJson); + projectId = getProjectIdFromServiceAccount(serviceAccount); + } catch (error) { + console.error('Failed to extract project ID from Service Account JSON:', error); return response.status(400).send({ error: true }); } const region = request.body.vertexai_region || 'us-central1'; diff --git a/src/endpoints/secrets.js b/src/endpoints/secrets.js index 237eda570..d1f015b42 100644 --- a/src/endpoints/secrets.js +++ b/src/endpoints/secrets.js @@ -54,7 +54,6 @@ export const SECRET_KEYS = { DEEPSEEK: 'api_key_deepseek', SERPER: 'api_key_serper', XAI: 'api_key_xai', - VERTEXAI_PROJECT_ID: 'vertexai_project_id', VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json', };