Merge pull request #4046 from InterestingDarknessII/vertexfull Add Vertex AI Full Version support

b61269445d730662e9af109e12f6655b9294a16b

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
8 files changed, +517 -30Showing whitespace changes
public/index.html+87 -5
@@ -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,12 +3225,25 @@
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 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 <h4>3241 <h4>
3229 <span data-i18n="Google Vertex AI API Key">3242 <span data-i18n="Google Vertex AI API Key">
3230 Google Vertex AI API Key3243 Google Vertex AI API Key
3231 </span>3244 </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">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">
3233 (Express mode keys only)3246 (Express mode)
3234 </a>3247 </a>
3235 </h4>3248 </h4>
3236 <div class="flex-container">3249 <div class="flex-container">
@@ -3240,17 +3253,86 @@
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.">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 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.
3242 </div>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 <div>3310 <div>
3244 <h4 data-i18n="Google Model">Google Model</h4>3311 <h4 data-i18n="Google Model">Google Model</h4>
3245 <select id="model_vertexai_select">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 <option value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option>3315 <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>3316 <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>3317 <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>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 </optgroup>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 <option value="gemini-2.0-flash-001">gemini-2.0-flash-001</option>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 <option value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option>3336 <option value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option>
3255 </optgroup>3337 </optgroup>
3256 </select>3338 </select>
public/scripts/RossAscends-mods.js+2 -1
@@ -402,7 +402,8 @@ function RA_autoconnect(PrevApi) {
402 || (secret_state[SECRET_KEYS.OPENROUTER] && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER)402 || (secret_state[SECRET_KEYS.OPENROUTER] && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER)
403 || (secret_state[SECRET_KEYS.AI21] && oai_settings.chat_completion_source == chat_completion_sources.AI21)403 || (secret_state[SECRET_KEYS.AI21] && oai_settings.chat_completion_source == chat_completion_sources.AI21)
404 || (secret_state[SECRET_KEYS.MAKERSUITE] && oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE)404 || (secret_state[SECRET_KEYS.MAKERSUITE] && oai_settings.chat_completion_source == chat_completion_sources.MAKERSUITE)
405 || (secret_state[SECRET_KEYS.VERTEXAI] && oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI)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 || (secret_state[SECRET_KEYS.MISTRALAI] && oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI)407 || (secret_state[SECRET_KEYS.MISTRALAI] && oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI)
407 || (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE)408 || (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE)
408 || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)409 || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)
public/scripts/extensions/shared.js+24 -2
@@ -56,6 +56,12 @@ 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_region = oai_settings.vertexai_region;
63 }
64
59 if (isOllama) {65 if (isOllama) {
60 if (extension_settings.caption.multimodal_model === 'ollama_current') {66 if (extension_settings.caption.multimodal_model === 'ollama_current') {
61 requestBody.model = textgenerationwebui_settings.ollama_model;67 requestBody.model = textgenerationwebui_settings.ollama_model;
@@ -164,8 +170,24 @@ function throwIfInvalidModel(useReverseProxy) {
164 throw new Error('Google AI Studio API key is not set.');170 throw new Error('Google AI Studio API key is not set.');
165 }171 }
166172
167 if (multimodalApi === 'vertexai' && !secret_state[SECRET_KEYS.VERTEXAI] && !useReverseProxy) {173 if (multimodalApi === '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 }
170192
171 if (multimodalApi === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] && !useReverseProxy) {193 if (multimodalApi === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] && !useReverseProxy) {
public/scripts/openai.js+173 -1
@@ -235,6 +235,7 @@ const sensitiveFields = [
235 'custom_include_body',235 'custom_include_body',
236 'custom_exclude_body',236 'custom_exclude_body',
237 'custom_include_headers',237 'custom_include_headers',
238 'vertexai_region',
238];239];
239240
240/**241/**
@@ -306,6 +307,8 @@ export const settingsToUpdate = {
306 assistant_impersonation: ['#claude_assistant_impersonation', 'assistant_impersonation', false, false],307 assistant_impersonation: ['#claude_assistant_impersonation', 'assistant_impersonation', false, false],
307 claude_use_sysprompt: ['#claude_use_sysprompt', 'claude_use_sysprompt', true, false],308 claude_use_sysprompt: ['#claude_use_sysprompt', 'claude_use_sysprompt', true, false],
308 use_makersuite_sysprompt: ['#use_makersuite_sysprompt', 'use_makersuite_sysprompt', true, false],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 use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true, true],312 use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true, true],
310 squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true, false],313 squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true, false],
311 image_inlining: ['#openai_image_inlining', 'image_inlining', true, false],314 image_inlining: ['#openai_image_inlining', 'image_inlining', true, false],
@@ -387,6 +390,8 @@ const default_settings = {
387 assistant_impersonation: '',390 assistant_impersonation: '',
388 claude_use_sysprompt: false,391 claude_use_sysprompt: false,
389 use_makersuite_sysprompt: true,392 use_makersuite_sysprompt: true,
393 vertexai_auth_mode: 'express',
394 vertexai_region: 'us-central1',
390 use_alt_scale: false,395 use_alt_scale: false,
391 squash_system_messages: false,396 squash_system_messages: false,
392 image_inlining: false,397 image_inlining: false,
@@ -471,6 +476,8 @@ const oai_settings = {
471 assistant_impersonation: '',476 assistant_impersonation: '',
472 claude_use_sysprompt: false,477 claude_use_sysprompt: false,
473 use_makersuite_sysprompt: true,478 use_makersuite_sysprompt: true,
479 vertexai_auth_mode: 'express',
480 vertexai_region: 'us-central1',
474 use_alt_scale: false,481 use_alt_scale: false,
475 squash_system_messages: false,482 squash_system_messages: false,
476 image_inlining: false,483 image_inlining: false,
@@ -2188,6 +2195,10 @@ async function sendOpenAIRequest(type, messages, signal) {
2188 generate_data['top_k'] = Number(oai_settings.top_k_openai);2195 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);2196 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;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 }
21922203
2193 if (isMistral) {2204 if (isMistral) {
@@ -3423,6 +3434,8 @@ function loadOpenAISettings(data, settings) {
3423 if (settings.openai_model !== undefined) oai_settings.openai_model = settings.openai_model;3434 if (settings.openai_model !== undefined) oai_settings.openai_model = settings.openai_model;
3424 if (settings.claude_use_sysprompt !== undefined) oai_settings.claude_use_sysprompt = !!settings.claude_use_sysprompt;3435 if (settings.claude_use_sysprompt !== undefined) oai_settings.claude_use_sysprompt = !!settings.claude_use_sysprompt;
3425 if (settings.use_makersuite_sysprompt !== undefined) oai_settings.use_makersuite_sysprompt = !!settings.use_makersuite_sysprompt;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 if (settings.use_alt_scale !== undefined) { oai_settings.use_alt_scale = !!settings.use_alt_scale; updateScaleForm(); }3439 if (settings.use_alt_scale !== undefined) { oai_settings.use_alt_scale = !!settings.use_alt_scale; updateScaleForm(); }
3427 $('#stream_toggle').prop('checked', oai_settings.stream_openai);3440 $('#stream_toggle').prop('checked', oai_settings.stream_openai);
3428 $('#api_url_scale').val(oai_settings.api_url_scale);3441 $('#api_url_scale').val(oai_settings.api_url_scale);
@@ -3478,6 +3491,11 @@ function loadOpenAISettings(data, settings) {
3478 $('#openai_external_category').toggle(oai_settings.show_external_models);3491 $('#openai_external_category').toggle(oai_settings.show_external_models);
3479 $('#claude_use_sysprompt').prop('checked', oai_settings.claude_use_sysprompt);3492 $('#claude_use_sysprompt').prop('checked', oai_settings.claude_use_sysprompt);
3480 $('#use_makersuite_sysprompt').prop('checked', oai_settings.use_makersuite_sysprompt);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 $('#scale-alt').prop('checked', oai_settings.use_alt_scale);3499 $('#scale-alt').prop('checked', oai_settings.use_alt_scale);
3482 $('#openrouter_use_fallback').prop('checked', oai_settings.openrouter_use_fallback);3500 $('#openrouter_use_fallback').prop('checked', oai_settings.openrouter_use_fallback);
3483 $('#openrouter_group_models').prop('checked', oai_settings.openrouter_group_models);3501 $('#openrouter_group_models').prop('checked', oai_settings.openrouter_group_models);
@@ -3800,6 +3818,8 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
3800 assistant_impersonation: settings.assistant_impersonation,3818 assistant_impersonation: settings.assistant_impersonation,
3801 claude_use_sysprompt: settings.claude_use_sysprompt,3819 claude_use_sysprompt: settings.claude_use_sysprompt,
3802 use_makersuite_sysprompt: settings.use_makersuite_sysprompt,3820 use_makersuite_sysprompt: settings.use_makersuite_sysprompt,
3821 vertexai_auth_mode: settings.vertexai_auth_mode,
3822 vertexai_region: settings.vertexai_region,
3803 use_alt_scale: settings.use_alt_scale,3823 use_alt_scale: settings.use_alt_scale,
3804 squash_system_messages: settings.squash_system_messages,3824 squash_system_messages: settings.squash_system_messages,
3805 image_inlining: settings.image_inlining,3825 image_inlining: settings.image_inlining,
@@ -4975,6 +4995,8 @@ async function onConnectButtonClick(e) {
4975 }4995 }
49764996
4977 if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) {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 const api_key_vertexai = String($('#api_key_vertexai').val()).trim();5000 const api_key_vertexai = String($('#api_key_vertexai').val()).trim();
49795001
4980 if (api_key_vertexai.length) {5002 if (api_key_vertexai.length) {
@@ -4982,10 +5004,20 @@ async function onConnectButtonClick(e) {
4982 }5004 }
49835005
4984 if (!secret_state[SECRET_KEYS.VERTEXAI] && !oai_settings.reverse_proxy) {5006 if (!secret_state[SECRET_KEYS.VERTEXAI] && !oai_settings.reverse_proxy) {
4985 console.log('No secret key saved for Vertex AI');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 return;5017 return;
4987 }5018 }
4988 }5019 }
5020 }
49895021
4990 if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {5022 if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
4991 const api_key_claude = String($('#api_key_claude').val()).trim();5023 const api_key_claude = String($('#api_key_claude').val()).trim();
@@ -5166,6 +5198,8 @@ function toggleChatCompletionForms() {
5166 }5198 }
5167 else if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) {5199 else if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) {
5168 $('#model_vertexai_select').trigger('change');5200 $('#model_vertexai_select').trigger('change');
5201 // Update UI based on authentication mode
5202 onVertexAIAuthModeChange.call($('#vertexai_auth_mode')[0]);
5169 }5203 }
5170 else if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) {5204 else if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) {
5171 $('#model_openrouter_select').trigger('change');5205 $('#model_openrouter_select').trigger('change');
@@ -5476,6 +5510,136 @@ function runProxyCallback(_, value) {
5476 return foundName;5510 return foundName;
5477}5511}
54785512
5513/**
5514 * Handle Vertex AI authentication mode change
5515 */
5516function 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 */
5532async 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 */
5575async 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 */
5589function 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 */
5620function 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
5479export function initOpenAI() {5643export function initOpenAI() {
5480 SlashCommandParser.addCommandObject(SlashCommand.fromProps({5644 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
5481 name: 'proxy',5645 name: 'proxy',
@@ -5939,6 +6103,14 @@ export function initOpenAI() {
5939 $('#model_scale_select').on('change', onModelChange);6103 $('#model_scale_select').on('change', onModelChange);
5940 $('#model_google_select').on('change', onModelChange);6104 $('#model_google_select').on('change', onModelChange);
5941 $('#model_vertexai_select').on('change', onModelChange);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 $('#model_openrouter_select').on('change', onModelChange);6114 $('#model_openrouter_select').on('change', onModelChange);
5943 $('#openrouter_group_models').on('change', onOpenrouterModelSortChange);6115 $('#openrouter_group_models').on('change', onOpenrouterModelSortChange);
5944 $('#openrouter_sort_models').on('change', onOpenrouterModelSortChange);6116 $('#openrouter_sort_models').on('change', onOpenrouterModelSortChange);
public/scripts/secrets.js+9 -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 = {
@@ -80,8 +81,13 @@ const INPUT_MAP = {
80 [SECRET_KEYS.GENERIC]: '#api_key_generic',81 [SECRET_KEYS.GENERIC]: '#api_key_generic',
81 [SECRET_KEYS.DEEPSEEK]: '#api_key_deepseek',82 [SECRET_KEYS.DEEPSEEK]: '#api_key_deepseek',
82 [SECRET_KEYS.XAI]: '#api_key_xai',83 [SECRET_KEYS.XAI]: '#api_key_xai',
84 [SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]: '#vertexai_service_account_json',
83};85};
8486
87const STATIC_PLACEHOLDER_KEYS = [
88 SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT,
89];
90
85async function clearSecret() {91async function clearSecret() {
86 const key = $(this).data('key');92 const key = $(this).data('key');
87 await writeSecret(key, '');93 await writeSecret(key, '');
@@ -93,6 +99,9 @@ async function clearSecret() {
9399
94export function updateSecretDisplay() {100export function updateSecretDisplay() {
95 for (const [secret_key, input_selector] of Object.entries(INPUT_MAP)) {101 for (const [secret_key, input_selector] of Object.entries(INPUT_MAP)) {
102 if (STATIC_PLACEHOLDER_KEYS.includes(secret_key)) {
103 continue;
104 }
96 const validSecret = !!secret_state[secret_key];105 const validSecret = !!secret_state[secret_key];
97106
98 const placeholder = $('#viewSecrets').attr(validSecret ? 'key_saved_text' : 'missing_key_text');107 const placeholder = $('#viewSecrets').attr(validSecret ? 'key_saved_text' : 'missing_key_text');
src/endpoints/backends/chat-completions.js+56 -8
@@ -43,6 +43,7 @@ import {
43 webTokenizers,43 webTokenizers,
44 getWebTokenizer,44 getWebTokenizer,
45} from '../tokenizers.js';45} from '../tokenizers.js';
46import { getVertexAIAuth, getProjectIdFromServiceAccount } from '../google.js';
4647
47const API_OPENAI = 'https://api.openai.com/v1';48const API_OPENAI = 'https://api.openai.com/v1';
48const API_CLAUDE = 'https://api.anthropic.com/v1';49const API_CLAUDE = 'https://api.anthropic.com/v1';
@@ -348,13 +349,20 @@ async function sendMakerSuiteRequest(request, response) {
348 let apiUrl;349 let apiUrl;
349 let apiKey;350 let apiKey;
350351
352 let authHeader;
353 let authType;
354
351 if (useVertexAi) {355 if (useVertexAi) {
352 apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI);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);
354357
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 } else {367 } else {
360 apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE);368 apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE);
@@ -364,6 +372,9 @@ async function sendMakerSuiteRequest(request, response) {
364 console.warn(`${apiName} API key is missing.`);372 console.warn(`${apiName} API key is missing.`);
365 return response.status(400).send({ error: true });373 return response.status(400).send({ error: true });
366 }374 }
375
376 authHeader = `Bearer ${apiKey}`;
377 authType = 'api_key';
367 }378 }
368379
369 const model = String(request.body.model);380 const model = String(request.body.model);
@@ -391,6 +402,7 @@ async function sendMakerSuiteRequest(request, response) {
391 const imageGenerationModels = [402 const imageGenerationModels = [
392 'gemini-2.0-flash-exp',403 'gemini-2.0-flash-exp',
393 'gemini-2.0-flash-exp-image-generation',404 'gemini-2.0-flash-exp-image-generation',
405 'gemini-2.0-flash-preview-image-generation',
394 ];406 ];
395407
396 // These models do not support setting the threshold to OFF at all.408 // These models do not support setting the threshold to OFF at all.
@@ -499,17 +511,53 @@ async function sendMakerSuiteRequest(request, response) {
499 const responseType = (stream ? 'streamGenerateContent' : 'generateContent');511 const responseType = (stream ? 'streamGenerateContent' : 'generateContent');
500512
501 let url;513 let url;
514 let headers = {
515 'Content-Type': 'application/json',
516 };
517
502 if (useVertexAi) {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 } else {553 } else {
505 url = `${apiUrl.toString().replace(/\/$/, '')}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`;554 url = `${apiUrl.toString().replace(/\/$/, '')}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`;
506 }555 }
556
507 const generateResponse = await fetch(url, {557 const generateResponse = await fetch(url, {
508 body: JSON.stringify(body),558 body: JSON.stringify(body),
509 method: 'POST',559 method: 'POST',
510 headers: {560 headers: headers,
511 'Content-Type': 'application/json',
512 },
513 signal: controller.signal,561 signal: controller.signal,
514 });562 });
515563
src/endpoints/google.js+165 -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,122 @@ 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
14export 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 */
62export 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
91export 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 */
116export function getProjectIdFromServiceAccount(serviceAccount) {
117 if (!serviceAccount || typeof serviceAccount !== 'object') {
118 throw new Error('Invalid service account object');
119 }
120
121 const projectId = serviceAccount.project_id;
122 if (!projectId || typeof projectId !== 'string') {
123 throw new Error('Project ID not found in service account JSON');
124 }
125
126 return projectId;
127}
128
12export const router = express.Router();129export const router = express.Router();
13130
14router.post('/caption-image', async (request, response) => {131router.post('/caption-image', async (request, response) => {
@@ -17,20 +134,57 @@ router.post('/caption-image', async (request, response) => {
17 const base64Data = request.body.image.split(',')[1];134 const base64Data = request.body.image.split(',')[1];
18 const useVertexAi = request.body.api === 'vertexai';135 const useVertexAi = request.body.api === 'vertexai';
19 const apiName = useVertexAi ? 'Google Vertex AI' : 'Google AI Studio';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 const model = request.body.model || 'gemini-2.0-flash';137 const model = request.body.model || 'gemini-2.0-flash';
138
30 let url;139 let url;
140 let headers = {
141 'Content-Type': 'application/json',
142 };
143
31 if (useVertexAi) {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 } else {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 url = `${apiUrl.origin}/v1beta/models/${model}:generateContent?key=${apiKey}`;188 url = `${apiUrl.origin}/v1beta/models/${model}:generateContent?key=${apiKey}`;
35 }189 }
36 const body = {190 const body = {
@@ -53,9 +207,7 @@ router.post('/caption-image', async (request, response) => {
53 const result = await fetch(url, {207 const result = await fetch(url, {
54 body: JSON.stringify(body),208 body: JSON.stringify(body),
55 method: 'POST',209 method: 'POST',
56 headers: {210 headers: headers,
57 'Content-Type': 'application/json',
58 },
59 });211 });
60212
61 if (!result.ok) {213 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