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.

5656c7950dcda9e5fa9c01d7fd544799f7880bc4

InterestingDarkness <computeridiot999@gmail.com>

7 files changed, +674 -45Ignore whitespace
public/index.html+128 -15
@@ -2779,7 +2779,7 @@
2779 <option value="deepseek">DeepSeek</option>2779 <option value="deepseek">DeepSeek</option>
2780 <option value="groq">Groq</option>2780 <option value="groq">Groq</option>
2781 <option value="makersuite">Google AI Studio</option>2781 <option value="makersuite">Google AI Studio</option>
2782 <option value="vertexai">Google Vertex AI (Express mode)</option>2782 <option value="vertexai">Google Vertex AI</option>
2783 <option value="mistralai">MistralAI</option>2783 <option value="mistralai">MistralAI</option>
2784 <option value="nanogpt">NanoGPT</option>2784 <option value="nanogpt">NanoGPT</option>
2785 <option value="openrouter">OpenRouter</option>2785 <option value="openrouter">OpenRouter</option>
@@ -3225,33 +3225,146 @@
3225 </div>3225 </div>
3226 </form>3226 </form>
3227 <div id="vertexai_form" data-source="vertexai">3227 <div id="vertexai_form" data-source="vertexai">
3228 <h4>3228 <h4 data-i18n="Google Vertex AI Configuration">Google Vertex AI Configuration</h4>
3229 <span data-i18n="Google Vertex AI API Key">3229
3230 Google Vertex AI API Key3230 <!-- Authentication Mode Selection -->
3231 </span>
3232 <a href="https://cloud.google.com/vertex-ai/generative-ai/docs/start/express-mode/overview" data-i18n="(Express mode keys only)" target="_blank" rel="noopener noreferrer">
3233 (Express mode keys only)
3234 </a>
3235 </h4>
3236 <div class="flex-container">3231 <div class="flex-container">
3237 <input id="api_key_vertexai" name="api_key_vertexai" class="text_pole flex1" value="" type="text" autocomplete="off">3232 <label for="vertexai_auth_mode" data-i18n="Authentication Mode">Authentication Mode:</label>
3238 <div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_vertexai"></div>3233 <select id="vertexai_auth_mode" class="text_pole">
3234 <option value="express" data-i18n="Express Mode (API Key)">Express Mode (API Key)</option>
3235 <option value="full" data-i18n="Full Version (Service Account)">Full Version (Service Account)</option>
3236 </select>
3239 </div>3237 </div>
3240 <div data-for="api_key_vertexai" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you reload the page.">3238
3241 For privacy reasons, your API key will be hidden after you reload the page.3239 <!-- Express Mode Configuration -->
3240 <div id="vertexai_express_config" class="vertexai-auth-section">
3241 <h4>
3242 <span data-i18n="Google Vertex AI API Key">
3243 Google Vertex AI API Key
3244 </span>
3245 <a href="https://cloud.google.com/vertex-ai/generative-ai/docs/start/express-mode/overview" data-i18n="(Express mode)" target="_blank" rel="noopener noreferrer">
3246 (Express mode)
3247 </a>
3248 </h4>
3249 <div class="flex-container">
3250 <input id="api_key_vertexai" name="api_key_vertexai" class="text_pole flex1" value="" type="text" autocomplete="off">
3251 <div title="Clear your API key" data-i18n="[title]Clear your API key" class="menu_button fa-solid fa-circle-xmark clear-api-key" data-key="api_key_vertexai"></div>
3252 </div>
3253 <div data-for="api_key_vertexai" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you reload the page.">
3254 For privacy reasons, your API key will be hidden after you reload the page.
3255 </div>
3256 </div>
3257
3258 <!-- Full Version Configuration -->
3259 <div id="vertexai_full_config" class="vertexai-auth-section" style="display: none;">
3260 <h4>
3261 <span data-i18n="Service Account Configuration">
3262 Service Account Configuration
3263 </span>
3264 <a href="https://cloud.google.com/vertex-ai/docs/authentication" target="_blank" rel="noopener noreferrer">
3265 <i class="fa-solid fa-circle-question"></i>
3266 </a>
3267 </h4>
3268
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 -->
3276 <div class="flex-container">
3277 <label for="vertexai_region" data-i18n="Region">Region:</label>
3278 <input id="vertexai_region" name="vertexai_region" class="text_pole flex1" value="us-central1" type="text" autocomplete="off" placeholder="e.g., us-central1, europe-west1, asia-northeast1">
3279 </div>
3280
3281 <!-- Service Account JSON Content -->
3282 <div class="flex-container flexFlowColumn">
3283 <label for="vertexai_service_account_json" data-i18n="Service Account JSON Content">Service Account JSON Content:</label>
3284 <textarea id="vertexai_service_account_json" class="text_pole textarea_compact" rows="8" placeholder='Paste your Service Account JSON content here, e.g.:
3285{
3286 "type": "service_account",
3287 "project_id": "your-project-id",
3288 "private_key_id": "...",
3289 "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
3290 "client_email": "...",
3291 "client_id": "...",
3292 "auth_uri": "https://accounts.google.com/o/oauth2/auth",
3293 "token_uri": "https://oauth2.googleapis.com/token"
3294}'></textarea>
3295 <div class="neutral_warning" data-i18n="Paste the complete JSON content of your Google Cloud Service Account key file. The content will be stored securely.">
3296 Paste the complete JSON content of your Google Cloud Service Account key file. The content will be stored securely.
3297 </div>
3298 <div class="flex-container">
3299 <button type="button" id="vertexai_validate_service_account" class="menu_button" data-i18n="Validate JSON">Validate JSON</button>
3300 <button type="button" id="vertexai_clear_service_account" class="menu_button" data-i18n="Clear">Clear</button>
3301 </div>
3302 <div id="vertexai_service_account_status" class="info-block" style="display: none;">
3303 <span id="vertexai_service_account_info"></span>
3304 </div>
3305 </div>
3242 </div>3306 </div>
3307
3308 <!-- Model Selection -->
3243 <div>3309 <div>
3244 <h4 data-i18n="Google Model">Google Model</h4>3310 <h4 data-i18n="Google Model">Google Model</h4>
3245 <select id="model_vertexai_select">3311 <select id="model_vertexai_select">
3246 <optgroup label="Gemini 2.5">3312 <!-- Express Mode Models -->
3313 <optgroup id="vertexai_express_models" label="Express Mode Models">
3247 <option value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option>3314 <option value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option>
3248 <option value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option>3315 <option value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option>
3249 <option value="gemini-2.5-flash-preview-05-20">gemini-2.5-flash-preview-05-20</option>3316 <option value="gemini-2.5-flash-preview-05-20">gemini-2.5-flash-preview-05-20</option>
3250 <option value="gemini-2.5-flash-preview-04-17">gemini-2.5-flash-preview-04-17</option>3317 <option value="gemini-2.5-flash-preview-04-17">gemini-2.5-flash-preview-04-17</option>
3318 <option value="gemini-2.0-flash-001">gemini-2.0-flash-001</option>
3319 <option value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option>
3251 </optgroup>3320 </optgroup>
3252 <optgroup label="Gemini 2.0">3321
3322 <!-- Full Version Models -->
3323 <optgroup id="vertexai_full_gemini_25" label="Gemini 2.5" style="display: none;">
3324 <option value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option>
3325 <option value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option>
3326 <option value="gemini-2.5-pro-exp-03-25">gemini-2.5-pro-exp-03-25</option>
3327 <option value="gemini-2.5-flash-preview-05-20">gemini-2.5-flash-preview-05-20</option>
3328 <option value="gemini-2.5-flash-preview-04-17">gemini-2.5-flash-preview-04-17</option>
3329 </optgroup>
3330 <optgroup id="vertexai_full_gemini_20" label="Gemini 2.0" style="display: none;">
3331 <option value="gemini-2.0-pro-exp-02-05">gemini-2.0-pro-exp-02-05 → 2.5-pro-exp-03-25</option>
3332 <option value="gemini-2.0-pro-exp">gemini-2.0-pro-exp → 2.5-pro-exp-03-25</option>
3333 <option value="gemini-exp-1206">gemini-exp-1206 → 2.5-pro-exp-03-25</option>
3253 <option value="gemini-2.0-flash-001">gemini-2.0-flash-001</option>3334 <option value="gemini-2.0-flash-001">gemini-2.0-flash-001</option>
3335 <option value="gemini-2.0-flash-exp-image-generation">gemini-2.0-flash-exp-image-generation</option>
3336 <option value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
3337 <option value="gemini-2.0-flash">gemini-2.0-flash</option>
3338 <option value="gemini-2.0-flash-thinking-exp-01-21">gemini-2.0-flash-thinking-exp-01-21 → 2.5-flash-preview-04-17</option>
3339 <option value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219 → 2.5-flash-preview-04-17</option>
3340 <option value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp → 2.5-flash-preview-04-17</option>
3254 <option value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option>3341 <option value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option>
3342 <option value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option>
3343 <option value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option>
3344 </optgroup>
3345 <optgroup id="vertexai_full_gemini_15" label="Gemini 1.5" style="display: none;">
3346 <option value="gemini-1.5-pro-latest">gemini-1.5-pro-latest</option>
3347 <option value="gemini-1.5-pro-002">gemini-1.5-pro-002</option>
3348 <option value="gemini-1.5-pro-001">gemini-1.5-pro-001</option>
3349 <option value="gemini-1.5-pro">gemini-1.5-pro</option>
3350 <option value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
3351 <option value="gemini-1.5-flash-002">gemini-1.5-flash-002</option>
3352 <option value="gemini-1.5-flash-001">gemini-1.5-flash-001</option>
3353 <option value="gemini-1.5-flash">gemini-1.5-flash</option>
3354 <option value="gemini-1.5-flash-8b-001">gemini-1.5-flash-8b-001</option>
3355 <option value="gemini-1.5-flash-8b-exp-0924">gemini-1.5-flash-8b-exp-0924</option>
3356 <option value="gemini-1.5-flash-8b-exp-0827">gemini-1.5-flash-8b-exp-0827</option>
3357 <option value="gemini-1.5-flash-8b">gemini-1.5-flash-8b</option>
3358 </optgroup>
3359 <optgroup id="vertexai_full_gemma" label="Gemma" style="display: none;">
3360 <option value="gemma-3-27b-it">gemma-3-27b-it</option>
3361 <option value="gemma-3-12b-it">gemma-3-12b-it</option>
3362 <option value="gemma-3-4b-it">gemma-3-4b-it</option>
3363 <option value="gemma-3-1b-it">gemma-3-1b-it</option>
3364 </optgroup>
3365 <optgroup id="vertexai_full_learnlm" label="LearnLM" style="display: none;">
3366 <option value="learnlm-2.0-flash-experimental">learnlm-2.0-flash-experimental</option>
3367 <option value="learnlm-1.5-pro-experimental">learnlm-1.5-pro-experimental</option>
3255 </optgroup>3368 </optgroup>
3256 </select>3369 </select>
3257 </div>3370 </div>
public/scripts/extensions/shared.js+28 -2
@@ -56,6 +56,13 @@ export async function getMultimodalCaption(base64Img, prompt) {
56 model: extension_settings.caption.multimodal_model || 'gpt-4-turbo',56 model: extension_settings.caption.multimodal_model || 'gpt-4-turbo',
57 };57 };
5858
59 // Add Vertex AI specific parameters if using Vertex AI
60 if (extension_settings.caption.multimodal_api === 'vertexai') {
61 requestBody.vertexai_auth_mode = oai_settings.vertexai_auth_mode;
62 requestBody.vertexai_project_id = oai_settings.vertexai_project_id;
63 requestBody.vertexai_region = oai_settings.vertexai_region;
64 }
65
59 if (isOllama) {66 if (isOllama) {
60 if (extension_settings.caption.multimodal_model === 'ollama_current') {67 if (extension_settings.caption.multimodal_model === 'ollama_current') {
61 requestBody.model = textgenerationwebui_settings.ollama_model;68 requestBody.model = textgenerationwebui_settings.ollama_model;
@@ -164,8 +171,27 @@ function throwIfInvalidModel(useReverseProxy) {
164 throw new Error('Google AI Studio API key is not set.');171 throw new Error('Google AI Studio API key is not set.');
165 }172 }
166173
167 if (multimodalApi === 'vertexai' && !secret_state[SECRET_KEYS.VERTEXAI] && !useReverseProxy) {174 if (multimodalApi === 'vertexai' && !useReverseProxy) {
168 throw new Error('Google Vertex AI API key is not set.');175 // Check based on authentication mode
176 const authMode = oai_settings.vertexai_auth_mode || 'express';
177
178 if (authMode === 'express') {
179 // Express mode requires API key
180 if (!secret_state[SECRET_KEYS.VERTEXAI]) {
181 throw new Error('Google Vertex AI API key is not set for Express mode.');
182 }
183 } else if (authMode === 'full') {
184 // Full mode requires Service Account JSON and project settings
185 if (!secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) {
186 throw new Error('Service Account JSON is required for Vertex AI Full mode. Please validate and save your Service Account JSON.');
187 }
188 if (!oai_settings.vertexai_project_id) {
189 throw new Error('Project ID is required for Vertex AI Full mode.');
190 }
191 if (!oai_settings.vertexai_region) {
192 throw new Error('Region is required for Vertex AI Full mode.');
193 }
194 }
169 }195 }
170196
171 if (multimodalApi === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] && !useReverseProxy) {197 if (multimodalApi === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] && !useReverseProxy) {
public/scripts/openai.js+220 -7
@@ -306,6 +306,10 @@ export const settingsToUpdate = {
306 assistant_impersonation: ['#claude_assistant_impersonation', 'assistant_impersonation', false, false],306 assistant_impersonation: ['#claude_assistant_impersonation', 'assistant_impersonation', false, false],
307 claude_use_sysprompt: ['#claude_use_sysprompt', 'claude_use_sysprompt', true, false],307 claude_use_sysprompt: ['#claude_use_sysprompt', 'claude_use_sysprompt', true, false],
308 use_makersuite_sysprompt: ['#use_makersuite_sysprompt', 'use_makersuite_sysprompt', true, false],308 use_makersuite_sysprompt: ['#use_makersuite_sysprompt', 'use_makersuite_sysprompt', true, false],
309 vertexai_auth_mode: ['#vertexai_auth_mode', 'vertexai_auth_mode', false, true],
310 vertexai_project_id: ['#vertexai_project_id', 'vertexai_project_id', false, true],
311 vertexai_region: ['#vertexai_region', 'vertexai_region', false, true],
312 vertexai_service_account_json: ['#vertexai_service_account_json', 'vertexai_service_account_json', false, true],
309 use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true, true],313 use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true, true],
310 squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true, false],314 squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true, false],
311 image_inlining: ['#openai_image_inlining', 'image_inlining', true, false],315 image_inlining: ['#openai_image_inlining', 'image_inlining', true, false],
@@ -387,6 +391,10 @@ const default_settings = {
387 assistant_impersonation: '',391 assistant_impersonation: '',
388 claude_use_sysprompt: false,392 claude_use_sysprompt: false,
389 use_makersuite_sysprompt: true,393 use_makersuite_sysprompt: true,
394 vertexai_auth_mode: 'express',
395 vertexai_project_id: '',
396 vertexai_region: 'us-central1',
397 vertexai_service_account_json: '',
390 use_alt_scale: false,398 use_alt_scale: false,
391 squash_system_messages: false,399 squash_system_messages: false,
392 image_inlining: false,400 image_inlining: false,
@@ -471,6 +479,10 @@ const oai_settings = {
471 assistant_impersonation: '',479 assistant_impersonation: '',
472 claude_use_sysprompt: false,480 claude_use_sysprompt: false,
473 use_makersuite_sysprompt: true,481 use_makersuite_sysprompt: true,
482 vertexai_auth_mode: 'express',
483 vertexai_project_id: '',
484 vertexai_region: 'us-central1',
485 vertexai_service_account_json: '',
474 use_alt_scale: false,486 use_alt_scale: false,
475 squash_system_messages: false,487 squash_system_messages: false,
476 image_inlining: false,488 image_inlining: false,
@@ -2188,6 +2200,11 @@ async function sendOpenAIRequest(type, messages, signal) {
2188 generate_data['top_k'] = Number(oai_settings.top_k_openai);2200 generate_data['top_k'] = Number(oai_settings.top_k_openai);
2189 generate_data['stop'] = getCustomStoppingStrings(stopStringsLimit).slice(0, stopStringsLimit).filter(x => x.length >= 1 && x.length <= 16);2201 generate_data['stop'] = getCustomStoppingStrings(stopStringsLimit).slice(0, stopStringsLimit).filter(x => x.length >= 1 && x.length <= 16);
2190 generate_data['use_makersuite_sysprompt'] = oai_settings.use_makersuite_sysprompt;2202 generate_data['use_makersuite_sysprompt'] = oai_settings.use_makersuite_sysprompt;
2203 if (isVertexAI) {
2204 generate_data['vertexai_auth_mode'] = oai_settings.vertexai_auth_mode;
2205 generate_data['vertexai_project_id'] = oai_settings.vertexai_project_id;
2206 generate_data['vertexai_region'] = oai_settings.vertexai_region;
2207 }
2191 }2208 }
21922209
2193 if (isMistral) {2210 if (isMistral) {
@@ -3427,6 +3444,9 @@ function loadOpenAISettings(data, settings) {
3427 if (settings.openai_model !== undefined) oai_settings.openai_model = settings.openai_model;3444 if (settings.openai_model !== undefined) oai_settings.openai_model = settings.openai_model;
3428 if (settings.claude_use_sysprompt !== undefined) oai_settings.claude_use_sysprompt = !!settings.claude_use_sysprompt;3445 if (settings.claude_use_sysprompt !== undefined) oai_settings.claude_use_sysprompt = !!settings.claude_use_sysprompt;
3429 if (settings.use_makersuite_sysprompt !== undefined) oai_settings.use_makersuite_sysprompt = !!settings.use_makersuite_sysprompt;3446 if (settings.use_makersuite_sysprompt !== undefined) oai_settings.use_makersuite_sysprompt = !!settings.use_makersuite_sysprompt;
3447 if (settings.vertexai_auth_mode !== undefined) oai_settings.vertexai_auth_mode = settings.vertexai_auth_mode;
3448 if (settings.vertexai_project_id !== undefined) oai_settings.vertexai_project_id = settings.vertexai_project_id;
3449 if (settings.vertexai_region !== undefined) oai_settings.vertexai_region = settings.vertexai_region;
3430 if (settings.use_alt_scale !== undefined) { oai_settings.use_alt_scale = !!settings.use_alt_scale; updateScaleForm(); }3450 if (settings.use_alt_scale !== undefined) { oai_settings.use_alt_scale = !!settings.use_alt_scale; updateScaleForm(); }
3431 $('#stream_toggle').prop('checked', oai_settings.stream_openai);3451 $('#stream_toggle').prop('checked', oai_settings.stream_openai);
3432 $('#api_url_scale').val(oai_settings.api_url_scale);3452 $('#api_url_scale').val(oai_settings.api_url_scale);
@@ -3482,6 +3502,12 @@ function loadOpenAISettings(data, settings) {
3482 $('#openai_external_category').toggle(oai_settings.show_external_models);3502 $('#openai_external_category').toggle(oai_settings.show_external_models);
3483 $('#claude_use_sysprompt').prop('checked', oai_settings.claude_use_sysprompt);3503 $('#claude_use_sysprompt').prop('checked', oai_settings.claude_use_sysprompt);
3484 $('#use_makersuite_sysprompt').prop('checked', oai_settings.use_makersuite_sysprompt);3504 $('#use_makersuite_sysprompt').prop('checked', oai_settings.use_makersuite_sysprompt);
3505 $('#vertexai_auth_mode').val(oai_settings.vertexai_auth_mode);
3506 $('#vertexai_project_id').val(oai_settings.vertexai_project_id);
3507 $('#vertexai_region').val(oai_settings.vertexai_region);
3508 // Don't display Service Account JSON in textarea - it's stored in backend secrets
3509 $('#vertexai_service_account_json').val('');
3510 updateVertexAIServiceAccountStatus();
3485 $('#scale-alt').prop('checked', oai_settings.use_alt_scale);3511 $('#scale-alt').prop('checked', oai_settings.use_alt_scale);
3486 $('#openrouter_use_fallback').prop('checked', oai_settings.openrouter_use_fallback);3512 $('#openrouter_use_fallback').prop('checked', oai_settings.openrouter_use_fallback);
3487 $('#openrouter_group_models').prop('checked', oai_settings.openrouter_group_models);3513 $('#openrouter_group_models').prop('checked', oai_settings.openrouter_group_models);
@@ -3804,6 +3830,10 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
3804 assistant_impersonation: settings.assistant_impersonation,3830 assistant_impersonation: settings.assistant_impersonation,
3805 claude_use_sysprompt: settings.claude_use_sysprompt,3831 claude_use_sysprompt: settings.claude_use_sysprompt,
3806 use_makersuite_sysprompt: settings.use_makersuite_sysprompt,3832 use_makersuite_sysprompt: settings.use_makersuite_sysprompt,
3833 vertexai_auth_mode: settings.vertexai_auth_mode,
3834 vertexai_project_id: settings.vertexai_project_id,
3835 vertexai_region: settings.vertexai_region,
3836 vertexai_service_account_json: settings.vertexai_service_account_json,
3807 use_alt_scale: settings.use_alt_scale,3837 use_alt_scale: settings.use_alt_scale,
3808 squash_system_messages: settings.squash_system_messages,3838 squash_system_messages: settings.squash_system_messages,
3809 image_inlining: settings.image_inlining,3839 image_inlining: settings.image_inlining,
@@ -4979,15 +5009,30 @@ async function onConnectButtonClick(e) {
4979 }5009 }
49805010
4981 if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) {5011 if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) {
4982 const api_key_vertexai = String($('#api_key_vertexai').val()).trim();5012 if (oai_settings.vertexai_auth_mode === 'express') {
5013 // Express mode - use API key
5014 const api_key_vertexai = String($('#api_key_vertexai').val()).trim();
49835015
4984 if (api_key_vertexai.length) {5016 if (api_key_vertexai.length) {
4985 await writeSecret(SECRET_KEYS.VERTEXAI, api_key_vertexai);5017 await writeSecret(SECRET_KEYS.VERTEXAI, api_key_vertexai);
4986 }5018 }
49875019
4988 if (!secret_state[SECRET_KEYS.VERTEXAI] && !oai_settings.reverse_proxy) {5020 if (!secret_state[SECRET_KEYS.VERTEXAI] && !oai_settings.reverse_proxy) {
4989 console.log('No secret key saved for Vertex AI');5021 console.log('No secret key saved for Vertex AI Express mode');
4990 return;5022 return;
5023 }
5024 } else {
5025 // Full version - use service account
5026 if (!oai_settings.vertexai_project_id.trim()) {
5027 toastr.error('Project ID is required for Vertex AI full version');
5028 return;
5029 }
5030
5031 // Check if service account JSON is saved in backend
5032 if (!secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) {
5033 toastr.error('Service Account JSON is required for Vertex AI full version. Please validate and save your Service Account JSON.');
5034 return;
5035 }
4991 }5036 }
4992 }5037 }
49935038
@@ -5170,6 +5215,8 @@ function toggleChatCompletionForms() {
5170 }5215 }
5171 else if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) {5216 else if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) {
5172 $('#model_vertexai_select').trigger('change');5217 $('#model_vertexai_select').trigger('change');
5218 // Update UI based on authentication mode
5219 onVertexAIAuthModeChange.call($('#vertexai_auth_mode')[0]);
5173 }5220 }
5174 else if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) {5221 else if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) {
5175 $('#model_openrouter_select').trigger('change');5222 $('#model_openrouter_select').trigger('change');
@@ -5480,6 +5527,160 @@ function runProxyCallback(_, value) {
5480 return foundName;5527 return foundName;
5481}5528}
54825529
5530/**
5531 * Handle Vertex AI authentication mode change
5532 */
5533function onVertexAIAuthModeChange() {
5534 const authMode = String($(this).val());
5535 oai_settings.vertexai_auth_mode = authMode;
5536
5537 // Show/hide appropriate configuration sections
5538 if (authMode === 'express') {
5539 $('#vertexai_express_config').show();
5540 $('#vertexai_full_config').hide();
5541 $('#vertexai_express_models').show();
5542 // Hide all full version model groups
5543 $('#vertexai_full_gemini_25').hide();
5544 $('#vertexai_full_gemini_20').hide();
5545 $('#vertexai_full_gemini_15').hide();
5546 $('#vertexai_full_gemma').hide();
5547 $('#vertexai_full_learnlm').hide();
5548 } else {
5549 $('#vertexai_express_config').hide();
5550 $('#vertexai_full_config').show();
5551 $('#vertexai_express_models').hide();
5552 // Show all full version model groups
5553 $('#vertexai_full_gemini_25').show();
5554 $('#vertexai_full_gemini_20').show();
5555 $('#vertexai_full_gemini_15').show();
5556 $('#vertexai_full_gemma').show();
5557 $('#vertexai_full_learnlm').show();
5558 }
5559
5560 saveSettingsDebounced();
5561}
5562
5563/**
5564 * Validate Vertex AI service account JSON
5565 */
5566async function onVertexAIValidateServiceAccount() {
5567 const jsonContent = String($('#vertexai_service_account_json').val()).trim();
5568
5569 if (!jsonContent) {
5570 toastr.error('Please enter Service Account JSON content');
5571 return;
5572 }
5573
5574 try {
5575 const serviceAccount = JSON.parse(jsonContent);
5576 const requiredFields = ['type', 'project_id', 'private_key', 'client_email', 'client_id'];
5577 const missingFields = requiredFields.filter(field => !serviceAccount[field]);
5578
5579 if (missingFields.length > 0) {
5580 toastr.error(`Missing required fields: ${missingFields.join(', ')}`);
5581 updateVertexAIServiceAccountStatus(false, `Missing fields: ${missingFields.join(', ')}`);
5582 return;
5583 }
5584
5585 if (serviceAccount.type !== 'service_account') {
5586 toastr.error('Invalid service account type. Expected "service_account"');
5587 updateVertexAIServiceAccountStatus(false, 'Invalid service account type');
5588 return;
5589 }
5590
5591 // Save to backend secret storage
5592 await writeSecret(SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT, jsonContent);
5593
5594 // Clear the textarea and update settings
5595 $('#vertexai_service_account_json').val('');
5596 oai_settings.vertexai_service_account_json = '';
5597
5598 // Show success status
5599 updateVertexAIServiceAccountStatus(true, `Project: ${serviceAccount.project_id}, Email: ${serviceAccount.client_email}`);
5600
5601 toastr.success('Service Account JSON is valid and saved securely');
5602 saveSettingsDebounced();
5603 } catch (error) {
5604 console.error('JSON validation error:', error);
5605 toastr.error('Invalid JSON format');
5606 updateVertexAIServiceAccountStatus(false, 'Invalid JSON format');
5607 }
5608}
5609
5610/**
5611 * Clear Vertex AI service account JSON
5612 */
5613async function onVertexAIClearServiceAccount() {
5614 $('#vertexai_service_account_json').val('');
5615 oai_settings.vertexai_service_account_json = '';
5616
5617 // Clear from backend secret storage
5618 await writeSecret(SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT, '');
5619
5620 updateVertexAIServiceAccountStatus(false);
5621 toastr.info('Service Account JSON cleared');
5622 saveSettingsDebounced();
5623}
5624
5625/**
5626 * Handle Vertex AI service account JSON input change
5627 */
5628function onVertexAIServiceAccountJsonChange() {
5629 const jsonContent = String($(this).val()).trim();
5630 // Don't save to settings automatically - only save when validated
5631 // oai_settings.vertexai_service_account_json = jsonContent;
5632
5633 if (jsonContent) {
5634 // Auto-validate when content is pasted
5635 try {
5636 const serviceAccount = JSON.parse(jsonContent);
5637 const requiredFields = ['type', 'project_id', 'private_key', 'client_email'];
5638 const hasAllFields = requiredFields.every(field => serviceAccount[field]);
5639
5640 if (hasAllFields && serviceAccount.type === 'service_account') {
5641 updateVertexAIServiceAccountStatus(false, 'JSON appears valid - click "Validate JSON" to save');
5642 } else {
5643 updateVertexAIServiceAccountStatus(false, 'Incomplete or invalid JSON');
5644 }
5645 } catch (error) {
5646 updateVertexAIServiceAccountStatus(false, 'Invalid JSON format');
5647 }
5648 } else {
5649 updateVertexAIServiceAccountStatus(false);
5650 }
5651
5652 // Don't save settings automatically
5653 // saveSettingsDebounced();
5654}
5655
5656/**
5657 * Update the Vertex AI service account status display
5658 * @param {boolean} isValid - Whether the service account is valid
5659 * @param {string} message - Status message to display
5660 */
5661function updateVertexAIServiceAccountStatus(isValid = false, message = '') {
5662 const statusDiv = $('#vertexai_service_account_status');
5663 const infoSpan = $('#vertexai_service_account_info');
5664
5665 // If no explicit message provided, check if we have a saved service account
5666 if (!message && secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) {
5667 isValid = true;
5668 message = 'Service Account JSON is saved and ready to use';
5669 }
5670
5671 if (isValid && message) {
5672 infoSpan.html(`<i class="fa-solid fa-check-circle" style="color: green;"></i> ${message}`);
5673 statusDiv.show();
5674 } else if (!isValid && message) {
5675 infoSpan.html(`<i class="fa-solid fa-exclamation-triangle" style="color: orange;"></i> ${message}`);
5676 statusDiv.show();
5677 } else {
5678 statusDiv.hide();
5679 }
5680}
5681
5682
5683
5483export function initOpenAI() {5684export function initOpenAI() {
5484 SlashCommandParser.addCommandObject(SlashCommand.fromProps({5685 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
5485 name: 'proxy',5686 name: 'proxy',
@@ -5943,6 +6144,18 @@ export function initOpenAI() {
5943 $('#model_scale_select').on('change', onModelChange);6144 $('#model_scale_select').on('change', onModelChange);
5944 $('#model_google_select').on('change', onModelChange);6145 $('#model_google_select').on('change', onModelChange);
5945 $('#model_vertexai_select').on('change', onModelChange);6146 $('#model_vertexai_select').on('change', onModelChange);
6147 $('#vertexai_auth_mode').on('change', onVertexAIAuthModeChange);
6148 $('#vertexai_project_id').on('input', function () {
6149 oai_settings.vertexai_project_id = String($(this).val());
6150 saveSettingsDebounced();
6151 });
6152 $('#vertexai_region').on('input', function () {
6153 oai_settings.vertexai_region = String($(this).val());
6154 saveSettingsDebounced();
6155 });
6156 $('#vertexai_service_account_json').on('input', onVertexAIServiceAccountJsonChange);
6157 $('#vertexai_validate_service_account').on('click', onVertexAIValidateServiceAccount);
6158 $('#vertexai_clear_service_account').on('click', onVertexAIClearServiceAccount);
5946 $('#model_openrouter_select').on('change', onModelChange);6159 $('#model_openrouter_select').on('change', onModelChange);
5947 $('#openrouter_group_models').on('change', onOpenrouterModelSortChange);6160 $('#openrouter_group_models').on('change', onOpenrouterModelSortChange);
5948 $('#openrouter_sort_models').on('change', onOpenrouterModelSortChange);6161 $('#openrouter_sort_models').on('change', onOpenrouterModelSortChange);
public/scripts/secrets.js+1 -0
@@ -44,6 +44,7 @@ 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_SERVICE_ACCOUNT: 'vertexai_service_account_json',
47};48};
4849
49const INPUT_MAP = {50const INPUT_MAP = {
src/endpoints/backends/chat-completions.js+159 -8
@@ -1,5 +1,6 @@
1import process from 'node:process';1import process from 'node:process';
2import util from 'node:util';2import util from 'node:util';
3import crypto from 'node:crypto';
3import express from 'express';4import express from 'express';
4import fetch from 'node-fetch';5import fetch from 'node-fetch';
56
@@ -61,6 +62,124 @@ const API_XAI = 'https://api.x.ai/v1';
61const API_POLLINATIONS = 'https://text.pollinations.ai/openai';62const API_POLLINATIONS = 'https://text.pollinations.ai/openai';
6263
63/**64/**
65 * Generates a JWT token for Google Cloud authentication using service account credentials.
66 * @param {object} serviceAccount Service account JSON object
67 * @returns {Promise<string>} JWT token
68 */
69async function generateJWTToken(serviceAccount) {
70 const now = Math.floor(Date.now() / 1000);
71 const expiry = now + 3600; // 1 hour
72
73 const header = {
74 alg: 'RS256',
75 typ: 'JWT'
76 };
77
78 const payload = {
79 iss: serviceAccount.client_email,
80 scope: 'https://www.googleapis.com/auth/cloud-platform',
81 aud: 'https://oauth2.googleapis.com/token',
82 iat: now,
83 exp: expiry
84 };
85
86 const headerBase64 = Buffer.from(JSON.stringify(header)).toString('base64url');
87 const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
88 const signatureInput = `${headerBase64}.${payloadBase64}`;
89
90 // Create signature using private key
91 const sign = crypto.createSign('RSA-SHA256');
92 sign.update(signatureInput);
93 const signature = sign.sign(serviceAccount.private_key, 'base64url');
94
95 return `${signatureInput}.${signature}`;
96}
97
98/**
99 * Gets an access token from Google OAuth2 using JWT assertion.
100 * @param {string} jwtToken JWT token
101 * @returns {Promise<string>} Access token
102 */
103async function getAccessToken(jwtToken) {
104 const response = await fetch('https://oauth2.googleapis.com/token', {
105 method: 'POST',
106 headers: {
107 'Content-Type': 'application/x-www-form-urlencoded'
108 },
109 body: new URLSearchParams({
110 grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
111 assertion: jwtToken
112 })
113 });
114
115 if (!response.ok) {
116 throw new Error(`Failed to get access token: ${response.status} ${response.statusText}`);
117 }
118
119 const data = await response.json();
120 return data.access_token;
121}
122
123/**
124 * Gets authentication for Vertex AI - either API key or service account token.
125 * @param {object} request Express request
126 * @returns {Promise<{authHeader: string, authType: string}>} Authentication header and type
127 */
128async function getVertexAIAuth(request) {
129 // Get the authentication mode from frontend
130 const authMode = request.body.vertexai_auth_mode || 'express';
131
132 // Check if using reverse proxy
133 if (request.body.reverse_proxy) {
134 return {
135 authHeader: `Bearer ${request.body.proxy_password}`,
136 authType: 'proxy'
137 };
138 }
139
140 if (authMode === 'express') {
141 // Express mode: use API key
142 const apiKey = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI);
143 if (apiKey) {
144 return {
145 authHeader: `Bearer ${apiKey}`,
146 authType: 'express'
147 };
148 }
149 throw new Error('API key is required for Vertex AI Express mode');
150 } else if (authMode === 'full') {
151 // Full mode: use service account JSON
152 // First try to read from backend secret storage
153 let serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT);
154
155 // If not found in secrets, fall back to request body (for backward compatibility)
156 if (!serviceAccountJson) {
157 serviceAccountJson = request.body.vertexai_service_account_json;
158 }
159
160 if (serviceAccountJson) {
161 try {
162 const serviceAccount = JSON.parse(serviceAccountJson);
163
164 const jwtToken = await generateJWTToken(serviceAccount);
165 const accessToken = await getAccessToken(jwtToken);
166
167 return {
168 authHeader: `Bearer ${accessToken}`,
169 authType: 'full'
170 };
171 } catch (error) {
172 console.error('Failed to authenticate with service account:', error);
173 throw new Error(`Service account authentication failed: ${error.message}`);
174 }
175 }
176 throw new Error('Service Account JSON is required for Vertex AI Full mode');
177 }
178
179 throw new Error(`Unsupported Vertex AI authentication mode: ${authMode}`);
180}
181
182/**
64 * Applies a post-processing step to the generated messages.183 * Applies a post-processing step to the generated messages.
65 * @param {object[]} messages Messages to post-process184 * @param {object[]} messages Messages to post-process
66 * @param {string} type Prompt conversion type185 * @param {string} type Prompt conversion type
@@ -348,13 +467,20 @@ async function sendMakerSuiteRequest(request, response) {
348 let apiUrl;467 let apiUrl;
349 let apiKey;468 let apiKey;
350469
470 let authHeader;
471 let authType;
472
351 if (useVertexAi) {473 if (useVertexAi) {
352 apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI);474 apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI);
353 apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.VERTEXAI);
354475
355 if (!request.body.reverse_proxy && !apiKey) {476 try {
356 console.warn(`${apiName} API key is missing.`);477 const auth = await getVertexAIAuth(request);
357 return response.status(400).send({ error: true });478 authHeader = auth.authHeader;
479 authType = auth.authType;
480 console.debug(`Using Vertex AI authentication type: ${authType}`);
481 } catch (error) {
482 console.warn(`${apiName} authentication failed: ${error.message}`);
483 return response.status(400).send({ error: true, message: error.message });
358 }484 }
359 } else {485 } else {
360 apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE);486 apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE);
@@ -364,6 +490,9 @@ async function sendMakerSuiteRequest(request, response) {
364 console.warn(`${apiName} API key is missing.`);490 console.warn(`${apiName} API key is missing.`);
365 return response.status(400).send({ error: true });491 return response.status(400).send({ error: true });
366 }492 }
493
494 authHeader = `Bearer ${apiKey}`;
495 authType = 'api_key';
367 }496 }
368497
369 const model = String(request.body.model);498 const model = String(request.body.model);
@@ -499,17 +628,39 @@ async function sendMakerSuiteRequest(request, response) {
499 const responseType = (stream ? 'streamGenerateContent' : 'generateContent');628 const responseType = (stream ? 'streamGenerateContent' : 'generateContent');
500629
501 let url;630 let url;
631 let headers = {
632 'Content-Type': 'application/json',
633 };
634
502 if (useVertexAi) {635 if (useVertexAi) {
503 url = `${apiUrl.toString().replace(/\/$/, '')}/v1/publishers/google/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`;636 if (authType === 'express') {
637 // For Express mode (API key authentication), use the key parameter
638 const keyParam = authHeader.replace('Bearer ', '');
639 url = `${apiUrl.toString().replace(/\/$/, '')}/v1/publishers/google/models/${model}:${responseType}?key=${keyParam}${stream ? '&alt=sse' : ''}`;
640 } else if (authType === 'full') {
641 // For Full mode (service account authentication), use project-specific URL
642 const projectId = request.body.vertexai_project_id || 'your-project-id';
643 const region = request.body.vertexai_region || 'us-central1';
644 // Handle global region differently - no region prefix in hostname
645 if (region === 'global') {
646 url = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${responseType}${stream ? '?alt=sse' : ''}`;
647 } else {
648 url = `https://${region}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${responseType}${stream ? '?alt=sse' : ''}`;
649 }
650 headers['Authorization'] = authHeader;
651 } else {
652 // For proxy mode, use the original URL with Authorization header
653 url = `${apiUrl.toString().replace(/\/$/, '')}/v1/publishers/google/models/${model}:${responseType}${stream ? '?alt=sse' : ''}`;
654 headers['Authorization'] = authHeader;
655 }
504 } else {656 } else {
505 url = `${apiUrl.toString().replace(/\/$/, '')}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`;657 url = `${apiUrl.toString().replace(/\/$/, '')}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`;
506 }658 }
659
507 const generateResponse = await fetch(url, {660 const generateResponse = await fetch(url, {
508 body: JSON.stringify(body),661 body: JSON.stringify(body),
509 method: 'POST',662 method: 'POST',
510 headers: {663 headers: headers,
511 'Content-Type': 'application/json',
512 },
513 signal: controller.signal,664 signal: controller.signal,
514 });665 });
515666
src/endpoints/google.js+137 -13
@@ -2,6 +2,7 @@ import { Buffer } from 'node:buffer';
2import fetch from 'node-fetch';2import fetch from 'node-fetch';
3import express from 'express';3import express from 'express';
4import { speak, languages } from 'google-translate-api-x';4import { speak, languages } from 'google-translate-api-x';
5import crypto from 'node:crypto';
56
6import { readSecret, SECRET_KEYS } from './secrets.js';7import { readSecret, SECRET_KEYS } from './secrets.js';
7import { GEMINI_SAFETY } from '../constants.js';8import { GEMINI_SAFETY } from '../constants.js';
@@ -9,6 +10,108 @@ import { GEMINI_SAFETY } from '../constants.js';
9const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';10const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';
10const API_VERTEX_AI = 'https://us-central1-aiplatform.googleapis.com';11const API_VERTEX_AI = 'https://us-central1-aiplatform.googleapis.com';
1112
13// Vertex AI authentication helper functions
14async function getVertexAIAuth(request) {
15 const authMode = request.body.vertexai_auth_mode || 'express';
16
17 if (request.body.reverse_proxy) {
18 return {
19 authHeader: `Bearer ${request.body.proxy_password}`,
20 authType: 'proxy'
21 };
22 }
23
24 if (authMode === 'express') {
25 const apiKey = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI);
26 if (apiKey) {
27 return {
28 authHeader: `Bearer ${apiKey}`,
29 authType: 'express'
30 };
31 }
32 throw new Error('API key is required for Vertex AI Express mode');
33 } else if (authMode === 'full') {
34 // Try to get service account JSON from backend storage first
35 let serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT);
36
37 // If not found in backend storage, try from request body (for backward compatibility)
38 if (!serviceAccountJson) {
39 serviceAccountJson = request.body.vertexai_service_account_json;
40 }
41
42 if (serviceAccountJson) {
43 try {
44 const serviceAccount = JSON.parse(serviceAccountJson);
45 const jwtToken = await generateJWTToken(serviceAccount);
46 const accessToken = await getAccessToken(jwtToken);
47 return {
48 authHeader: `Bearer ${accessToken}`,
49 authType: 'full'
50 };
51 } catch (error) {
52 console.error('Failed to authenticate with service account:', error);
53 throw new Error(`Service account authentication failed: ${error.message}`);
54 }
55 }
56 throw new Error('Service Account JSON is required for Vertex AI Full mode');
57 }
58
59 throw new Error(`Unsupported Vertex AI authentication mode: ${authMode}`);
60}
61
62/**
63 * Generates a JWT token for Google Cloud authentication using service account credentials.
64 * @param {object} serviceAccount Service account JSON object
65 * @returns {Promise<string>} JWT token
66 */
67async function generateJWTToken(serviceAccount) {
68 const now = Math.floor(Date.now() / 1000);
69 const expiry = now + 3600; // 1 hour
70
71 const header = {
72 alg: 'RS256',
73 typ: 'JWT'
74 };
75
76 const payload = {
77 iss: serviceAccount.client_email,
78 scope: 'https://www.googleapis.com/auth/cloud-platform',
79 aud: 'https://oauth2.googleapis.com/token',
80 iat: now,
81 exp: expiry
82 };
83
84 const headerBase64 = Buffer.from(JSON.stringify(header)).toString('base64url');
85 const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
86 const signatureInput = `${headerBase64}.${payloadBase64}`;
87
88 // Create signature using private key
89 const sign = crypto.createSign('RSA-SHA256');
90 sign.update(signatureInput);
91 const signature = sign.sign(serviceAccount.private_key, 'base64url');
92
93 return `${signatureInput}.${signature}`;
94}
95
96async function getAccessToken(jwtToken) {
97 const response = await fetch('https://oauth2.googleapis.com/token', {
98 method: 'POST',
99 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
100 body: new URLSearchParams({
101 grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
102 assertion: jwtToken
103 })
104 });
105
106 if (!response.ok) {
107 const error = await response.text();
108 throw new Error(`Failed to get access token: ${error}`);
109 }
110
111 const data = await response.json();
112 return data.access_token;
113}
114
12export const router = express.Router();115export const router = express.Router();
13116
14router.post('/caption-image', async (request, response) => {117router.post('/caption-image', async (request, response) => {
@@ -17,20 +120,43 @@ router.post('/caption-image', async (request, response) => {
17 const base64Data = request.body.image.split(',')[1];120 const base64Data = request.body.image.split(',')[1];
18 const useVertexAi = request.body.api === 'vertexai';121 const useVertexAi = request.body.api === 'vertexai';
19 const apiName = useVertexAi ? 'Google Vertex AI' : 'Google AI Studio';122 const apiName = useVertexAi ? 'Google Vertex AI' : 'Google AI Studio';
20 let apiKey;
21 let apiUrl;
22 if (useVertexAi) {
23 apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.VERTEXAI);
24 apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI);
25 } else {
26 apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);
27 apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE);
28 }
29 const model = request.body.model || 'gemini-2.0-flash';123 const model = request.body.model || 'gemini-2.0-flash';
124
30 let url;125 let url;
126 let headers = {
127 'Content-Type': 'application/json',
128 };
129
31 if (useVertexAi) {130 if (useVertexAi) {
32 url = `${apiUrl.origin}/v1/publishers/google/models/${model}:generateContent?key=${apiKey}`;131 // Get authentication for Vertex AI
132 const { authHeader, authType } = await getVertexAIAuth(request);
133
134 if (authType === 'express') {
135 // Express mode: use API key parameter
136 const keyParam = authHeader.replace('Bearer ', '');
137 const apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI);
138 url = `${apiUrl.origin}/v1/publishers/google/models/${model}:generateContent?key=${keyParam}`;
139 } else if (authType === 'full') {
140 // Full mode: use project-specific URL with Authorization header
141 const projectId = request.body.vertexai_project_id || 'your-project-id';
142 const region = request.body.vertexai_region || 'us-central1';
143 // Handle global region differently - no region prefix in hostname
144 if (region === 'global') {
145 url = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:generateContent`;
146 } else {
147 url = `https://${region}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:generateContent`;
148 }
149 headers['Authorization'] = authHeader;
150 } else {
151 // Proxy mode: use Authorization header
152 const apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI);
153 url = `${apiUrl.origin}/v1/publishers/google/models/${model}:generateContent`;
154 headers['Authorization'] = authHeader;
155 }
33 } else {156 } else {
157 // Google AI Studio
158 const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);
159 const apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE);
34 url = `${apiUrl.origin}/v1beta/models/${model}:generateContent?key=${apiKey}`;160 url = `${apiUrl.origin}/v1beta/models/${model}:generateContent?key=${apiKey}`;
35 }161 }
36 const body = {162 const body = {
@@ -53,9 +179,7 @@ router.post('/caption-image', async (request, response) => {
53 const result = await fetch(url, {179 const result = await fetch(url, {
54 body: JSON.stringify(body),180 body: JSON.stringify(body),
55 method: 'POST',181 method: 'POST',
56 headers: {182 headers: headers,
57 'Content-Type': 'application/json',
58 },
59 });183 });
60184
61 if (!result.ok) {185 if (!result.ok) {
src/endpoints/secrets.js+1 -0
@@ -54,6 +54,7 @@ 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_SERVICE_ACCOUNT: 'vertexai_service_account_json',
57};58};
5859
59// These are the keys that are safe to expose, even if allowKeysExposure is false60// These are the keys that are safe to expose, even if allowKeysExposure is false