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.

75e3f599e6e808d7858176f382af2baabafa7b0c

InterestingDarkness <computeridiot999@gmail.com>

7 files changed, +48 -35Showing whitespace changes
public/index.html+0 -6
@@ -3266,12 +3266,6 @@
3266 </a>3266 </a>
3267 </h4>3267 </h4>
32683268
3269 <!-- Project ID -->
3270 <div class="flex-container">
3271 <label for="vertexai_project_id" data-i18n="Project ID">Project ID:</label>
3272 <input id="vertexai_project_id" name="vertexai_project_id" class="text_pole flex1" value="" type="text" autocomplete="off" placeholder="your-gcp-project-id">
3273 </div>
3274
3275 <!-- Region -->3269 <!-- Region -->
3276 <div class="flex-container">3270 <div class="flex-container">
3277 <label for="vertexai_region" data-i18n="Region">Region:</label>3271 <label for="vertexai_region" data-i18n="Region">Region:</label>
public/scripts/extensions/shared.js+1 -4
@@ -180,13 +180,10 @@ function throwIfInvalidModel(useReverseProxy) {
180 throw new Error('Google Vertex AI API key is not set for Express mode.');180 throw new Error('Google Vertex AI API key is not set for Express mode.');
181 }181 }
182 } else if (authMode === 'full') {182 } else if (authMode === 'full') {
183 // Full mode requires Service Account JSON and project settings183 // Full mode requires Service Account JSON and region settings
184 if (!secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) {184 if (!secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) {
185 throw new Error('Service Account JSON is required for Vertex AI Full mode. Please validate and save your Service Account JSON.');185 throw new Error('Service Account JSON is required for Vertex AI Full mode. Please validate and save your Service Account JSON.');
186 }186 }
187 if (!secret_state[SECRET_KEYS.VERTEXAI_PROJECT_ID]) {
188 throw new Error('Project ID is required for Vertex AI Full mode.');
189 }
190 if (!oai_settings.vertexai_region) {187 if (!oai_settings.vertexai_region) {
191 throw new Error('Region is required for Vertex AI Full mode.');188 throw new Error('Region is required for Vertex AI Full mode.');
192 }189 }
public/scripts/openai.js+1 -15
@@ -3496,8 +3496,6 @@ function loadOpenAISettings(data, settings) {
3496 $('#claude_use_sysprompt').prop('checked', oai_settings.claude_use_sysprompt);3496 $('#claude_use_sysprompt').prop('checked', oai_settings.claude_use_sysprompt);
3497 $('#use_makersuite_sysprompt').prop('checked', oai_settings.use_makersuite_sysprompt);3497 $('#use_makersuite_sysprompt').prop('checked', oai_settings.use_makersuite_sysprompt);
3498 $('#vertexai_auth_mode').val(oai_settings.vertexai_auth_mode);3498 $('#vertexai_auth_mode').val(oai_settings.vertexai_auth_mode);
3499 // Don't display Project ID in input - it's stored in backend secrets
3500 $('#vertexai_project_id').val('');
3501 $('#vertexai_region').val(oai_settings.vertexai_region);3499 $('#vertexai_region').val(oai_settings.vertexai_region);
3502 // Don't display Service Account JSON in textarea - it's stored in backend secrets3500 // Don't display Service Account JSON in textarea - it's stored in backend secrets
3503 $('#vertexai_service_account_json').val('');3501 $('#vertexai_service_account_json').val('');
@@ -5015,19 +5013,7 @@ async function onConnectButtonClick(e) {
5015 }5013 }
5016 } else {5014 } else {
5017 // Full version - use service account5015 // Full version - use service account
5018 // Check if we have a saved project ID, otherwise use the input value5016 // Project ID will be extracted from the Service Account JSON
5019 const savedProjectId = secret_state[SECRET_KEYS.VERTEXAI_PROJECT_ID];
5020 const inputProjectId = String($('#vertexai_project_id').val()).trim();
5021
5022 if (!savedProjectId && !inputProjectId) {
5023 toastr.error(t`Project ID is required for Vertex AI full version`);
5024 return;
5025 }
5026
5027 // Save project ID to secrets if we have an input value
5028 if (inputProjectId.length) {
5029 await writeSecret(SECRET_KEYS.VERTEXAI_PROJECT_ID, inputProjectId);
5030 }
50315017
5032 // Check if service account JSON is saved in backend5018 // Check if service account JSON is saved in backend
5033 if (!secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) {5019 if (!secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) {
public/scripts/secrets.js+0 -1
@@ -44,7 +44,6 @@ export const SECRET_KEYS = {
44 SERPER: 'api_key_serper',44 SERPER: 'api_key_serper',
45 FALAI: 'api_key_falai',45 FALAI: 'api_key_falai',
46 XAI: 'api_key_xai',46 XAI: 'api_key_xai',
47 VERTEXAI_PROJECT_ID: 'vertexai_project_id',
48 VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',47 VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',
49};48};
5049
src/endpoints/backends/chat-completions.js+14 -5
@@ -43,7 +43,7 @@ import {
43 webTokenizers,43 webTokenizers,
44 getWebTokenizer,44 getWebTokenizer,
45} from '../tokenizers.js';45} from '../tokenizers.js';
46import { getVertexAIAuth } from '../google.js';46import { getVertexAIAuth, getProjectIdFromServiceAccount } from '../google.js';
4747
48const API_OPENAI = 'https://api.openai.com/v1';48const API_OPENAI = 'https://api.openai.com/v1';
49const API_CLAUDE = 'https://api.anthropic.com/v1';49const API_CLAUDE = 'https://api.anthropic.com/v1';
@@ -528,10 +528,19 @@ async function sendMakerSuiteRequest(request, response) {
528 url = `${apiUrl.toString().replace(/\/$/, '')}/v1/publishers/google/models/${model}:${responseType}?key=${keyParam}${stream ? '&alt=sse' : ''}`;528 url = `${apiUrl.toString().replace(/\/$/, '')}/v1/publishers/google/models/${model}:${responseType}?key=${keyParam}${stream ? '&alt=sse' : ''}`;
529 } else if (authType === 'full') {529 } else if (authType === 'full') {
530 // For Full mode (service account authentication), use project-specific URL530 // For Full mode (service account authentication), use project-specific URL
531 // Only use project ID from secrets531 // Get project ID from Service Account JSON
532 const projectId = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_PROJECT_ID);532 const serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT);
533 if (!projectId) {533 if (!serviceAccountJson) {
534 console.warn('Vertex AI project ID is missing.');534 console.warn('Vertex AI Service Account JSON is missing.');
535 return response.status(400).send({ error: true });
536 }
537
538 let projectId;
539 try {
540 const serviceAccount = JSON.parse(serviceAccountJson);
541 projectId = getProjectIdFromServiceAccount(serviceAccount);
542 } catch (error) {
543 console.error('Failed to extract project ID from Service Account JSON:', error);
535 return response.status(400).send({ error: true });544 return response.status(400).send({ error: true });
536 }545 }
537 const region = request.body.vertexai_region || 'us-central1';546 const region = request.body.vertexai_region || 'us-central1';
src/endpoints/google.js+32 -3
@@ -107,6 +107,25 @@ export async function getAccessToken(jwtToken) {
107 return data.access_token;107 return data.access_token;
108}108}
109109
110/**
111 * Extracts the project ID from a Service Account JSON object.
112 * @param {object} serviceAccount Service account JSON object
113 * @returns {string} Project ID
114 * @throws {Error} If project ID is not found in the service account
115 */
116export function getProjectIdFromServiceAccount(serviceAccount) {
117 if (!serviceAccount || typeof serviceAccount !== 'object') {
118 throw new Error('Invalid service account object');
119 }
120
121 const projectId = serviceAccount.project_id;
122 if (!projectId || typeof projectId !== 'string') {
123 throw new Error('Project ID not found in service account JSON');
124 }
125
126 return projectId;
127}
128
110export const router = express.Router();129export const router = express.Router();
111130
112router.post('/caption-image', async (request, response) => {131router.post('/caption-image', async (request, response) => {
@@ -133,9 +152,19 @@ router.post('/caption-image', async (request, response) => {
133 url = `${apiUrl.origin}/v1/publishers/google/models/${model}:generateContent?key=${keyParam}`;152 url = `${apiUrl.origin}/v1/publishers/google/models/${model}:generateContent?key=${keyParam}`;
134 } else if (authType === 'full') {153 } else if (authType === 'full') {
135 // Full mode: use project-specific URL with Authorization header154 // Full mode: use project-specific URL with Authorization header
136 const projectId = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_PROJECT_ID);155 // Get project ID from Service Account JSON
137 if (!projectId) {156 const serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT);
138 console.warn('Vertex AI project ID is missing.');157 if (!serviceAccountJson) {
158 console.warn('Vertex AI Service Account JSON is missing.');
159 return response.status(400).send({ error: true });
160 }
161
162 let projectId;
163 try {
164 const serviceAccount = JSON.parse(serviceAccountJson);
165 projectId = getProjectIdFromServiceAccount(serviceAccount);
166 } catch (error) {
167 console.error('Failed to extract project ID from Service Account JSON:', error);
139 return response.status(400).send({ error: true });168 return response.status(400).send({ error: true });
140 }169 }
141 const region = request.body.vertexai_region || 'us-central1';170 const region = request.body.vertexai_region || 'us-central1';
src/endpoints/secrets.js+0 -1
@@ -54,7 +54,6 @@ export const SECRET_KEYS = {
54 DEEPSEEK: 'api_key_deepseek',54 DEEPSEEK: 'api_key_deepseek',
55 SERPER: 'api_key_serper',55 SERPER: 'api_key_serper',
56 XAI: 'api_key_xai',56 XAI: 'api_key_xai',
57 VERTEXAI_PROJECT_ID: 'vertexai_project_id',
58 VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',57 VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',
59};58};
6059