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