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.
This commit is contained in:
InterestingDarkness
2025-05-28 21:57:17 +08:00
parent 9f698dd6e3
commit 75e3f599e6
7 changed files with 48 additions and 35 deletions

View File

@@ -3266,12 +3266,6 @@
</a> </a>
</h4> </h4>
<!-- Project ID -->
<div class="flex-container">
<label for="vertexai_project_id" data-i18n="Project ID">Project ID:</label>
<input id="vertexai_project_id" name="vertexai_project_id" class="text_pole flex1" value="" type="text" autocomplete="off" placeholder="your-gcp-project-id">
</div>
<!-- Region --> <!-- Region -->
<div class="flex-container"> <div class="flex-container">
<label for="vertexai_region" data-i18n="Region">Region:</label> <label for="vertexai_region" data-i18n="Region">Region:</label>

View File

@@ -180,13 +180,10 @@ function throwIfInvalidModel(useReverseProxy) {
throw new Error('Google Vertex AI API key is not set for Express mode.'); throw new Error('Google Vertex AI API key is not set for Express mode.');
} }
} else if (authMode === 'full') { } 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]) { 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.'); 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) { if (!oai_settings.vertexai_region) {
throw new Error('Region is required for Vertex AI Full mode.'); throw new Error('Region is required for Vertex AI Full mode.');
} }

View File

@@ -3496,8 +3496,6 @@ function loadOpenAISettings(data, settings) {
$('#claude_use_sysprompt').prop('checked', oai_settings.claude_use_sysprompt); $('#claude_use_sysprompt').prop('checked', oai_settings.claude_use_sysprompt);
$('#use_makersuite_sysprompt').prop('checked', oai_settings.use_makersuite_sysprompt); $('#use_makersuite_sysprompt').prop('checked', oai_settings.use_makersuite_sysprompt);
$('#vertexai_auth_mode').val(oai_settings.vertexai_auth_mode); $('#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); $('#vertexai_region').val(oai_settings.vertexai_region);
// Don't display Service Account JSON in textarea - it's stored in backend secrets // Don't display Service Account JSON in textarea - it's stored in backend secrets
$('#vertexai_service_account_json').val(''); $('#vertexai_service_account_json').val('');
@@ -5015,19 +5013,7 @@ async function onConnectButtonClick(e) {
} }
} else { } else {
// Full version - use service account // Full version - use service account
// Check if we have a saved project ID, otherwise use the input value // Project ID will be extracted from the Service Account JSON
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);
}
// Check if service account JSON is saved in backend // Check if service account JSON is saved in backend
if (!secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) { if (!secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) {

View File

@@ -44,7 +44,6 @@ export const SECRET_KEYS = {
SERPER: 'api_key_serper', SERPER: 'api_key_serper',
FALAI: 'api_key_falai', FALAI: 'api_key_falai',
XAI: 'api_key_xai', XAI: 'api_key_xai',
VERTEXAI_PROJECT_ID: 'vertexai_project_id',
VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json', VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',
}; };

View File

@@ -43,7 +43,7 @@ import {
webTokenizers, webTokenizers,
getWebTokenizer, getWebTokenizer,
} from '../tokenizers.js'; } from '../tokenizers.js';
import { getVertexAIAuth } from '../google.js'; import { getVertexAIAuth, getProjectIdFromServiceAccount } from '../google.js';
const API_OPENAI = 'https://api.openai.com/v1'; const API_OPENAI = 'https://api.openai.com/v1';
const API_CLAUDE = 'https://api.anthropic.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' : ''}`; url = `${apiUrl.toString().replace(/\/$/, '')}/v1/publishers/google/models/${model}:${responseType}?key=${keyParam}${stream ? '&alt=sse' : ''}`;
} else if (authType === 'full') { } else if (authType === 'full') {
// For Full mode (service account authentication), use project-specific URL // For Full mode (service account authentication), use project-specific URL
// Only use project ID from secrets // Get project ID from Service Account JSON
const projectId = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_PROJECT_ID); const serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT);
if (!projectId) { if (!serviceAccountJson) {
console.warn('Vertex AI project ID is missing.'); 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 }); return response.status(400).send({ error: true });
} }
const region = request.body.vertexai_region || 'us-central1'; const region = request.body.vertexai_region || 'us-central1';

View File

@@ -107,6 +107,25 @@ export async function getAccessToken(jwtToken) {
return data.access_token; 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(); export const router = express.Router();
router.post('/caption-image', async (request, response) => { 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}`; url = `${apiUrl.origin}/v1/publishers/google/models/${model}:generateContent?key=${keyParam}`;
} else if (authType === 'full') { } else if (authType === 'full') {
// Full mode: use project-specific URL with Authorization header // Full mode: use project-specific URL with Authorization header
const projectId = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_PROJECT_ID); // Get project ID from Service Account JSON
if (!projectId) { const serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT);
console.warn('Vertex AI project ID is missing.'); 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 }); return response.status(400).send({ error: true });
} }
const region = request.body.vertexai_region || 'us-central1'; const region = request.body.vertexai_region || 'us-central1';

View File

@@ -54,7 +54,6 @@ export const SECRET_KEYS = {
DEEPSEEK: 'api_key_deepseek', DEEPSEEK: 'api_key_deepseek',
SERPER: 'api_key_serper', SERPER: 'api_key_serper',
XAI: 'api_key_xai', XAI: 'api_key_xai',
VERTEXAI_PROJECT_ID: 'vertexai_project_id',
VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json', VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',
}; };