Merge pull request #4046 from InterestingDarknessII/vertexfull Add Vertex AI Full Version support
Signed| @@ -2779,7 +2779,7 @@ | ||
| 2779 | 2779 | <option value="deepseek">DeepSeek</option> |
| 2780 | 2780 | <option value="groq">Groq</option> |
| 2781 | 2781 | <option value="makersuite">Google AI Studio</option> |
| 2782 | 2782 | <option value="vertexai">Google Vertex AI (Express mode)</option> |
| 2783 | 2783 | <option value="mistralai">MistralAI</option> |
| 2784 | 2784 | <option value="nanogpt">NanoGPT</option> |
| 2785 | 2785 | <option value="openrouter">OpenRouter</option> |
| @@ -3225,12 +3225,25 @@ | ||
| 3225 | 3225 | </div> |
| 3226 | 3226 | </form> |
| 3227 | 3227 | <div id="vertexai_form" data-source="vertexai"> |
| 3228 | + <h4 data-i18n="Google Vertex AI Configuration">Google Vertex AI Configuration</h4> | |
| 3229 | + | |
| 3230 | + <!-- Authentication Mode Selection --> | |
| 3231 | + <div class="flex-container"> | |
| 3232 | + <label for="vertexai_auth_mode" data-i18n="Authentication Mode">Authentication Mode:</label> | |
| 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> | |
| 3237 | + </div> | |
| 3238 | + | |
| 3239 | + <!-- Express Mode Configuration --> | |
| 3240 | + <div id="vertexai_express_config" class="vertexai-auth-section" data-mode="express"> | |
| 3228 | 3241 | <h4> |
| 3229 | 3242 | <span data-i18n="Google Vertex AI API Key"> |
| 3230 | 3243 | Google Vertex AI API Key |
| 3231 | 3244 | </span> |
| 3232 | 3245 | <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 | 3246 | (Express mode keys only) |
| 3234 | 3247 | </a> |
| 3235 | 3248 | </h4> |
| 3236 | 3249 | <div class="flex-container"> |
| @@ -3240,17 +3253,86 @@ | ||
| 3240 | 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."> |
| 3241 | 3254 | For privacy reasons, your API key will be hidden after you reload the page. |
| 3242 | 3255 | </div> |
| 3256 | + </div> | |
| 3257 | + | |
| 3258 | + <!-- Full Version Configuration --> | |
| 3259 | + <div id="vertexai_full_config" class="vertexai-auth-section" data-mode="full"> | |
| 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 | + <!-- Region --> | |
| 3270 | + <div class="flex-container flexFlowColumn"> | |
| 3271 | + <label for="vertexai_region"> | |
| 3272 | + <span data-i18n="Region"> | |
| 3273 | + Region: | |
| 3274 | + </span> | |
| 3275 | + <a href="https://cloud.google.com/vertex-ai/generative-ai/docs/learn/locations" target="_blank" rel="noopener noreferrer" title="View available regions and models" data-i18n="[title]View available regions and models" class="notes-link"> | |
| 3276 | + <span class="fa-solid fa-circle-question note-link-span"></span> | |
| 3277 | + </a> | |
| 3278 | + </label> | |
| 3279 | + <input id="vertexai_region" name="vertexai_region" class="text_pole flex1" value="us-central1" type="text" autocomplete="off" placeholder="e.g., global, us-central1, europe-west1, asia-northeast1"> | |
| 3280 | + </div> | |
| 3281 | + | |
| 3282 | + <!-- Service Account JSON Content --> | |
| 3283 | + <div class="flex-container flexFlowColumn"> | |
| 3284 | + <label for="vertexai_service_account_json" data-i18n="Service Account JSON Content">Service Account JSON Content:</label> | |
| 3285 | + <div id="vertexai_service_account_status" class="info-block marginTopBot5" style="display: none;"> | |
| 3286 | + <span id="vertexai_service_account_info"></span> | |
| 3287 | + </div> | |
| 3288 | + <textarea id="vertexai_service_account_json" class="text_pole textarea_compact" rows="4" placeholder='Paste your Service Account JSON content here, e.g.: | |
| 3289 | +{ | |
| 3290 | + "type": "service_account", | |
| 3291 | + "project_id": "your-project-id", | |
| 3292 | + "private_key_id": "...", | |
| 3293 | + "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n", | |
| 3294 | + "client_email": "...", | |
| 3295 | + "client_id": "...", | |
| 3296 | + "auth_uri": "https://accounts.google.com/o/oauth2/auth", | |
| 3297 | + "token_uri": "https://oauth2.googleapis.com/token" | |
| 3298 | +}'></textarea> | |
| 3299 | + <div data-for="vertexai_service_account_json" class="neutral_warning" data-i18n="For privacy reasons, your Service Account JSON content will be hidden after you reload the page."> | |
| 3300 | + For privacy reasons, your Service Account JSON content will be hidden after you reload the page. | |
| 3301 | + </div> | |
| 3302 | + <div class="flex-container"> | |
| 3303 | + <button type="button" id="vertexai_validate_service_account" class="menu_button menu_button_icon" data-i18n="Validate JSON">Validate JSON</button> | |
| 3304 | + <button type="button" id="vertexai_clear_service_account" class="menu_button menu_button_icon" data-i18n="Clear">Clear</button> | |
| 3305 | + </div> | |
| 3306 | + </div> | |
| 3307 | + </div> | |
| 3308 | + | |
| 3309 | + <!-- Model Selection --> | |
| 3243 | 3310 | <div> |
| 3244 | 3311 | <h4 data-i18n="Google Model">Google Model</h4> |
| 3245 | 3312 | <select id="model_vertexai_select"> |
| 3246 | - <optgroup label="Gemini 2.5"> | |
| 3313 | + <!-- Express Mode Models --> | |
| 3314 | + <optgroup id="vertexai_express_models" label="Express Mode Models" data-mode="express"> | |
| 3247 | 3315 | <option value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option> |
| 3248 | 3316 | <option value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option> |
| 3249 | 3317 | <option value="gemini-2.5-flash-preview-05-20">gemini-2.5-flash-preview-05-20</option> |
| 3250 | 3318 | <option value="gemini-2.5-flash-preview-04-17">gemini-2.5-flash-preview-04-17</option> |
| 3319 | + <option value="gemini-2.0-flash-001">gemini-2.0-flash-001</option> | |
| 3320 | + <option value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option> | |
| 3251 | 3321 | </optgroup> |
| 3252 | - <optgroup label="Gemini 2.0"> | |
| 3322 | + | |
| 3323 | + <!-- Full Version Models --> | |
| 3324 | + <optgroup id="vertexai_full_gemini_25" label="Gemini 2.5" data-mode="full"> | |
| 3325 | + <option value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option> | |
| 3326 | + <option value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option> | |
| 3327 | + <option value="gemini-2.5-pro-exp-03-25">gemini-2.5-pro-exp-03-25</option> | |
| 3328 | + <option value="gemini-2.5-flash-preview-05-20">gemini-2.5-flash-preview-05-20</option> | |
| 3329 | + <option value="gemini-2.5-flash-preview-04-17">gemini-2.5-flash-preview-04-17</option> | |
| 3330 | + </optgroup> | |
| 3331 | + <optgroup id="vertexai_full_gemini_20" label="Gemini 2.0" data-mode="full"> | |
| 3253 | 3332 | <option value="gemini-2.0-flash-001">gemini-2.0-flash-001</option> |
| 3333 | + <option value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option> | |
| 3334 | + <option value="gemini-2.0-flash-preview-image-generation">gemini-2.0-flash-preview-image-generation</option> | |
| 3335 | + <option value="gemini-2.0-flash">gemini-2.0-flash</option> | |
| 3254 | 3336 | <option value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option> |
| 3255 | 3337 | </optgroup> |
| 3256 | 3338 | </select> |
| @@ -402,7 +402,8 @@ function RA_autoconnect(PrevApi) { | ||
| 402 | 402 | || (secret_state[SECRET_KEYS.OPENROUTER] && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) |
| 403 | 403 | || (secret_state[SECRET_KEYS.AI21] && oai_settings.chat_completion_source == chat_completion_sources.AI21) |
| 404 | 404 | || (secret_state[SECRET_KEYS.MAKERSUITE] && oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE) |
| 405 | 405 | || (secret_state[SECRET_KEYS.VERTEXAI] && oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI && oai_settings.vertexai_auth_mode === 'express') |
| 406 | + || (secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT] && oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI && oai_settings.vertexai_auth_mode === 'full') | |
| 406 | 407 | || (secret_state[SECRET_KEYS.MISTRALAI] && oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) |
| 407 | 408 | || (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE) |
| 408 | 409 | || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY) |
| @@ -56,6 +56,12 @@ export async function getMultimodalCaption(base64Img, prompt) { | ||
| 56 | 56 | model: extension_settings.caption.multimodal_model || 'gpt-4-turbo', |
| 57 | 57 | }; |
| 58 | 58 | |
| 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_region = oai_settings.vertexai_region; | |
| 63 | + } | |
| 64 | + | |
| 59 | 65 | if (isOllama) { |
| 60 | 66 | if (extension_settings.caption.multimodal_model === 'ollama_current') { |
| 61 | 67 | requestBody.model = textgenerationwebui_settings.ollama_model; |
| @@ -164,8 +170,24 @@ function throwIfInvalidModel(useReverseProxy) { | ||
| 164 | 170 | throw new Error('Google AI Studio API key is not set.'); |
| 165 | 171 | } |
| 166 | 172 | |
| 167 | 173 | if (multimodalApi === 'vertexai' && !secret_state[SECRET_KEYS.VERTEXAI] && !useReverseProxy) { |
| 168 | - throw new Error('Google Vertex AI API key is not set.'); | |
| 174 | + // Check based on authentication mode | |
| 175 | + const authMode = oai_settings.vertexai_auth_mode || 'express'; | |
| 176 | + | |
| 177 | + if (authMode === 'express') { | |
| 178 | + // Express mode requires API key | |
| 179 | + if (!secret_state[SECRET_KEYS.VERTEXAI]) { | |
| 180 | + throw new Error('Google Vertex AI API key is not set for Express mode.'); | |
| 181 | + } | |
| 182 | + } else if (authMode === 'full') { | |
| 183 | + // Full mode requires Service Account JSON and region settings | |
| 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.'); | |
| 186 | + } | |
| 187 | + if (!oai_settings.vertexai_region) { | |
| 188 | + throw new Error('Region is required for Vertex AI Full mode.'); | |
| 189 | + } | |
| 190 | + } | |
| 169 | 191 | } |
| 170 | 192 | |
| 171 | 193 | if (multimodalApi === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] && !useReverseProxy) { |
| @@ -235,6 +235,7 @@ const sensitiveFields = [ | ||
| 235 | 235 | 'custom_include_body', |
| 236 | 236 | 'custom_exclude_body', |
| 237 | 237 | 'custom_include_headers', |
| 238 | + 'vertexai_region', | |
| 238 | 239 | ]; |
| 239 | 240 | |
| 240 | 241 | /** |
| @@ -306,6 +307,8 @@ export const settingsToUpdate = { | ||
| 306 | 307 | assistant_impersonation: ['#claude_assistant_impersonation', 'assistant_impersonation', false, false], |
| 307 | 308 | claude_use_sysprompt: ['#claude_use_sysprompt', 'claude_use_sysprompt', true, false], |
| 308 | 309 | use_makersuite_sysprompt: ['#use_makersuite_sysprompt', 'use_makersuite_sysprompt', true, false], |
| 310 | + vertexai_auth_mode: ['#vertexai_auth_mode', 'vertexai_auth_mode', false, true], | |
| 311 | + vertexai_region: ['#vertexai_region', 'vertexai_region', false, true], | |
| 309 | 312 | use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true, true], |
| 310 | 313 | squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true, false], |
| 311 | 314 | image_inlining: ['#openai_image_inlining', 'image_inlining', true, false], |
| @@ -387,6 +390,8 @@ const default_settings = { | ||
| 387 | 390 | assistant_impersonation: '', |
| 388 | 391 | claude_use_sysprompt: false, |
| 389 | 392 | use_makersuite_sysprompt: true, |
| 393 | + vertexai_auth_mode: 'express', | |
| 394 | + vertexai_region: 'us-central1', | |
| 390 | 395 | use_alt_scale: false, |
| 391 | 396 | squash_system_messages: false, |
| 392 | 397 | image_inlining: false, |
| @@ -471,6 +476,8 @@ const oai_settings = { | ||
| 471 | 476 | assistant_impersonation: '', |
| 472 | 477 | claude_use_sysprompt: false, |
| 473 | 478 | use_makersuite_sysprompt: true, |
| 479 | + vertexai_auth_mode: 'express', | |
| 480 | + vertexai_region: 'us-central1', | |
| 474 | 481 | use_alt_scale: false, |
| 475 | 482 | squash_system_messages: false, |
| 476 | 483 | image_inlining: false, |
| @@ -2188,6 +2195,10 @@ async function sendOpenAIRequest(type, messages, signal) { | ||
| 2188 | 2195 | generate_data['top_k'] = Number(oai_settings.top_k_openai); |
| 2189 | 2196 | generate_data['stop'] = getCustomStoppingStrings(stopStringsLimit).slice(0, stopStringsLimit).filter(x => x.length >= 1 && x.length <= 16); |
| 2190 | 2197 | generate_data['use_makersuite_sysprompt'] = oai_settings.use_makersuite_sysprompt; |
| 2198 | + if (isVertexAI) { | |
| 2199 | + generate_data['vertexai_auth_mode'] = oai_settings.vertexai_auth_mode; | |
| 2200 | + generate_data['vertexai_region'] = oai_settings.vertexai_region; | |
| 2201 | + } | |
| 2191 | 2202 | } |
| 2192 | 2203 | |
| 2193 | 2204 | if (isMistral) { |
| @@ -3423,6 +3434,8 @@ function loadOpenAISettings(data, settings) { | ||
| 3423 | 3434 | if (settings.openai_model !== undefined) oai_settings.openai_model = settings.openai_model; |
| 3424 | 3435 | if (settings.claude_use_sysprompt !== undefined) oai_settings.claude_use_sysprompt = !!settings.claude_use_sysprompt; |
| 3425 | 3436 | if (settings.use_makersuite_sysprompt !== undefined) oai_settings.use_makersuite_sysprompt = !!settings.use_makersuite_sysprompt; |
| 3437 | + if (settings.vertexai_auth_mode !== undefined) oai_settings.vertexai_auth_mode = settings.vertexai_auth_mode; | |
| 3438 | + if (settings.vertexai_region !== undefined) oai_settings.vertexai_region = settings.vertexai_region; | |
| 3426 | 3439 | if (settings.use_alt_scale !== undefined) { oai_settings.use_alt_scale = !!settings.use_alt_scale; updateScaleForm(); } |
| 3427 | 3440 | $('#stream_toggle').prop('checked', oai_settings.stream_openai); |
| 3428 | 3441 | $('#api_url_scale').val(oai_settings.api_url_scale); |
| @@ -3478,6 +3491,11 @@ function loadOpenAISettings(data, settings) { | ||
| 3478 | 3491 | $('#openai_external_category').toggle(oai_settings.show_external_models); |
| 3479 | 3492 | $('#claude_use_sysprompt').prop('checked', oai_settings.claude_use_sysprompt); |
| 3480 | 3493 | $('#use_makersuite_sysprompt').prop('checked', oai_settings.use_makersuite_sysprompt); |
| 3494 | + $('#vertexai_auth_mode').val(oai_settings.vertexai_auth_mode); | |
| 3495 | + $('#vertexai_region').val(oai_settings.vertexai_region); | |
| 3496 | + // Don't display Service Account JSON in textarea - it's stored in backend secrets | |
| 3497 | + $('#vertexai_service_account_json').val(''); | |
| 3498 | + updateVertexAIServiceAccountStatus(); | |
| 3481 | 3499 | $('#scale-alt').prop('checked', oai_settings.use_alt_scale); |
| 3482 | 3500 | $('#openrouter_use_fallback').prop('checked', oai_settings.openrouter_use_fallback); |
| 3483 | 3501 | $('#openrouter_group_models').prop('checked', oai_settings.openrouter_group_models); |
| @@ -3800,6 +3818,8 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { | ||
| 3800 | 3818 | assistant_impersonation: settings.assistant_impersonation, |
| 3801 | 3819 | claude_use_sysprompt: settings.claude_use_sysprompt, |
| 3802 | 3820 | use_makersuite_sysprompt: settings.use_makersuite_sysprompt, |
| 3821 | + vertexai_auth_mode: settings.vertexai_auth_mode, | |
| 3822 | + vertexai_region: settings.vertexai_region, | |
| 3803 | 3823 | use_alt_scale: settings.use_alt_scale, |
| 3804 | 3824 | squash_system_messages: settings.squash_system_messages, |
| 3805 | 3825 | image_inlining: settings.image_inlining, |
| @@ -4975,6 +4995,8 @@ async function onConnectButtonClick(e) { | ||
| 4975 | 4995 | } |
| 4976 | 4996 | |
| 4977 | 4997 | if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) { |
| 4998 | + if (oai_settings.vertexai_auth_mode === 'express') { | |
| 4999 | + // Express mode - use API key | |
| 4978 | 5000 | const api_key_vertexai = String($('#api_key_vertexai').val()).trim(); |
| 4979 | 5001 | |
| 4980 | 5002 | if (api_key_vertexai.length) { |
| @@ -4982,10 +5004,20 @@ async function onConnectButtonClick(e) { | ||
| 4982 | 5004 | } |
| 4983 | 5005 | |
| 4984 | 5006 | if (!secret_state[SECRET_KEYS.VERTEXAI] && !oai_settings.reverse_proxy) { |
| 4985 | 5007 | console.log('No secret key saved for Vertex AI Express mode'); |
| 5008 | + return; | |
| 5009 | + } | |
| 5010 | + } else { | |
| 5011 | + // Full version - use service account | |
| 5012 | + // Project ID will be extracted from the Service Account JSON | |
| 5013 | + | |
| 5014 | + // Check if service account JSON is saved in backend | |
| 5015 | + if (!secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) { | |
| 5016 | + toastr.error('Service Account JSON is required for Vertex AI full version. Please validate and save your Service Account JSON.'); | |
| 4986 | 5017 | return; |
| 4987 | 5018 | } |
| 4988 | 5019 | } |
| 5020 | + } | |
| 4989 | 5021 | |
| 4990 | 5022 | if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) { |
| 4991 | 5023 | const api_key_claude = String($('#api_key_claude').val()).trim(); |
| @@ -5166,6 +5198,8 @@ function toggleChatCompletionForms() { | ||
| 5166 | 5198 | } |
| 5167 | 5199 | else if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) { |
| 5168 | 5200 | $('#model_vertexai_select').trigger('change'); |
| 5201 | + // Update UI based on authentication mode | |
| 5202 | + onVertexAIAuthModeChange.call($('#vertexai_auth_mode')[0]); | |
| 5169 | 5203 | } |
| 5170 | 5204 | else if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) { |
| 5171 | 5205 | $('#model_openrouter_select').trigger('change'); |
| @@ -5476,6 +5510,136 @@ function runProxyCallback(_, value) { | ||
| 5476 | 5510 | return foundName; |
| 5477 | 5511 | } |
| 5478 | 5512 | |
| 5513 | +/** | |
| 5514 | + * Handle Vertex AI authentication mode change | |
| 5515 | + */ | |
| 5516 | +function onVertexAIAuthModeChange() { | |
| 5517 | + const authMode = String($(this).val()); | |
| 5518 | + oai_settings.vertexai_auth_mode = authMode; | |
| 5519 | + | |
| 5520 | + $('#vertexai_form [data-mode]').each(function () { | |
| 5521 | + const mode = $(this).data('mode'); | |
| 5522 | + $(this).toggle(mode === authMode); | |
| 5523 | + $(this).find('option').toggle(mode === authMode); | |
| 5524 | + }); | |
| 5525 | + | |
| 5526 | + saveSettingsDebounced(); | |
| 5527 | +} | |
| 5528 | + | |
| 5529 | +/** | |
| 5530 | + * Validate Vertex AI service account JSON | |
| 5531 | + */ | |
| 5532 | +async function onVertexAIValidateServiceAccount() { | |
| 5533 | + const jsonContent = String($('#vertexai_service_account_json').val()).trim(); | |
| 5534 | + | |
| 5535 | + if (!jsonContent) { | |
| 5536 | + toastr.error(t`Please enter Service Account JSON content`); | |
| 5537 | + return; | |
| 5538 | + } | |
| 5539 | + | |
| 5540 | + try { | |
| 5541 | + const serviceAccount = JSON.parse(jsonContent); | |
| 5542 | + const requiredFields = ['type', 'project_id', 'private_key', 'client_email', 'client_id']; | |
| 5543 | + const missingFields = requiredFields.filter(field => !serviceAccount[field]); | |
| 5544 | + | |
| 5545 | + if (missingFields.length > 0) { | |
| 5546 | + toastr.error(t`Missing required fields: ${missingFields.join(', ')}`); | |
| 5547 | + updateVertexAIServiceAccountStatus(false, t`Missing fields: ${missingFields.join(', ')}`); | |
| 5548 | + return; | |
| 5549 | + } | |
| 5550 | + | |
| 5551 | + if (serviceAccount.type !== 'service_account') { | |
| 5552 | + toastr.error(t`Invalid service account type. Expected "service_account"`); | |
| 5553 | + updateVertexAIServiceAccountStatus(false, t`Invalid service account type`); | |
| 5554 | + return; | |
| 5555 | + } | |
| 5556 | + | |
| 5557 | + // Save to backend secret storage | |
| 5558 | + await writeSecret(SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT, jsonContent); | |
| 5559 | + | |
| 5560 | + // Show success status | |
| 5561 | + updateVertexAIServiceAccountStatus(true, `Project: ${serviceAccount.project_id}, Email: ${serviceAccount.client_email}`); | |
| 5562 | + | |
| 5563 | + toastr.success(t`Service Account JSON is valid and saved securely`); | |
| 5564 | + saveSettingsDebounced(); | |
| 5565 | + } catch (error) { | |
| 5566 | + console.error('JSON validation error:', error); | |
| 5567 | + toastr.error(t`Invalid JSON format`); | |
| 5568 | + updateVertexAIServiceAccountStatus(false, t`Invalid JSON format`); | |
| 5569 | + } | |
| 5570 | +} | |
| 5571 | + | |
| 5572 | +/** | |
| 5573 | + * Clear Vertex AI service account JSON | |
| 5574 | + */ | |
| 5575 | +async function onVertexAIClearServiceAccount() { | |
| 5576 | + $('#vertexai_service_account_json').val(''); | |
| 5577 | + | |
| 5578 | + // Clear from backend secret storage | |
| 5579 | + await writeSecret(SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT, ''); | |
| 5580 | + | |
| 5581 | + updateVertexAIServiceAccountStatus(false); | |
| 5582 | + toastr.info(t`Service Account JSON cleared`); | |
| 5583 | + saveSettingsDebounced(); | |
| 5584 | +} | |
| 5585 | + | |
| 5586 | +/** | |
| 5587 | + * Handle Vertex AI service account JSON input change | |
| 5588 | + */ | |
| 5589 | +function onVertexAIServiceAccountJsonChange() { | |
| 5590 | + const jsonContent = String($(this).val()).trim(); | |
| 5591 | + | |
| 5592 | + if (jsonContent) { | |
| 5593 | + // Auto-validate when content is pasted | |
| 5594 | + try { | |
| 5595 | + const serviceAccount = JSON.parse(jsonContent); | |
| 5596 | + const requiredFields = ['type', 'project_id', 'private_key', 'client_email']; | |
| 5597 | + const hasAllFields = requiredFields.every(field => serviceAccount[field]); | |
| 5598 | + | |
| 5599 | + if (hasAllFields && serviceAccount.type === 'service_account') { | |
| 5600 | + updateVertexAIServiceAccountStatus(false, t`JSON appears valid - click "Validate JSON" to save`); | |
| 5601 | + } else { | |
| 5602 | + updateVertexAIServiceAccountStatus(false, t`Incomplete or invalid JSON`); | |
| 5603 | + } | |
| 5604 | + } catch (error) { | |
| 5605 | + updateVertexAIServiceAccountStatus(false, t`Invalid JSON format`); | |
| 5606 | + } | |
| 5607 | + } else { | |
| 5608 | + updateVertexAIServiceAccountStatus(false); | |
| 5609 | + } | |
| 5610 | + | |
| 5611 | + // Don't save settings automatically | |
| 5612 | + // saveSettingsDebounced(); | |
| 5613 | +} | |
| 5614 | + | |
| 5615 | +/** | |
| 5616 | + * Update the Vertex AI service account status display | |
| 5617 | + * @param {boolean} isValid - Whether the service account is valid | |
| 5618 | + * @param {string} message - Status message to display | |
| 5619 | + */ | |
| 5620 | +function updateVertexAIServiceAccountStatus(isValid = false, message = '') { | |
| 5621 | + const statusDiv = $('#vertexai_service_account_status'); | |
| 5622 | + const infoSpan = $('#vertexai_service_account_info'); | |
| 5623 | + | |
| 5624 | + // If no explicit message provided, check if we have a saved service account | |
| 5625 | + if (!message && secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) { | |
| 5626 | + isValid = true; | |
| 5627 | + message = t`Service Account JSON is saved and ready to use`; | |
| 5628 | + } | |
| 5629 | + | |
| 5630 | + if (isValid && message) { | |
| 5631 | + infoSpan.html(`<i class="fa-solid fa-check-circle" style="color: green;"></i> ${message}`); | |
| 5632 | + statusDiv.show(); | |
| 5633 | + } else if (!isValid && message) { | |
| 5634 | + infoSpan.html(`<i class="fa-solid fa-exclamation-triangle" style="color: orange;"></i> ${message}`); | |
| 5635 | + statusDiv.show(); | |
| 5636 | + } else { | |
| 5637 | + statusDiv.hide(); | |
| 5638 | + } | |
| 5639 | +} | |
| 5640 | + | |
| 5641 | + | |
| 5642 | + | |
| 5479 | 5643 | export function initOpenAI() { |
| 5480 | 5644 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 5481 | 5645 | name: 'proxy', |
| @@ -5939,6 +6103,14 @@ export function initOpenAI() { | ||
| 5939 | 6103 | $('#model_scale_select').on('change', onModelChange); |
| 5940 | 6104 | $('#model_google_select').on('change', onModelChange); |
| 5941 | 6105 | $('#model_vertexai_select').on('change', onModelChange); |
| 6106 | + $('#vertexai_auth_mode').on('change', onVertexAIAuthModeChange); | |
| 6107 | + $('#vertexai_region').on('input', function () { | |
| 6108 | + oai_settings.vertexai_region = String($(this).val()); | |
| 6109 | + saveSettingsDebounced(); | |
| 6110 | + }); | |
| 6111 | + $('#vertexai_service_account_json').on('input', onVertexAIServiceAccountJsonChange); | |
| 6112 | + $('#vertexai_validate_service_account').on('click', onVertexAIValidateServiceAccount); | |
| 6113 | + $('#vertexai_clear_service_account').on('click', onVertexAIClearServiceAccount); | |
| 5942 | 6114 | $('#model_openrouter_select').on('change', onModelChange); |
| 5943 | 6115 | $('#openrouter_group_models').on('change', onOpenrouterModelSortChange); |
| 5944 | 6116 | $('#openrouter_sort_models').on('change', onOpenrouterModelSortChange); |
| @@ -44,6 +44,7 @@ export const SECRET_KEYS = { | ||
| 44 | 44 | SERPER: 'api_key_serper', |
| 45 | 45 | FALAI: 'api_key_falai', |
| 46 | 46 | XAI: 'api_key_xai', |
| 47 | + VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json', | |
| 47 | 48 | }; |
| 48 | 49 | |
| 49 | 50 | const INPUT_MAP = { |
| @@ -80,8 +81,13 @@ const INPUT_MAP = { | ||
| 80 | 81 | [SECRET_KEYS.GENERIC]: '#api_key_generic', |
| 81 | 82 | [SECRET_KEYS.DEEPSEEK]: '#api_key_deepseek', |
| 82 | 83 | [SECRET_KEYS.XAI]: '#api_key_xai', |
| 84 | + [SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]: '#vertexai_service_account_json', | |
| 83 | 85 | }; |
| 84 | 86 | |
| 87 | +const STATIC_PLACEHOLDER_KEYS = [ | |
| 88 | + SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT, | |
| 89 | +]; | |
| 90 | + | |
| 85 | 91 | async function clearSecret() { |
| 86 | 92 | const key = $(this).data('key'); |
| 87 | 93 | await writeSecret(key, ''); |
| @@ -93,6 +99,9 @@ async function clearSecret() { | ||
| 93 | 99 | |
| 94 | 100 | export function updateSecretDisplay() { |
| 95 | 101 | for (const [secret_key, input_selector] of Object.entries(INPUT_MAP)) { |
| 102 | + if (STATIC_PLACEHOLDER_KEYS.includes(secret_key)) { | |
| 103 | + continue; | |
| 104 | + } | |
| 96 | 105 | const validSecret = !!secret_state[secret_key]; |
| 97 | 106 | |
| 98 | 107 | const placeholder = $('#viewSecrets').attr(validSecret ? 'key_saved_text' : 'missing_key_text'); |
| @@ -43,6 +43,7 @@ import { | ||
| 43 | 43 | webTokenizers, |
| 44 | 44 | getWebTokenizer, |
| 45 | 45 | } from '../tokenizers.js'; |
| 46 | +import { getVertexAIAuth, getProjectIdFromServiceAccount } from '../google.js'; | |
| 46 | 47 | |
| 47 | 48 | const API_OPENAI = 'https://api.openai.com/v1'; |
| 48 | 49 | const API_CLAUDE = 'https://api.anthropic.com/v1'; |
| @@ -348,13 +349,20 @@ async function sendMakerSuiteRequest(request, response) { | ||
| 348 | 349 | let apiUrl; |
| 349 | 350 | let apiKey; |
| 350 | 351 | |
| 352 | + let authHeader; | |
| 353 | + let authType; | |
| 354 | + | |
| 351 | 355 | if (useVertexAi) { |
| 352 | 356 | 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); | |
| 354 | 357 | |
| 355 | - if (!request.body.reverse_proxy && !apiKey) { | |
| 358 | + try { | |
| 356 | - console.warn(`${apiName} API key is missing.`); | |
| 359 | + const auth = await getVertexAIAuth(request); | |
| 357 | - return response.status(400).send({ error: true }); | |
| 360 | + authHeader = auth.authHeader; | |
| 361 | + authType = auth.authType; | |
| 362 | + console.debug(`Using Vertex AI authentication type: ${authType}`); | |
| 363 | + } catch (error) { | |
| 364 | + console.warn(`${apiName} authentication failed: ${error.message}`); | |
| 365 | + return response.status(400).send({ error: true, message: error.message }); | |
| 358 | 366 | } |
| 359 | 367 | } else { |
| 360 | 368 | apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE); |
| @@ -364,6 +372,9 @@ async function sendMakerSuiteRequest(request, response) { | ||
| 364 | 372 | console.warn(`${apiName} API key is missing.`); |
| 365 | 373 | return response.status(400).send({ error: true }); |
| 366 | 374 | } |
| 375 | + | |
| 376 | + authHeader = `Bearer ${apiKey}`; | |
| 377 | + authType = 'api_key'; | |
| 367 | 378 | } |
| 368 | 379 | |
| 369 | 380 | const model = String(request.body.model); |
| @@ -391,6 +402,7 @@ async function sendMakerSuiteRequest(request, response) { | ||
| 391 | 402 | const imageGenerationModels = [ |
| 392 | 403 | 'gemini-2.0-flash-exp', |
| 393 | 404 | 'gemini-2.0-flash-exp-image-generation', |
| 405 | + 'gemini-2.0-flash-preview-image-generation', | |
| 394 | 406 | ]; |
| 395 | 407 | |
| 396 | 408 | // These models do not support setting the threshold to OFF at all. |
| @@ -499,17 +511,53 @@ async function sendMakerSuiteRequest(request, response) { | ||
| 499 | 511 | const responseType = (stream ? 'streamGenerateContent' : 'generateContent'); |
| 500 | 512 | |
| 501 | 513 | let url; |
| 514 | + let headers = { | |
| 515 | + 'Content-Type': 'application/json', | |
| 516 | + }; | |
| 517 | + | |
| 502 | 518 | if (useVertexAi) { |
| 503 | - url = `${apiUrl.toString().replace(/\/$/, '')}/v1/publishers/google/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`; | |
| 519 | + if (authType === 'express') { | |
| 520 | + // For Express mode (API key authentication), use the key parameter | |
| 521 | + const keyParam = authHeader.replace('Bearer ', ''); | |
| 522 | + url = `${apiUrl.toString().replace(/\/$/, '')}/v1/publishers/google/models/${model}:${responseType}?key=${keyParam}${stream ? '&alt=sse' : ''}`; | |
| 523 | + } else if (authType === 'full') { | |
| 524 | + // For Full mode (service account authentication), use project-specific URL | |
| 525 | + // Get project ID from Service Account JSON | |
| 526 | + const serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT); | |
| 527 | + if (!serviceAccountJson) { | |
| 528 | + console.warn('Vertex AI Service Account JSON is missing.'); | |
| 529 | + return response.status(400).send({ error: true }); | |
| 530 | + } | |
| 531 | + | |
| 532 | + let projectId; | |
| 533 | + try { | |
| 534 | + const serviceAccount = JSON.parse(serviceAccountJson); | |
| 535 | + projectId = getProjectIdFromServiceAccount(serviceAccount); | |
| 536 | + } catch (error) { | |
| 537 | + console.error('Failed to extract project ID from Service Account JSON:', error); | |
| 538 | + return response.status(400).send({ error: true }); | |
| 539 | + } | |
| 540 | + const region = request.body.vertexai_region || 'us-central1'; | |
| 541 | + // Handle global region differently - no region prefix in hostname | |
| 542 | + if (region === 'global') { | |
| 543 | + url = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${responseType}${stream ? '?alt=sse' : ''}`; | |
| 544 | + } else { | |
| 545 | + url = `https://${region}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${responseType}${stream ? '?alt=sse' : ''}`; | |
| 546 | + } | |
| 547 | + headers['Authorization'] = authHeader; | |
| 548 | + } else { | |
| 549 | + // For proxy mode, use the original URL with Authorization header | |
| 550 | + url = `${apiUrl.toString().replace(/\/$/, '')}/v1/publishers/google/models/${model}:${responseType}${stream ? '?alt=sse' : ''}`; | |
| 551 | + headers['Authorization'] = authHeader; | |
| 552 | + } | |
| 504 | 553 | } else { |
| 505 | 554 | url = `${apiUrl.toString().replace(/\/$/, '')}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`; |
| 506 | 555 | } |
| 556 | + | |
| 507 | 557 | const generateResponse = await fetch(url, { |
| 508 | 558 | body: JSON.stringify(body), |
| 509 | 559 | method: 'POST', |
| 510 | 560 | headers: {headers, |
| 511 | - 'Content-Type': 'application/json', | |
| 512 | - }, | |
| 513 | 561 | signal: controller.signal, |
| 514 | 562 | }); |
| 515 | 563 | |
| @@ -2,6 +2,7 @@ import { Buffer } from 'node:buffer'; | ||
| 2 | 2 | import fetch from 'node-fetch'; |
| 3 | 3 | import express from 'express'; |
| 4 | 4 | import { speak, languages } from 'google-translate-api-x'; |
| 5 | +import crypto from 'node:crypto'; | |
| 5 | 6 | |
| 6 | 7 | import { readSecret, SECRET_KEYS } from './secrets.js'; |
| 7 | 8 | import { GEMINI_SAFETY } from '../constants.js'; |
| @@ -9,6 +10,122 @@ import { GEMINI_SAFETY } from '../constants.js'; | ||
| 9 | 10 | const API_MAKERSUITE = 'https://generativelanguage.googleapis.com'; |
| 10 | 11 | const API_VERTEX_AI = 'https://us-central1-aiplatform.googleapis.com'; |
| 11 | 12 | |
| 13 | +// Vertex AI authentication helper functions | |
| 14 | +export async 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 | + // Get service account JSON from backend storage | |
| 35 | + const serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT); | |
| 36 | + | |
| 37 | + if (serviceAccountJson) { | |
| 38 | + try { | |
| 39 | + const serviceAccount = JSON.parse(serviceAccountJson); | |
| 40 | + const jwtToken = await generateJWTToken(serviceAccount); | |
| 41 | + const accessToken = await getAccessToken(jwtToken); | |
| 42 | + return { | |
| 43 | + authHeader: `Bearer ${accessToken}`, | |
| 44 | + authType: 'full', | |
| 45 | + }; | |
| 46 | + } catch (error) { | |
| 47 | + console.error('Failed to authenticate with service account:', error); | |
| 48 | + throw new Error(`Service account authentication failed: ${error.message}`); | |
| 49 | + } | |
| 50 | + } | |
| 51 | + throw new Error('Service Account JSON is required for Vertex AI Full mode'); | |
| 52 | + } | |
| 53 | + | |
| 54 | + throw new Error(`Unsupported Vertex AI authentication mode: ${authMode}`); | |
| 55 | +} | |
| 56 | + | |
| 57 | +/** | |
| 58 | + * Generates a JWT token for Google Cloud authentication using service account credentials. | |
| 59 | + * @param {object} serviceAccount Service account JSON object | |
| 60 | + * @returns {Promise<string>} JWT token | |
| 61 | + */ | |
| 62 | +export async function generateJWTToken(serviceAccount) { | |
| 63 | + const now = Math.floor(Date.now() / 1000); | |
| 64 | + const expiry = now + 3600; // 1 hour | |
| 65 | + | |
| 66 | + const header = { | |
| 67 | + alg: 'RS256', | |
| 68 | + typ: 'JWT', | |
| 69 | + }; | |
| 70 | + | |
| 71 | + const payload = { | |
| 72 | + iss: serviceAccount.client_email, | |
| 73 | + scope: 'https://www.googleapis.com/auth/cloud-platform', | |
| 74 | + aud: 'https://oauth2.googleapis.com/token', | |
| 75 | + iat: now, | |
| 76 | + exp: expiry, | |
| 77 | + }; | |
| 78 | + | |
| 79 | + const headerBase64 = Buffer.from(JSON.stringify(header)).toString('base64url'); | |
| 80 | + const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString('base64url'); | |
| 81 | + const signatureInput = `${headerBase64}.${payloadBase64}`; | |
| 82 | + | |
| 83 | + // Create signature using private key | |
| 84 | + const sign = crypto.createSign('RSA-SHA256'); | |
| 85 | + sign.update(signatureInput); | |
| 86 | + const signature = sign.sign(serviceAccount.private_key, 'base64url'); | |
| 87 | + | |
| 88 | + return `${signatureInput}.${signature}`; | |
| 89 | +} | |
| 90 | + | |
| 91 | +export async function getAccessToken(jwtToken) { | |
| 92 | + const response = await fetch('https://oauth2.googleapis.com/token', { | |
| 93 | + method: 'POST', | |
| 94 | + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | |
| 95 | + body: new URLSearchParams({ | |
| 96 | + grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', | |
| 97 | + assertion: jwtToken, | |
| 98 | + }), | |
| 99 | + }); | |
| 100 | + | |
| 101 | + if (!response.ok) { | |
| 102 | + const error = await response.text(); | |
| 103 | + throw new Error(`Failed to get access token: ${error}`); | |
| 104 | + } | |
| 105 | + | |
| 106 | + const data = await response.json(); | |
| 107 | + return data.access_token; | |
| 108 | +} | |
| 109 | + | |
| 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 | + */ | |
| 116 | +export 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 | + | |
| 12 | 129 | export const router = express.Router(); |
| 13 | 130 | |
| 14 | 131 | router.post('/caption-image', async (request, response) => { |
| @@ -17,20 +134,57 @@ router.post('/caption-image', async (request, response) => { | ||
| 17 | 134 | const base64Data = request.body.image.split(',')[1]; |
| 18 | 135 | const useVertexAi = request.body.api === 'vertexai'; |
| 19 | 136 | 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 | 137 | const model = request.body.model || 'gemini-2.0-flash'; |
| 138 | + | |
| 30 | 139 | let url; |
| 140 | + let headers = { | |
| 141 | + 'Content-Type': 'application/json', | |
| 142 | + }; | |
| 143 | + | |
| 31 | 144 | if (useVertexAi) { |
| 32 | - url = `${apiUrl.origin}/v1/publishers/google/models/${model}:generateContent?key=${apiKey}`; | |
| 145 | + // Get authentication for Vertex AI | |
| 146 | + const { authHeader, authType } = await getVertexAIAuth(request); | |
| 147 | + | |
| 148 | + if (authType === 'express') { | |
| 149 | + // Express mode: use API key parameter | |
| 150 | + const keyParam = authHeader.replace('Bearer ', ''); | |
| 151 | + const apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI); | |
| 152 | + url = `${apiUrl.origin}/v1/publishers/google/models/${model}:generateContent?key=${keyParam}`; | |
| 153 | + } else if (authType === 'full') { | |
| 154 | + // Full mode: use project-specific URL with Authorization header | |
| 155 | + // Get project ID from Service Account JSON | |
| 156 | + const serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT); | |
| 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); | |
| 168 | + return response.status(400).send({ error: true }); | |
| 169 | + } | |
| 170 | + const region = request.body.vertexai_region || 'us-central1'; | |
| 171 | + // Handle global region differently - no region prefix in hostname | |
| 172 | + if (region === 'global') { | |
| 173 | + url = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:generateContent`; | |
| 174 | + } else { | |
| 175 | + url = `https://${region}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:generateContent`; | |
| 176 | + } | |
| 177 | + headers['Authorization'] = authHeader; | |
| 33 | 178 | } else { |
| 179 | + // Proxy mode: use Authorization header | |
| 180 | + const apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI); | |
| 181 | + url = `${apiUrl.origin}/v1/publishers/google/models/${model}:generateContent`; | |
| 182 | + headers['Authorization'] = authHeader; | |
| 183 | + } | |
| 184 | + } else { | |
| 185 | + // Google AI Studio | |
| 186 | + const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE); | |
| 187 | + const apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE); | |
| 34 | 188 | url = `${apiUrl.origin}/v1beta/models/${model}:generateContent?key=${apiKey}`; |
| 35 | 189 | } |
| 36 | 190 | const body = { |
| @@ -53,9 +207,7 @@ router.post('/caption-image', async (request, response) => { | ||
| 53 | 207 | const result = await fetch(url, { |
| 54 | 208 | body: JSON.stringify(body), |
| 55 | 209 | method: 'POST', |
| 56 | 210 | headers: {headers, |
| 57 | - 'Content-Type': 'application/json', | |
| 58 | - }, | |
| 59 | 211 | }); |
| 60 | 212 | |
| 61 | 213 | if (!result.ok) { |
| @@ -54,6 +54,7 @@ export const SECRET_KEYS = { | ||
| 54 | 54 | DEEPSEEK: 'api_key_deepseek', |
| 55 | 55 | SERPER: 'api_key_serper', |
| 56 | 56 | XAI: 'api_key_xai', |
| 57 | + VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json', | |
| 57 | 58 | }; |
| 58 | 59 | |
| 59 | 60 | // These are the keys that are safe to expose, even if allowKeysExposure is false |