Merge pull request #4046 from InterestingDarknessII/vertexfull Add Vertex AI Full Version support
Signed| @@ -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,32 +3225,114 @@ | |||
| 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 Key | 3230 | <!-- 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" data-mode="express"> | ||
| 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> | ||
| 3242 | </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> |
| @@ -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) |
| @@ -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 | }; |
| 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 | 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 | } |
| 166 | 172 | ||
| 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 | } |
| 170 | 192 | ||
| 171 | if (multimodalApi === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] && !useReverseProxy) { | 193 | if (multimodalApi === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] && !useReverseProxy) { |
| @@ -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 | ]; |
| 239 | 240 | ||
| 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 | } |
| 2192 | 2203 | ||
| 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,15 +4995,27 @@ async function onConnectButtonClick(e) { | |||
| 4975 | } | 4995 | } |
| 4976 | 4996 | ||
| 4977 | if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) { | 4997 | if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) { |
| 4978 | const api_key_vertexai = String($('#api_key_vertexai').val()).trim(); | 4998 | if (oai_settings.vertexai_auth_mode === 'express') { |
| 4999 | // Express mode - use API key | ||
| 5000 | const api_key_vertexai = String($('#api_key_vertexai').val()).trim(); | ||
| 4979 | 5001 | ||
| 4980 | if (api_key_vertexai.length) { | 5002 | if (api_key_vertexai.length) { |
| 4981 | await writeSecret(SECRET_KEYS.VERTEXAI, api_key_vertexai); | 5003 | await writeSecret(SECRET_KEYS.VERTEXAI, api_key_vertexai); |
| 4982 | } | 5004 | } |
| 4983 | 5005 | ||
| 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'); |
| 4986 | return; | 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.'); | ||
| 5017 | return; | ||
| 5018 | } | ||
| 4987 | } | 5019 | } |
| 4988 | } | 5020 | } |
| 4989 | 5021 | ||
| @@ -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 | } |
| 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 | export function initOpenAI() { | 5643 | export 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); |
| @@ -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 | }; |
| 48 | 49 | ||
| 49 | const INPUT_MAP = { | 50 | const 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 | }; |
| 84 | 86 | ||
| 87 | const STATIC_PLACEHOLDER_KEYS = [ | ||
| 88 | SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT, | ||
| 89 | ]; | ||
| 90 | |||
| 85 | async function clearSecret() { | 91 | async 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() { | |||
| 93 | 99 | ||
| 94 | export function updateSecretDisplay() { | 100 | export 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]; |
| 97 | 106 | ||
| 98 | const placeholder = $('#viewSecrets').attr(validSecret ? 'key_saved_text' : 'missing_key_text'); | 107 | const placeholder = $('#viewSecrets').attr(validSecret ? 'key_saved_text' : 'missing_key_text'); |
| @@ -43,6 +43,7 @@ import { | |||
| 43 | webTokenizers, | 43 | webTokenizers, |
| 44 | getWebTokenizer, | 44 | getWebTokenizer, |
| 45 | } from '../tokenizers.js'; | 45 | } from '../tokenizers.js'; |
| 46 | import { getVertexAIAuth, getProjectIdFromServiceAccount } from '../google.js'; | ||
| 46 | 47 | ||
| 47 | const API_OPENAI = 'https://api.openai.com/v1'; | 48 | const API_OPENAI = 'https://api.openai.com/v1'; |
| 48 | const API_CLAUDE = 'https://api.anthropic.com/v1'; | 49 | const 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; |
| 350 | 351 | ||
| 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); | ||
| 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 | } 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 | } |
| 368 | 379 | ||
| 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 | ]; |
| 395 | 407 | ||
| 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'); |
| 500 | 512 | ||
| 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 | }); |
| 515 | 563 | ||
| @@ -2,6 +2,7 @@ import { Buffer } from 'node:buffer'; | |||
| 2 | import fetch from 'node-fetch'; | 2 | import fetch from 'node-fetch'; |
| 3 | import express from 'express'; | 3 | import express from 'express'; |
| 4 | import { speak, languages } from 'google-translate-api-x'; | 4 | import { speak, languages } from 'google-translate-api-x'; |
| 5 | import crypto from 'node:crypto'; | ||
| 5 | 6 | ||
| 6 | import { readSecret, SECRET_KEYS } from './secrets.js'; | 7 | import { readSecret, SECRET_KEYS } from './secrets.js'; |
| 7 | import { GEMINI_SAFETY } from '../constants.js'; | 8 | import { GEMINI_SAFETY } from '../constants.js'; |
| @@ -9,6 +10,122 @@ import { GEMINI_SAFETY } from '../constants.js'; | |||
| 9 | const API_MAKERSUITE = 'https://generativelanguage.googleapis.com'; | 10 | const API_MAKERSUITE = 'https://generativelanguage.googleapis.com'; |
| 10 | const API_VERTEX_AI = 'https://us-central1-aiplatform.googleapis.com'; | 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 | export const router = express.Router(); | 129 | export const router = express.Router(); |
| 13 | 130 | ||
| 14 | router.post('/caption-image', async (request, response) => { | 131 | router.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; | ||
| 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 | } | ||
| 33 | } else { | 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 | }); |
| 60 | 212 | ||
| 61 | if (!result.ok) { | 213 | if (!result.ok) { |
| @@ -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 | }; |
| 58 | 59 | ||
| 59 | // These are the keys that are safe to expose, even if allowKeysExposure is false | 60 | // These are the keys that are safe to expose, even if allowKeysExposure is false |