mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
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.
This commit is contained in:
@@ -2779,7 +2779,7 @@
|
|||||||
<option value="deepseek">DeepSeek</option>
|
<option value="deepseek">DeepSeek</option>
|
||||||
<option value="groq">Groq</option>
|
<option value="groq">Groq</option>
|
||||||
<option value="makersuite">Google AI Studio</option>
|
<option value="makersuite">Google AI Studio</option>
|
||||||
<option value="vertexai">Google Vertex AI (Express mode)</option>
|
<option value="vertexai">Google Vertex AI</option>
|
||||||
<option value="mistralai">MistralAI</option>
|
<option value="mistralai">MistralAI</option>
|
||||||
<option value="nanogpt">NanoGPT</option>
|
<option value="nanogpt">NanoGPT</option>
|
||||||
<option value="openrouter">OpenRouter</option>
|
<option value="openrouter">OpenRouter</option>
|
||||||
@@ -3225,34 +3225,147 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div id="vertexai_form" data-source="vertexai">
|
<div id="vertexai_form" data-source="vertexai">
|
||||||
<h4>
|
<h4 data-i18n="Google Vertex AI Configuration">Google Vertex AI Configuration</h4>
|
||||||
<span data-i18n="Google Vertex AI API Key">
|
|
||||||
Google Vertex AI API Key
|
<!-- Authentication Mode Selection -->
|
||||||
</span>
|
|
||||||
<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">
|
|
||||||
(Express mode keys only)
|
|
||||||
</a>
|
|
||||||
</h4>
|
|
||||||
<div class="flex-container">
|
<div class="flex-container">
|
||||||
<input id="api_key_vertexai" name="api_key_vertexai" class="text_pole flex1" value="" type="text" autocomplete="off">
|
<label for="vertexai_auth_mode" data-i18n="Authentication Mode">Authentication Mode:</label>
|
||||||
<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>
|
<select id="vertexai_auth_mode" class="text_pole">
|
||||||
|
<option value="express" data-i18n="Express Mode (API Key)">Express Mode (API Key)</option>
|
||||||
|
<option value="full" data-i18n="Full Version (Service Account)">Full Version (Service Account)</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<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.">
|
|
||||||
For privacy reasons, your API key will be hidden after you reload the page.
|
<!-- Express Mode Configuration -->
|
||||||
|
<div id="vertexai_express_config" class="vertexai-auth-section">
|
||||||
|
<h4>
|
||||||
|
<span data-i18n="Google Vertex AI API Key">
|
||||||
|
Google Vertex AI API Key
|
||||||
|
</span>
|
||||||
|
<a href="https://cloud.google.com/vertex-ai/generative-ai/docs/start/express-mode/overview" data-i18n="(Express mode)" target="_blank" rel="noopener noreferrer">
|
||||||
|
(Express mode)
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
<div class="flex-container">
|
||||||
|
<input id="api_key_vertexai" name="api_key_vertexai" class="text_pole flex1" value="" type="text" autocomplete="off">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<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.">
|
||||||
|
For privacy reasons, your API key will be hidden after you reload the page.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Full Version Configuration -->
|
||||||
|
<div id="vertexai_full_config" class="vertexai-auth-section" style="display: none;">
|
||||||
|
<h4>
|
||||||
|
<span data-i18n="Service Account Configuration">
|
||||||
|
Service Account Configuration
|
||||||
|
</span>
|
||||||
|
<a href="https://cloud.google.com/vertex-ai/docs/authentication" target="_blank" rel="noopener noreferrer">
|
||||||
|
<i class="fa-solid fa-circle-question"></i>
|
||||||
|
</a>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<!-- Project ID -->
|
||||||
|
<div class="flex-container">
|
||||||
|
<label for="vertexai_project_id" data-i18n="Project ID">Project ID:</label>
|
||||||
|
<input id="vertexai_project_id" name="vertexai_project_id" class="text_pole flex1" value="" type="text" autocomplete="off" placeholder="your-gcp-project-id">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Region -->
|
||||||
|
<div class="flex-container">
|
||||||
|
<label for="vertexai_region" data-i18n="Region">Region:</label>
|
||||||
|
<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">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Service Account JSON Content -->
|
||||||
|
<div class="flex-container flexFlowColumn">
|
||||||
|
<label for="vertexai_service_account_json" data-i18n="Service Account JSON Content">Service Account JSON Content:</label>
|
||||||
|
<textarea id="vertexai_service_account_json" class="text_pole textarea_compact" rows="8" placeholder='Paste your Service Account JSON content here, e.g.:
|
||||||
|
{
|
||||||
|
"type": "service_account",
|
||||||
|
"project_id": "your-project-id",
|
||||||
|
"private_key_id": "...",
|
||||||
|
"private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
|
||||||
|
"client_email": "...",
|
||||||
|
"client_id": "...",
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token"
|
||||||
|
}'></textarea>
|
||||||
|
<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.">
|
||||||
|
Paste the complete JSON content of your Google Cloud Service Account key file. The content will be stored securely.
|
||||||
|
</div>
|
||||||
|
<div class="flex-container">
|
||||||
|
<button type="button" id="vertexai_validate_service_account" class="menu_button" data-i18n="Validate JSON">Validate JSON</button>
|
||||||
|
<button type="button" id="vertexai_clear_service_account" class="menu_button" data-i18n="Clear">Clear</button>
|
||||||
|
</div>
|
||||||
|
<div id="vertexai_service_account_status" class="info-block" style="display: none;">
|
||||||
|
<span id="vertexai_service_account_info"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Model Selection -->
|
||||||
<div>
|
<div>
|
||||||
<h4 data-i18n="Google Model">Google Model</h4>
|
<h4 data-i18n="Google Model">Google Model</h4>
|
||||||
<select id="model_vertexai_select">
|
<select id="model_vertexai_select">
|
||||||
<optgroup label="Gemini 2.5">
|
<!-- Express Mode Models -->
|
||||||
|
<optgroup id="vertexai_express_models" label="Express Mode Models">
|
||||||
<option value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option>
|
<option value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option>
|
||||||
<option value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option>
|
<option value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option>
|
||||||
<option value="gemini-2.5-flash-preview-05-20">gemini-2.5-flash-preview-05-20</option>
|
<option value="gemini-2.5-flash-preview-05-20">gemini-2.5-flash-preview-05-20</option>
|
||||||
<option value="gemini-2.5-flash-preview-04-17">gemini-2.5-flash-preview-04-17</option>
|
<option value="gemini-2.5-flash-preview-04-17">gemini-2.5-flash-preview-04-17</option>
|
||||||
</optgroup>
|
|
||||||
<optgroup label="Gemini 2.0">
|
|
||||||
<option value="gemini-2.0-flash-001">gemini-2.0-flash-001</option>
|
<option value="gemini-2.0-flash-001">gemini-2.0-flash-001</option>
|
||||||
<option value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option>
|
<option value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
|
|
||||||
|
<!-- Full Version Models -->
|
||||||
|
<optgroup id="vertexai_full_gemini_25" label="Gemini 2.5" style="display: none;">
|
||||||
|
<option value="gemini-2.5-pro-preview-05-06">gemini-2.5-pro-preview-05-06</option>
|
||||||
|
<option value="gemini-2.5-pro-preview-03-25">gemini-2.5-pro-preview-03-25</option>
|
||||||
|
<option value="gemini-2.5-pro-exp-03-25">gemini-2.5-pro-exp-03-25</option>
|
||||||
|
<option value="gemini-2.5-flash-preview-05-20">gemini-2.5-flash-preview-05-20</option>
|
||||||
|
<option value="gemini-2.5-flash-preview-04-17">gemini-2.5-flash-preview-04-17</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup id="vertexai_full_gemini_20" label="Gemini 2.0" style="display: none;">
|
||||||
|
<option value="gemini-2.0-pro-exp-02-05">gemini-2.0-pro-exp-02-05 → 2.5-pro-exp-03-25</option>
|
||||||
|
<option value="gemini-2.0-pro-exp">gemini-2.0-pro-exp → 2.5-pro-exp-03-25</option>
|
||||||
|
<option value="gemini-exp-1206">gemini-exp-1206 → 2.5-pro-exp-03-25</option>
|
||||||
|
<option value="gemini-2.0-flash-001">gemini-2.0-flash-001</option>
|
||||||
|
<option value="gemini-2.0-flash-exp-image-generation">gemini-2.0-flash-exp-image-generation</option>
|
||||||
|
<option value="gemini-2.0-flash-exp">gemini-2.0-flash-exp</option>
|
||||||
|
<option value="gemini-2.0-flash">gemini-2.0-flash</option>
|
||||||
|
<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>
|
||||||
|
<option value="gemini-2.0-flash-thinking-exp-1219">gemini-2.0-flash-thinking-exp-1219 → 2.5-flash-preview-04-17</option>
|
||||||
|
<option value="gemini-2.0-flash-thinking-exp">gemini-2.0-flash-thinking-exp → 2.5-flash-preview-04-17</option>
|
||||||
|
<option value="gemini-2.0-flash-lite-001">gemini-2.0-flash-lite-001</option>
|
||||||
|
<option value="gemini-2.0-flash-lite-preview-02-05">gemini-2.0-flash-lite-preview-02-05</option>
|
||||||
|
<option value="gemini-2.0-flash-lite-preview">gemini-2.0-flash-lite-preview</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup id="vertexai_full_gemini_15" label="Gemini 1.5" style="display: none;">
|
||||||
|
<option value="gemini-1.5-pro-latest">gemini-1.5-pro-latest</option>
|
||||||
|
<option value="gemini-1.5-pro-002">gemini-1.5-pro-002</option>
|
||||||
|
<option value="gemini-1.5-pro-001">gemini-1.5-pro-001</option>
|
||||||
|
<option value="gemini-1.5-pro">gemini-1.5-pro</option>
|
||||||
|
<option value="gemini-1.5-flash-latest">gemini-1.5-flash-latest</option>
|
||||||
|
<option value="gemini-1.5-flash-002">gemini-1.5-flash-002</option>
|
||||||
|
<option value="gemini-1.5-flash-001">gemini-1.5-flash-001</option>
|
||||||
|
<option value="gemini-1.5-flash">gemini-1.5-flash</option>
|
||||||
|
<option value="gemini-1.5-flash-8b-001">gemini-1.5-flash-8b-001</option>
|
||||||
|
<option value="gemini-1.5-flash-8b-exp-0924">gemini-1.5-flash-8b-exp-0924</option>
|
||||||
|
<option value="gemini-1.5-flash-8b-exp-0827">gemini-1.5-flash-8b-exp-0827</option>
|
||||||
|
<option value="gemini-1.5-flash-8b">gemini-1.5-flash-8b</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup id="vertexai_full_gemma" label="Gemma" style="display: none;">
|
||||||
|
<option value="gemma-3-27b-it">gemma-3-27b-it</option>
|
||||||
|
<option value="gemma-3-12b-it">gemma-3-12b-it</option>
|
||||||
|
<option value="gemma-3-4b-it">gemma-3-4b-it</option>
|
||||||
|
<option value="gemma-3-1b-it">gemma-3-1b-it</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup id="vertexai_full_learnlm" label="LearnLM" style="display: none;">
|
||||||
|
<option value="learnlm-2.0-flash-experimental">learnlm-2.0-flash-experimental</option>
|
||||||
|
<option value="learnlm-1.5-pro-experimental">learnlm-1.5-pro-experimental</option>
|
||||||
|
</optgroup>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -56,6 +56,13 @@ export async function getMultimodalCaption(base64Img, prompt) {
|
|||||||
model: extension_settings.caption.multimodal_model || 'gpt-4-turbo',
|
model: extension_settings.caption.multimodal_model || 'gpt-4-turbo',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add Vertex AI specific parameters if using Vertex AI
|
||||||
|
if (extension_settings.caption.multimodal_api === 'vertexai') {
|
||||||
|
requestBody.vertexai_auth_mode = oai_settings.vertexai_auth_mode;
|
||||||
|
requestBody.vertexai_project_id = oai_settings.vertexai_project_id;
|
||||||
|
requestBody.vertexai_region = oai_settings.vertexai_region;
|
||||||
|
}
|
||||||
|
|
||||||
if (isOllama) {
|
if (isOllama) {
|
||||||
if (extension_settings.caption.multimodal_model === 'ollama_current') {
|
if (extension_settings.caption.multimodal_model === 'ollama_current') {
|
||||||
requestBody.model = textgenerationwebui_settings.ollama_model;
|
requestBody.model = textgenerationwebui_settings.ollama_model;
|
||||||
@@ -164,8 +171,27 @@ function throwIfInvalidModel(useReverseProxy) {
|
|||||||
throw new Error('Google AI Studio API key is not set.');
|
throw new Error('Google AI Studio API key is not set.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (multimodalApi === 'vertexai' && !secret_state[SECRET_KEYS.VERTEXAI] && !useReverseProxy) {
|
if (multimodalApi === 'vertexai' && !useReverseProxy) {
|
||||||
throw new Error('Google Vertex AI API key is not set.');
|
// Check based on authentication mode
|
||||||
|
const authMode = oai_settings.vertexai_auth_mode || 'express';
|
||||||
|
|
||||||
|
if (authMode === 'express') {
|
||||||
|
// Express mode requires API key
|
||||||
|
if (!secret_state[SECRET_KEYS.VERTEXAI]) {
|
||||||
|
throw new Error('Google Vertex AI API key is not set for Express mode.');
|
||||||
|
}
|
||||||
|
} else if (authMode === 'full') {
|
||||||
|
// Full mode requires Service Account JSON and project settings
|
||||||
|
if (!secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) {
|
||||||
|
throw new Error('Service Account JSON is required for Vertex AI Full mode. Please validate and save your Service Account JSON.');
|
||||||
|
}
|
||||||
|
if (!oai_settings.vertexai_project_id) {
|
||||||
|
throw new Error('Project ID is required for Vertex AI Full mode.');
|
||||||
|
}
|
||||||
|
if (!oai_settings.vertexai_region) {
|
||||||
|
throw new Error('Region is required for Vertex AI Full mode.');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (multimodalApi === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] && !useReverseProxy) {
|
if (multimodalApi === 'mistral' && !secret_state[SECRET_KEYS.MISTRALAI] && !useReverseProxy) {
|
||||||
|
@@ -306,6 +306,10 @@ export const settingsToUpdate = {
|
|||||||
assistant_impersonation: ['#claude_assistant_impersonation', 'assistant_impersonation', false, false],
|
assistant_impersonation: ['#claude_assistant_impersonation', 'assistant_impersonation', false, false],
|
||||||
claude_use_sysprompt: ['#claude_use_sysprompt', 'claude_use_sysprompt', true, false],
|
claude_use_sysprompt: ['#claude_use_sysprompt', 'claude_use_sysprompt', true, false],
|
||||||
use_makersuite_sysprompt: ['#use_makersuite_sysprompt', 'use_makersuite_sysprompt', true, false],
|
use_makersuite_sysprompt: ['#use_makersuite_sysprompt', 'use_makersuite_sysprompt', true, false],
|
||||||
|
vertexai_auth_mode: ['#vertexai_auth_mode', 'vertexai_auth_mode', false, true],
|
||||||
|
vertexai_project_id: ['#vertexai_project_id', 'vertexai_project_id', false, true],
|
||||||
|
vertexai_region: ['#vertexai_region', 'vertexai_region', false, true],
|
||||||
|
vertexai_service_account_json: ['#vertexai_service_account_json', 'vertexai_service_account_json', false, true],
|
||||||
use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true, true],
|
use_alt_scale: ['#use_alt_scale', 'use_alt_scale', true, true],
|
||||||
squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true, false],
|
squash_system_messages: ['#squash_system_messages', 'squash_system_messages', true, false],
|
||||||
image_inlining: ['#openai_image_inlining', 'image_inlining', true, false],
|
image_inlining: ['#openai_image_inlining', 'image_inlining', true, false],
|
||||||
@@ -387,6 +391,10 @@ const default_settings = {
|
|||||||
assistant_impersonation: '',
|
assistant_impersonation: '',
|
||||||
claude_use_sysprompt: false,
|
claude_use_sysprompt: false,
|
||||||
use_makersuite_sysprompt: true,
|
use_makersuite_sysprompt: true,
|
||||||
|
vertexai_auth_mode: 'express',
|
||||||
|
vertexai_project_id: '',
|
||||||
|
vertexai_region: 'us-central1',
|
||||||
|
vertexai_service_account_json: '',
|
||||||
use_alt_scale: false,
|
use_alt_scale: false,
|
||||||
squash_system_messages: false,
|
squash_system_messages: false,
|
||||||
image_inlining: false,
|
image_inlining: false,
|
||||||
@@ -471,6 +479,10 @@ const oai_settings = {
|
|||||||
assistant_impersonation: '',
|
assistant_impersonation: '',
|
||||||
claude_use_sysprompt: false,
|
claude_use_sysprompt: false,
|
||||||
use_makersuite_sysprompt: true,
|
use_makersuite_sysprompt: true,
|
||||||
|
vertexai_auth_mode: 'express',
|
||||||
|
vertexai_project_id: '',
|
||||||
|
vertexai_region: 'us-central1',
|
||||||
|
vertexai_service_account_json: '',
|
||||||
use_alt_scale: false,
|
use_alt_scale: false,
|
||||||
squash_system_messages: false,
|
squash_system_messages: false,
|
||||||
image_inlining: false,
|
image_inlining: false,
|
||||||
@@ -2188,6 +2200,11 @@ async function sendOpenAIRequest(type, messages, signal) {
|
|||||||
generate_data['top_k'] = Number(oai_settings.top_k_openai);
|
generate_data['top_k'] = Number(oai_settings.top_k_openai);
|
||||||
generate_data['stop'] = getCustomStoppingStrings(stopStringsLimit).slice(0, stopStringsLimit).filter(x => x.length >= 1 && x.length <= 16);
|
generate_data['stop'] = getCustomStoppingStrings(stopStringsLimit).slice(0, stopStringsLimit).filter(x => x.length >= 1 && x.length <= 16);
|
||||||
generate_data['use_makersuite_sysprompt'] = oai_settings.use_makersuite_sysprompt;
|
generate_data['use_makersuite_sysprompt'] = oai_settings.use_makersuite_sysprompt;
|
||||||
|
if (isVertexAI) {
|
||||||
|
generate_data['vertexai_auth_mode'] = oai_settings.vertexai_auth_mode;
|
||||||
|
generate_data['vertexai_project_id'] = oai_settings.vertexai_project_id;
|
||||||
|
generate_data['vertexai_region'] = oai_settings.vertexai_region;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isMistral) {
|
if (isMistral) {
|
||||||
@@ -3427,6 +3444,9 @@ function loadOpenAISettings(data, settings) {
|
|||||||
if (settings.openai_model !== undefined) oai_settings.openai_model = settings.openai_model;
|
if (settings.openai_model !== undefined) oai_settings.openai_model = settings.openai_model;
|
||||||
if (settings.claude_use_sysprompt !== undefined) oai_settings.claude_use_sysprompt = !!settings.claude_use_sysprompt;
|
if (settings.claude_use_sysprompt !== undefined) oai_settings.claude_use_sysprompt = !!settings.claude_use_sysprompt;
|
||||||
if (settings.use_makersuite_sysprompt !== undefined) oai_settings.use_makersuite_sysprompt = !!settings.use_makersuite_sysprompt;
|
if (settings.use_makersuite_sysprompt !== undefined) oai_settings.use_makersuite_sysprompt = !!settings.use_makersuite_sysprompt;
|
||||||
|
if (settings.vertexai_auth_mode !== undefined) oai_settings.vertexai_auth_mode = settings.vertexai_auth_mode;
|
||||||
|
if (settings.vertexai_project_id !== undefined) oai_settings.vertexai_project_id = settings.vertexai_project_id;
|
||||||
|
if (settings.vertexai_region !== undefined) oai_settings.vertexai_region = settings.vertexai_region;
|
||||||
if (settings.use_alt_scale !== undefined) { oai_settings.use_alt_scale = !!settings.use_alt_scale; updateScaleForm(); }
|
if (settings.use_alt_scale !== undefined) { oai_settings.use_alt_scale = !!settings.use_alt_scale; updateScaleForm(); }
|
||||||
$('#stream_toggle').prop('checked', oai_settings.stream_openai);
|
$('#stream_toggle').prop('checked', oai_settings.stream_openai);
|
||||||
$('#api_url_scale').val(oai_settings.api_url_scale);
|
$('#api_url_scale').val(oai_settings.api_url_scale);
|
||||||
@@ -3482,6 +3502,12 @@ function loadOpenAISettings(data, settings) {
|
|||||||
$('#openai_external_category').toggle(oai_settings.show_external_models);
|
$('#openai_external_category').toggle(oai_settings.show_external_models);
|
||||||
$('#claude_use_sysprompt').prop('checked', oai_settings.claude_use_sysprompt);
|
$('#claude_use_sysprompt').prop('checked', oai_settings.claude_use_sysprompt);
|
||||||
$('#use_makersuite_sysprompt').prop('checked', oai_settings.use_makersuite_sysprompt);
|
$('#use_makersuite_sysprompt').prop('checked', oai_settings.use_makersuite_sysprompt);
|
||||||
|
$('#vertexai_auth_mode').val(oai_settings.vertexai_auth_mode);
|
||||||
|
$('#vertexai_project_id').val(oai_settings.vertexai_project_id);
|
||||||
|
$('#vertexai_region').val(oai_settings.vertexai_region);
|
||||||
|
// Don't display Service Account JSON in textarea - it's stored in backend secrets
|
||||||
|
$('#vertexai_service_account_json').val('');
|
||||||
|
updateVertexAIServiceAccountStatus();
|
||||||
$('#scale-alt').prop('checked', oai_settings.use_alt_scale);
|
$('#scale-alt').prop('checked', oai_settings.use_alt_scale);
|
||||||
$('#openrouter_use_fallback').prop('checked', oai_settings.openrouter_use_fallback);
|
$('#openrouter_use_fallback').prop('checked', oai_settings.openrouter_use_fallback);
|
||||||
$('#openrouter_group_models').prop('checked', oai_settings.openrouter_group_models);
|
$('#openrouter_group_models').prop('checked', oai_settings.openrouter_group_models);
|
||||||
@@ -3804,6 +3830,10 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
|
|||||||
assistant_impersonation: settings.assistant_impersonation,
|
assistant_impersonation: settings.assistant_impersonation,
|
||||||
claude_use_sysprompt: settings.claude_use_sysprompt,
|
claude_use_sysprompt: settings.claude_use_sysprompt,
|
||||||
use_makersuite_sysprompt: settings.use_makersuite_sysprompt,
|
use_makersuite_sysprompt: settings.use_makersuite_sysprompt,
|
||||||
|
vertexai_auth_mode: settings.vertexai_auth_mode,
|
||||||
|
vertexai_project_id: settings.vertexai_project_id,
|
||||||
|
vertexai_region: settings.vertexai_region,
|
||||||
|
vertexai_service_account_json: settings.vertexai_service_account_json,
|
||||||
use_alt_scale: settings.use_alt_scale,
|
use_alt_scale: settings.use_alt_scale,
|
||||||
squash_system_messages: settings.squash_system_messages,
|
squash_system_messages: settings.squash_system_messages,
|
||||||
image_inlining: settings.image_inlining,
|
image_inlining: settings.image_inlining,
|
||||||
@@ -4979,15 +5009,30 @@ async function onConnectButtonClick(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) {
|
if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) {
|
||||||
const api_key_vertexai = String($('#api_key_vertexai').val()).trim();
|
if (oai_settings.vertexai_auth_mode === 'express') {
|
||||||
|
// Express mode - use API key
|
||||||
|
const api_key_vertexai = String($('#api_key_vertexai').val()).trim();
|
||||||
|
|
||||||
if (api_key_vertexai.length) {
|
if (api_key_vertexai.length) {
|
||||||
await writeSecret(SECRET_KEYS.VERTEXAI, api_key_vertexai);
|
await writeSecret(SECRET_KEYS.VERTEXAI, api_key_vertexai);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!secret_state[SECRET_KEYS.VERTEXAI] && !oai_settings.reverse_proxy) {
|
if (!secret_state[SECRET_KEYS.VERTEXAI] && !oai_settings.reverse_proxy) {
|
||||||
console.log('No secret key saved for Vertex AI');
|
console.log('No secret key saved for Vertex AI Express mode');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Full version - use service account
|
||||||
|
if (!oai_settings.vertexai_project_id.trim()) {
|
||||||
|
toastr.error('Project ID is required for Vertex AI full version');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if service account JSON is saved in backend
|
||||||
|
if (!secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) {
|
||||||
|
toastr.error('Service Account JSON is required for Vertex AI full version. Please validate and save your Service Account JSON.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5170,6 +5215,8 @@ function toggleChatCompletionForms() {
|
|||||||
}
|
}
|
||||||
else if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) {
|
else if (oai_settings.chat_completion_source == chat_completion_sources.VERTEXAI) {
|
||||||
$('#model_vertexai_select').trigger('change');
|
$('#model_vertexai_select').trigger('change');
|
||||||
|
// Update UI based on authentication mode
|
||||||
|
onVertexAIAuthModeChange.call($('#vertexai_auth_mode')[0]);
|
||||||
}
|
}
|
||||||
else if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) {
|
else if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER) {
|
||||||
$('#model_openrouter_select').trigger('change');
|
$('#model_openrouter_select').trigger('change');
|
||||||
@@ -5480,6 +5527,160 @@ function runProxyCallback(_, value) {
|
|||||||
return foundName;
|
return foundName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Vertex AI authentication mode change
|
||||||
|
*/
|
||||||
|
function onVertexAIAuthModeChange() {
|
||||||
|
const authMode = String($(this).val());
|
||||||
|
oai_settings.vertexai_auth_mode = authMode;
|
||||||
|
|
||||||
|
// Show/hide appropriate configuration sections
|
||||||
|
if (authMode === 'express') {
|
||||||
|
$('#vertexai_express_config').show();
|
||||||
|
$('#vertexai_full_config').hide();
|
||||||
|
$('#vertexai_express_models').show();
|
||||||
|
// Hide all full version model groups
|
||||||
|
$('#vertexai_full_gemini_25').hide();
|
||||||
|
$('#vertexai_full_gemini_20').hide();
|
||||||
|
$('#vertexai_full_gemini_15').hide();
|
||||||
|
$('#vertexai_full_gemma').hide();
|
||||||
|
$('#vertexai_full_learnlm').hide();
|
||||||
|
} else {
|
||||||
|
$('#vertexai_express_config').hide();
|
||||||
|
$('#vertexai_full_config').show();
|
||||||
|
$('#vertexai_express_models').hide();
|
||||||
|
// Show all full version model groups
|
||||||
|
$('#vertexai_full_gemini_25').show();
|
||||||
|
$('#vertexai_full_gemini_20').show();
|
||||||
|
$('#vertexai_full_gemini_15').show();
|
||||||
|
$('#vertexai_full_gemma').show();
|
||||||
|
$('#vertexai_full_learnlm').show();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSettingsDebounced();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate Vertex AI service account JSON
|
||||||
|
*/
|
||||||
|
async function onVertexAIValidateServiceAccount() {
|
||||||
|
const jsonContent = String($('#vertexai_service_account_json').val()).trim();
|
||||||
|
|
||||||
|
if (!jsonContent) {
|
||||||
|
toastr.error('Please enter Service Account JSON content');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const serviceAccount = JSON.parse(jsonContent);
|
||||||
|
const requiredFields = ['type', 'project_id', 'private_key', 'client_email', 'client_id'];
|
||||||
|
const missingFields = requiredFields.filter(field => !serviceAccount[field]);
|
||||||
|
|
||||||
|
if (missingFields.length > 0) {
|
||||||
|
toastr.error(`Missing required fields: ${missingFields.join(', ')}`);
|
||||||
|
updateVertexAIServiceAccountStatus(false, `Missing fields: ${missingFields.join(', ')}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceAccount.type !== 'service_account') {
|
||||||
|
toastr.error('Invalid service account type. Expected "service_account"');
|
||||||
|
updateVertexAIServiceAccountStatus(false, 'Invalid service account type');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to backend secret storage
|
||||||
|
await writeSecret(SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT, jsonContent);
|
||||||
|
|
||||||
|
// Clear the textarea and update settings
|
||||||
|
$('#vertexai_service_account_json').val('');
|
||||||
|
oai_settings.vertexai_service_account_json = '';
|
||||||
|
|
||||||
|
// Show success status
|
||||||
|
updateVertexAIServiceAccountStatus(true, `Project: ${serviceAccount.project_id}, Email: ${serviceAccount.client_email}`);
|
||||||
|
|
||||||
|
toastr.success('Service Account JSON is valid and saved securely');
|
||||||
|
saveSettingsDebounced();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('JSON validation error:', error);
|
||||||
|
toastr.error('Invalid JSON format');
|
||||||
|
updateVertexAIServiceAccountStatus(false, 'Invalid JSON format');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear Vertex AI service account JSON
|
||||||
|
*/
|
||||||
|
async function onVertexAIClearServiceAccount() {
|
||||||
|
$('#vertexai_service_account_json').val('');
|
||||||
|
oai_settings.vertexai_service_account_json = '';
|
||||||
|
|
||||||
|
// Clear from backend secret storage
|
||||||
|
await writeSecret(SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT, '');
|
||||||
|
|
||||||
|
updateVertexAIServiceAccountStatus(false);
|
||||||
|
toastr.info('Service Account JSON cleared');
|
||||||
|
saveSettingsDebounced();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Vertex AI service account JSON input change
|
||||||
|
*/
|
||||||
|
function onVertexAIServiceAccountJsonChange() {
|
||||||
|
const jsonContent = String($(this).val()).trim();
|
||||||
|
// Don't save to settings automatically - only save when validated
|
||||||
|
// oai_settings.vertexai_service_account_json = jsonContent;
|
||||||
|
|
||||||
|
if (jsonContent) {
|
||||||
|
// Auto-validate when content is pasted
|
||||||
|
try {
|
||||||
|
const serviceAccount = JSON.parse(jsonContent);
|
||||||
|
const requiredFields = ['type', 'project_id', 'private_key', 'client_email'];
|
||||||
|
const hasAllFields = requiredFields.every(field => serviceAccount[field]);
|
||||||
|
|
||||||
|
if (hasAllFields && serviceAccount.type === 'service_account') {
|
||||||
|
updateVertexAIServiceAccountStatus(false, 'JSON appears valid - click "Validate JSON" to save');
|
||||||
|
} else {
|
||||||
|
updateVertexAIServiceAccountStatus(false, 'Incomplete or invalid JSON');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
updateVertexAIServiceAccountStatus(false, 'Invalid JSON format');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateVertexAIServiceAccountStatus(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't save settings automatically
|
||||||
|
// saveSettingsDebounced();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the Vertex AI service account status display
|
||||||
|
* @param {boolean} isValid - Whether the service account is valid
|
||||||
|
* @param {string} message - Status message to display
|
||||||
|
*/
|
||||||
|
function updateVertexAIServiceAccountStatus(isValid = false, message = '') {
|
||||||
|
const statusDiv = $('#vertexai_service_account_status');
|
||||||
|
const infoSpan = $('#vertexai_service_account_info');
|
||||||
|
|
||||||
|
// If no explicit message provided, check if we have a saved service account
|
||||||
|
if (!message && secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]) {
|
||||||
|
isValid = true;
|
||||||
|
message = 'Service Account JSON is saved and ready to use';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid && message) {
|
||||||
|
infoSpan.html(`<i class="fa-solid fa-check-circle" style="color: green;"></i> ${message}`);
|
||||||
|
statusDiv.show();
|
||||||
|
} else if (!isValid && message) {
|
||||||
|
infoSpan.html(`<i class="fa-solid fa-exclamation-triangle" style="color: orange;"></i> ${message}`);
|
||||||
|
statusDiv.show();
|
||||||
|
} else {
|
||||||
|
statusDiv.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function initOpenAI() {
|
export function initOpenAI() {
|
||||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||||
name: 'proxy',
|
name: 'proxy',
|
||||||
@@ -5943,6 +6144,18 @@ export function initOpenAI() {
|
|||||||
$('#model_scale_select').on('change', onModelChange);
|
$('#model_scale_select').on('change', onModelChange);
|
||||||
$('#model_google_select').on('change', onModelChange);
|
$('#model_google_select').on('change', onModelChange);
|
||||||
$('#model_vertexai_select').on('change', onModelChange);
|
$('#model_vertexai_select').on('change', onModelChange);
|
||||||
|
$('#vertexai_auth_mode').on('change', onVertexAIAuthModeChange);
|
||||||
|
$('#vertexai_project_id').on('input', function () {
|
||||||
|
oai_settings.vertexai_project_id = String($(this).val());
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
$('#vertexai_region').on('input', function () {
|
||||||
|
oai_settings.vertexai_region = String($(this).val());
|
||||||
|
saveSettingsDebounced();
|
||||||
|
});
|
||||||
|
$('#vertexai_service_account_json').on('input', onVertexAIServiceAccountJsonChange);
|
||||||
|
$('#vertexai_validate_service_account').on('click', onVertexAIValidateServiceAccount);
|
||||||
|
$('#vertexai_clear_service_account').on('click', onVertexAIClearServiceAccount);
|
||||||
$('#model_openrouter_select').on('change', onModelChange);
|
$('#model_openrouter_select').on('change', onModelChange);
|
||||||
$('#openrouter_group_models').on('change', onOpenrouterModelSortChange);
|
$('#openrouter_group_models').on('change', onOpenrouterModelSortChange);
|
||||||
$('#openrouter_sort_models').on('change', onOpenrouterModelSortChange);
|
$('#openrouter_sort_models').on('change', onOpenrouterModelSortChange);
|
||||||
|
@@ -44,6 +44,7 @@ export const SECRET_KEYS = {
|
|||||||
SERPER: 'api_key_serper',
|
SERPER: 'api_key_serper',
|
||||||
FALAI: 'api_key_falai',
|
FALAI: 'api_key_falai',
|
||||||
XAI: 'api_key_xai',
|
XAI: 'api_key_xai',
|
||||||
|
VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',
|
||||||
};
|
};
|
||||||
|
|
||||||
const INPUT_MAP = {
|
const INPUT_MAP = {
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import util from 'node:util';
|
import util from 'node:util';
|
||||||
|
import crypto from 'node:crypto';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
@@ -60,6 +61,124 @@ const API_DEEPSEEK = 'https://api.deepseek.com/beta';
|
|||||||
const API_XAI = 'https://api.x.ai/v1';
|
const API_XAI = 'https://api.x.ai/v1';
|
||||||
const API_POLLINATIONS = 'https://text.pollinations.ai/openai';
|
const API_POLLINATIONS = 'https://text.pollinations.ai/openai';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a JWT token for Google Cloud authentication using service account credentials.
|
||||||
|
* @param {object} serviceAccount Service account JSON object
|
||||||
|
* @returns {Promise<string>} JWT token
|
||||||
|
*/
|
||||||
|
async function generateJWTToken(serviceAccount) {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const expiry = now + 3600; // 1 hour
|
||||||
|
|
||||||
|
const header = {
|
||||||
|
alg: 'RS256',
|
||||||
|
typ: 'JWT'
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
iss: serviceAccount.client_email,
|
||||||
|
scope: 'https://www.googleapis.com/auth/cloud-platform',
|
||||||
|
aud: 'https://oauth2.googleapis.com/token',
|
||||||
|
iat: now,
|
||||||
|
exp: expiry
|
||||||
|
};
|
||||||
|
|
||||||
|
const headerBase64 = Buffer.from(JSON.stringify(header)).toString('base64url');
|
||||||
|
const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
||||||
|
const signatureInput = `${headerBase64}.${payloadBase64}`;
|
||||||
|
|
||||||
|
// Create signature using private key
|
||||||
|
const sign = crypto.createSign('RSA-SHA256');
|
||||||
|
sign.update(signatureInput);
|
||||||
|
const signature = sign.sign(serviceAccount.private_key, 'base64url');
|
||||||
|
|
||||||
|
return `${signatureInput}.${signature}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an access token from Google OAuth2 using JWT assertion.
|
||||||
|
* @param {string} jwtToken JWT token
|
||||||
|
* @returns {Promise<string>} Access token
|
||||||
|
*/
|
||||||
|
async function getAccessToken(jwtToken) {
|
||||||
|
const response = await fetch('https://oauth2.googleapis.com/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||||
|
assertion: jwtToken
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to get access token: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets authentication for Vertex AI - either API key or service account token.
|
||||||
|
* @param {object} request Express request
|
||||||
|
* @returns {Promise<{authHeader: string, authType: string}>} Authentication header and type
|
||||||
|
*/
|
||||||
|
async function getVertexAIAuth(request) {
|
||||||
|
// Get the authentication mode from frontend
|
||||||
|
const authMode = request.body.vertexai_auth_mode || 'express';
|
||||||
|
|
||||||
|
// Check if using reverse proxy
|
||||||
|
if (request.body.reverse_proxy) {
|
||||||
|
return {
|
||||||
|
authHeader: `Bearer ${request.body.proxy_password}`,
|
||||||
|
authType: 'proxy'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authMode === 'express') {
|
||||||
|
// Express mode: use API key
|
||||||
|
const apiKey = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI);
|
||||||
|
if (apiKey) {
|
||||||
|
return {
|
||||||
|
authHeader: `Bearer ${apiKey}`,
|
||||||
|
authType: 'express'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error('API key is required for Vertex AI Express mode');
|
||||||
|
} else if (authMode === 'full') {
|
||||||
|
// Full mode: use service account JSON
|
||||||
|
// First try to read from backend secret storage
|
||||||
|
let serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT);
|
||||||
|
|
||||||
|
// If not found in secrets, fall back to request body (for backward compatibility)
|
||||||
|
if (!serviceAccountJson) {
|
||||||
|
serviceAccountJson = request.body.vertexai_service_account_json;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceAccountJson) {
|
||||||
|
try {
|
||||||
|
const serviceAccount = JSON.parse(serviceAccountJson);
|
||||||
|
|
||||||
|
const jwtToken = await generateJWTToken(serviceAccount);
|
||||||
|
const accessToken = await getAccessToken(jwtToken);
|
||||||
|
|
||||||
|
return {
|
||||||
|
authHeader: `Bearer ${accessToken}`,
|
||||||
|
authType: 'full'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to authenticate with service account:', error);
|
||||||
|
throw new Error(`Service account authentication failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('Service Account JSON is required for Vertex AI Full mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unsupported Vertex AI authentication mode: ${authMode}`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies a post-processing step to the generated messages.
|
* Applies a post-processing step to the generated messages.
|
||||||
* @param {object[]} messages Messages to post-process
|
* @param {object[]} messages Messages to post-process
|
||||||
@@ -348,13 +467,20 @@ async function sendMakerSuiteRequest(request, response) {
|
|||||||
let apiUrl;
|
let apiUrl;
|
||||||
let apiKey;
|
let apiKey;
|
||||||
|
|
||||||
|
let authHeader;
|
||||||
|
let authType;
|
||||||
|
|
||||||
if (useVertexAi) {
|
if (useVertexAi) {
|
||||||
apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI);
|
apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI);
|
||||||
apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.VERTEXAI);
|
|
||||||
|
|
||||||
if (!request.body.reverse_proxy && !apiKey) {
|
try {
|
||||||
console.warn(`${apiName} API key is missing.`);
|
const auth = await getVertexAIAuth(request);
|
||||||
return response.status(400).send({ error: true });
|
authHeader = auth.authHeader;
|
||||||
|
authType = auth.authType;
|
||||||
|
console.debug(`Using Vertex AI authentication type: ${authType}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`${apiName} authentication failed: ${error.message}`);
|
||||||
|
return response.status(400).send({ error: true, message: error.message });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE);
|
apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE);
|
||||||
@@ -364,6 +490,9 @@ async function sendMakerSuiteRequest(request, response) {
|
|||||||
console.warn(`${apiName} API key is missing.`);
|
console.warn(`${apiName} API key is missing.`);
|
||||||
return response.status(400).send({ error: true });
|
return response.status(400).send({ error: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authHeader = `Bearer ${apiKey}`;
|
||||||
|
authType = 'api_key';
|
||||||
}
|
}
|
||||||
|
|
||||||
const model = String(request.body.model);
|
const model = String(request.body.model);
|
||||||
@@ -499,17 +628,39 @@ async function sendMakerSuiteRequest(request, response) {
|
|||||||
const responseType = (stream ? 'streamGenerateContent' : 'generateContent');
|
const responseType = (stream ? 'streamGenerateContent' : 'generateContent');
|
||||||
|
|
||||||
let url;
|
let url;
|
||||||
|
let headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
if (useVertexAi) {
|
if (useVertexAi) {
|
||||||
url = `${apiUrl.toString().replace(/\/$/, '')}/v1/publishers/google/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`;
|
if (authType === 'express') {
|
||||||
|
// For Express mode (API key authentication), use the key parameter
|
||||||
|
const keyParam = authHeader.replace('Bearer ', '');
|
||||||
|
url = `${apiUrl.toString().replace(/\/$/, '')}/v1/publishers/google/models/${model}:${responseType}?key=${keyParam}${stream ? '&alt=sse' : ''}`;
|
||||||
|
} else if (authType === 'full') {
|
||||||
|
// For Full mode (service account authentication), use project-specific URL
|
||||||
|
const projectId = request.body.vertexai_project_id || 'your-project-id';
|
||||||
|
const region = request.body.vertexai_region || 'us-central1';
|
||||||
|
// Handle global region differently - no region prefix in hostname
|
||||||
|
if (region === 'global') {
|
||||||
|
url = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${responseType}${stream ? '?alt=sse' : ''}`;
|
||||||
|
} else {
|
||||||
|
url = `https://${region}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:${responseType}${stream ? '?alt=sse' : ''}`;
|
||||||
|
}
|
||||||
|
headers['Authorization'] = authHeader;
|
||||||
|
} else {
|
||||||
|
// For proxy mode, use the original URL with Authorization header
|
||||||
|
url = `${apiUrl.toString().replace(/\/$/, '')}/v1/publishers/google/models/${model}:${responseType}${stream ? '?alt=sse' : ''}`;
|
||||||
|
headers['Authorization'] = authHeader;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
url = `${apiUrl.toString().replace(/\/$/, '')}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`;
|
url = `${apiUrl.toString().replace(/\/$/, '')}/${apiVersion}/models/${model}:${responseType}?key=${apiKey}${stream ? '&alt=sse' : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateResponse = await fetch(url, {
|
const generateResponse = await fetch(url, {
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: headers,
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -2,6 +2,7 @@ import { Buffer } from 'node:buffer';
|
|||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { speak, languages } from 'google-translate-api-x';
|
import { speak, languages } from 'google-translate-api-x';
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
|
||||||
import { readSecret, SECRET_KEYS } from './secrets.js';
|
import { readSecret, SECRET_KEYS } from './secrets.js';
|
||||||
import { GEMINI_SAFETY } from '../constants.js';
|
import { GEMINI_SAFETY } from '../constants.js';
|
||||||
@@ -9,6 +10,108 @@ import { GEMINI_SAFETY } from '../constants.js';
|
|||||||
const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';
|
const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';
|
||||||
const API_VERTEX_AI = 'https://us-central1-aiplatform.googleapis.com';
|
const API_VERTEX_AI = 'https://us-central1-aiplatform.googleapis.com';
|
||||||
|
|
||||||
|
// Vertex AI authentication helper functions
|
||||||
|
async function getVertexAIAuth(request) {
|
||||||
|
const authMode = request.body.vertexai_auth_mode || 'express';
|
||||||
|
|
||||||
|
if (request.body.reverse_proxy) {
|
||||||
|
return {
|
||||||
|
authHeader: `Bearer ${request.body.proxy_password}`,
|
||||||
|
authType: 'proxy'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authMode === 'express') {
|
||||||
|
const apiKey = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI);
|
||||||
|
if (apiKey) {
|
||||||
|
return {
|
||||||
|
authHeader: `Bearer ${apiKey}`,
|
||||||
|
authType: 'express'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw new Error('API key is required for Vertex AI Express mode');
|
||||||
|
} else if (authMode === 'full') {
|
||||||
|
// Try to get service account JSON from backend storage first
|
||||||
|
let serviceAccountJson = readSecret(request.user.directories, SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT);
|
||||||
|
|
||||||
|
// If not found in backend storage, try from request body (for backward compatibility)
|
||||||
|
if (!serviceAccountJson) {
|
||||||
|
serviceAccountJson = request.body.vertexai_service_account_json;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serviceAccountJson) {
|
||||||
|
try {
|
||||||
|
const serviceAccount = JSON.parse(serviceAccountJson);
|
||||||
|
const jwtToken = await generateJWTToken(serviceAccount);
|
||||||
|
const accessToken = await getAccessToken(jwtToken);
|
||||||
|
return {
|
||||||
|
authHeader: `Bearer ${accessToken}`,
|
||||||
|
authType: 'full'
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to authenticate with service account:', error);
|
||||||
|
throw new Error(`Service account authentication failed: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('Service Account JSON is required for Vertex AI Full mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unsupported Vertex AI authentication mode: ${authMode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a JWT token for Google Cloud authentication using service account credentials.
|
||||||
|
* @param {object} serviceAccount Service account JSON object
|
||||||
|
* @returns {Promise<string>} JWT token
|
||||||
|
*/
|
||||||
|
async function generateJWTToken(serviceAccount) {
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const expiry = now + 3600; // 1 hour
|
||||||
|
|
||||||
|
const header = {
|
||||||
|
alg: 'RS256',
|
||||||
|
typ: 'JWT'
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
iss: serviceAccount.client_email,
|
||||||
|
scope: 'https://www.googleapis.com/auth/cloud-platform',
|
||||||
|
aud: 'https://oauth2.googleapis.com/token',
|
||||||
|
iat: now,
|
||||||
|
exp: expiry
|
||||||
|
};
|
||||||
|
|
||||||
|
const headerBase64 = Buffer.from(JSON.stringify(header)).toString('base64url');
|
||||||
|
const payloadBase64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
||||||
|
const signatureInput = `${headerBase64}.${payloadBase64}`;
|
||||||
|
|
||||||
|
// Create signature using private key
|
||||||
|
const sign = crypto.createSign('RSA-SHA256');
|
||||||
|
sign.update(signatureInput);
|
||||||
|
const signature = sign.sign(serviceAccount.private_key, 'base64url');
|
||||||
|
|
||||||
|
return `${signatureInput}.${signature}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAccessToken(jwtToken) {
|
||||||
|
const response = await fetch('https://oauth2.googleapis.com/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
|
||||||
|
assertion: jwtToken
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.text();
|
||||||
|
throw new Error(`Failed to get access token: ${error}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data.access_token;
|
||||||
|
}
|
||||||
|
|
||||||
export const router = express.Router();
|
export const router = express.Router();
|
||||||
|
|
||||||
router.post('/caption-image', async (request, response) => {
|
router.post('/caption-image', async (request, response) => {
|
||||||
@@ -17,20 +120,43 @@ router.post('/caption-image', async (request, response) => {
|
|||||||
const base64Data = request.body.image.split(',')[1];
|
const base64Data = request.body.image.split(',')[1];
|
||||||
const useVertexAi = request.body.api === 'vertexai';
|
const useVertexAi = request.body.api === 'vertexai';
|
||||||
const apiName = useVertexAi ? 'Google Vertex AI' : 'Google AI Studio';
|
const apiName = useVertexAi ? 'Google Vertex AI' : 'Google AI Studio';
|
||||||
let apiKey;
|
|
||||||
let apiUrl;
|
|
||||||
if (useVertexAi) {
|
|
||||||
apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.VERTEXAI);
|
|
||||||
apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI);
|
|
||||||
} else {
|
|
||||||
apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);
|
|
||||||
apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE);
|
|
||||||
}
|
|
||||||
const model = request.body.model || 'gemini-2.0-flash';
|
const model = request.body.model || 'gemini-2.0-flash';
|
||||||
|
|
||||||
let url;
|
let url;
|
||||||
|
let headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
if (useVertexAi) {
|
if (useVertexAi) {
|
||||||
url = `${apiUrl.origin}/v1/publishers/google/models/${model}:generateContent?key=${apiKey}`;
|
// Get authentication for Vertex AI
|
||||||
|
const { authHeader, authType } = await getVertexAIAuth(request);
|
||||||
|
|
||||||
|
if (authType === 'express') {
|
||||||
|
// Express mode: use API key parameter
|
||||||
|
const keyParam = authHeader.replace('Bearer ', '');
|
||||||
|
const apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI);
|
||||||
|
url = `${apiUrl.origin}/v1/publishers/google/models/${model}:generateContent?key=${keyParam}`;
|
||||||
|
} else if (authType === 'full') {
|
||||||
|
// Full mode: use project-specific URL with Authorization header
|
||||||
|
const projectId = request.body.vertexai_project_id || 'your-project-id';
|
||||||
|
const region = request.body.vertexai_region || 'us-central1';
|
||||||
|
// Handle global region differently - no region prefix in hostname
|
||||||
|
if (region === 'global') {
|
||||||
|
url = `https://aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:generateContent`;
|
||||||
|
} else {
|
||||||
|
url = `https://${region}-aiplatform.googleapis.com/v1/projects/${projectId}/locations/${region}/publishers/google/models/${model}:generateContent`;
|
||||||
|
}
|
||||||
|
headers['Authorization'] = authHeader;
|
||||||
|
} else {
|
||||||
|
// Proxy mode: use Authorization header
|
||||||
|
const apiUrl = new URL(request.body.reverse_proxy || API_VERTEX_AI);
|
||||||
|
url = `${apiUrl.origin}/v1/publishers/google/models/${model}:generateContent`;
|
||||||
|
headers['Authorization'] = authHeader;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Google AI Studio
|
||||||
|
const apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE);
|
||||||
|
const apiUrl = new URL(request.body.reverse_proxy || API_MAKERSUITE);
|
||||||
url = `${apiUrl.origin}/v1beta/models/${model}:generateContent?key=${apiKey}`;
|
url = `${apiUrl.origin}/v1beta/models/${model}:generateContent?key=${apiKey}`;
|
||||||
}
|
}
|
||||||
const body = {
|
const body = {
|
||||||
@@ -53,9 +179,7 @@ router.post('/caption-image', async (request, response) => {
|
|||||||
const result = await fetch(url, {
|
const result = await fetch(url, {
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: headers,
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
|
@@ -54,6 +54,7 @@ export const SECRET_KEYS = {
|
|||||||
DEEPSEEK: 'api_key_deepseek',
|
DEEPSEEK: 'api_key_deepseek',
|
||||||
SERPER: 'api_key_serper',
|
SERPER: 'api_key_serper',
|
||||||
XAI: 'api_key_xai',
|
XAI: 'api_key_xai',
|
||||||
|
VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',
|
||||||
};
|
};
|
||||||
|
|
||||||
// These are the keys that are safe to expose, even if allowKeysExposure is false
|
// These are the keys that are safe to expose, even if allowKeysExposure is false
|
||||||
|
Reference in New Issue
Block a user