From 9936fb483d706e0226fd9e8c813477dc80b6dd26 Mon Sep 17 00:00:00 2001 From: munimunigamer Date: Mon, 29 Dec 2025 02:21:30 -1200 Subject: [PATCH 1/8] added external api settings to extension settings --- src/core/state.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/state.js b/src/core/state.js index 03fabf9..4d004a2 100644 --- a/src/core/state.js +++ b/src/core/state.js @@ -172,6 +172,14 @@ export let extensionSettings = { // Auto avatar generation settings autoGenerateAvatars: false, // Master toggle for auto-generating avatars avatarLLMCustomInstruction: '', // Custom instruction for LLM prompt generation + // External API settings for 'external' generation mode + externalApiSettings: { + baseUrl: '', // OpenAI-compatible API base URL (e.g., "https://api.openai.com/v1") + apiKey: '', // API key for the external service + model: '', // Model identifier (e.g., "gpt-4o-mini") + maxTokens: 2048, // Maximum tokens for generation + temperature: 0.7 // Temperature setting for generation + } }; /** From 1d4a64bac746c48fa9a902318bd3ed3cdad6f637 Mon Sep 17 00:00:00 2001 From: munimunigamer Date: Mon, 29 Dec 2025 02:38:08 -1200 Subject: [PATCH 2/8] added external api --- index.js | 101 +++++++- src/i18n/en.json | 13 +- src/i18n/zh-tw.json | 15 +- src/systems/generation/apiClient.js | 143 ++++++++++- src/systems/ui/layout.js | 8 +- template.html | 353 ++++++++++++++++++++-------- 6 files changed, 526 insertions(+), 107 deletions(-) diff --git a/index.js b/index.js index 4fe6ed5..5880c50 100644 --- a/index.js +++ b/index.js @@ -51,7 +51,7 @@ import { generateSeparateUpdatePrompt } from './src/systems/generation/promptBuilder.js'; import { parseResponse, parseUserStats } from './src/systems/generation/parser.js'; -import { updateRPGData } from './src/systems/generation/apiClient.js'; +import { updateRPGData, testExternalAPIConnection } from './src/systems/generation/apiClient.js'; import { onGenerationStarted } from './src/systems/generation/injector.js'; // Rendering modules @@ -621,6 +621,95 @@ async function initUI() { } }); + // External API settings event handlers + $('#rpg-external-base-url').on('change', function() { + if (!extensionSettings.externalApiSettings) { + extensionSettings.externalApiSettings = { + baseUrl: '', apiKey: '', model: '', maxTokens: 2048, temperature: 0.7 + }; + } + extensionSettings.externalApiSettings.baseUrl = String($(this).val()).trim(); + saveSettings(); + }); + + $('#rpg-external-api-key').on('change', function() { + if (!extensionSettings.externalApiSettings) { + extensionSettings.externalApiSettings = { + baseUrl: '', apiKey: '', model: '', maxTokens: 2048, temperature: 0.7 + }; + } + extensionSettings.externalApiSettings.apiKey = String($(this).val()).trim(); + saveSettings(); + }); + + $('#rpg-external-model').on('change', function() { + if (!extensionSettings.externalApiSettings) { + extensionSettings.externalApiSettings = { + baseUrl: '', apiKey: '', model: '', maxTokens: 2048, temperature: 0.7 + }; + } + extensionSettings.externalApiSettings.model = String($(this).val()).trim(); + saveSettings(); + }); + + $('#rpg-external-max-tokens').on('change', function() { + if (!extensionSettings.externalApiSettings) { + extensionSettings.externalApiSettings = { + baseUrl: '', apiKey: '', model: '', maxTokens: 2048, temperature: 0.7 + }; + } + extensionSettings.externalApiSettings.maxTokens = parseInt(String($(this).val())); + saveSettings(); + }); + + $('#rpg-external-temperature').on('change', function() { + if (!extensionSettings.externalApiSettings) { + extensionSettings.externalApiSettings = { + baseUrl: '', apiKey: '', model: '', maxTokens: 2048, temperature: 0.7 + }; + } + extensionSettings.externalApiSettings.temperature = parseFloat(String($(this).val())); + saveSettings(); + }); + + $('#rpg-toggle-api-key-visibility').on('click', function() { + const $input = $('#rpg-external-api-key'); + const type = $input.attr('type') === 'password' ? 'text' : 'password'; + $input.attr('type', type); + $(this).find('i').toggleClass('fa-eye fa-eye-slash'); + }); + + $('#rpg-test-external-api').on('click', async function() { + const $result = $('#rpg-external-api-test-result'); + const $btn = $(this); + const originalText = $btn.html(); + + $btn.html(' Testing...').prop('disabled', true); + $result.hide().removeClass('rpg-success-message rpg-error-message'); + + try { + const result = await testExternalAPIConnection(); + + if (result.success) { + $result.addClass('rpg-success-message') + .html(` ${result.message}`) + .slideDown(); + toastr.success(result.message); + } else { + $result.addClass('rpg-error-message') + .html(` ${result.message}`) + .slideDown(); + toastr.error(result.message); + } + } catch (error) { + $result.addClass('rpg-error-message') + .html(` Error: ${error.message}`) + .slideDown(); + } finally { + $btn.html(originalText).prop('disabled', false); + } + }); + // Initialize UI state (enable/disable is in Extensions tab) $('#rpg-toggle-auto-update').prop('checked', extensionSettings.autoUpdate); $('#rpg-position-select').val(extensionSettings.panelPosition); @@ -678,6 +767,16 @@ async function initUI() { $('#rpg-custom-accent').val(extensionSettings.customColors.accent); $('#rpg-custom-text').val(extensionSettings.customColors.text); $('#rpg-custom-highlight').val(extensionSettings.customColors.highlight); + + // Initialize External API settings values + if (extensionSettings.externalApiSettings) { + $('#rpg-external-base-url').val(extensionSettings.externalApiSettings.baseUrl || ''); + $('#rpg-external-api-key').val(extensionSettings.externalApiSettings.apiKey || ''); + $('#rpg-external-model').val(extensionSettings.externalApiSettings.model || ''); + $('#rpg-external-max-tokens').val(extensionSettings.externalApiSettings.maxTokens || 2048); + $('#rpg-external-temperature').val(extensionSettings.externalApiSettings.temperature ?? 0.7); + } + $('#rpg-generation-mode').val(extensionSettings.generationMode); $('#rpg-skip-guided-mode').val(extensionSettings.skipInjectionsForGuided); $('#rpg-save-tracker-history').prop('checked', extensionSettings.saveTrackerHistory); diff --git a/src/i18n/en.json b/src/i18n/en.json index f657e0e..2f9fbb9 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -53,7 +53,18 @@ "template.settingsModal.advanced.generationMode": "Generation Mode:", "template.settingsModal.advanced.generationModeOptions.together": "Together with Main Generation", "template.settingsModal.advanced.generationModeOptions.separate": "Separate Generation", - "template.settingsModal.advanced.generationModeNote": "Together: Adds RPG tracking to main roleplay. Separate: Generates RPG data separately (manual or auto).", + "template.settingsModal.advanced.generationModeNote": "Together: Adds RPG tracking to main roleplay. Separate: Generates RPG data separately (manual or auto). External: Connects to a 3rd party API directly.", + "template.settingsModal.advanced.generationModeOptions.external": "External API", + "template.settingsModal.advanced.externalApi.title": "External API Settings", + "template.settingsModal.advanced.externalApi.baseUrl": "API Base URL", + "template.settingsModal.advanced.externalApi.baseUrlNote": "OpenAI-compatible endpoint (e.g., OpenAI, OpenRouter, local LLM server)", + "template.settingsModal.advanced.externalApi.apiKey": "API Key", + "template.settingsModal.advanced.externalApi.apiKeyNote": "Your API key for the external service", + "template.settingsModal.advanced.externalApi.model": "Model", + "template.settingsModal.advanced.externalApi.modelNote": "Model identifier (e.g., gpt-4o-mini, claude-3-haiku, mistral-7b)", + "template.settingsModal.advanced.externalApi.maxTokens": "Max Tokens", + "template.settingsModal.advanced.externalApi.temperature": "Temperature", + "template.settingsModal.advanced.externalApi.testConnection": "Test Connection", "template.settingsModal.advanced.contextMessages": "Context Messages:", "template.settingsModal.advanced.contextMessagesNote": "Number of recent messages to include (Separate mode only)", "template.settingsModal.advanced.memoryBatchSize": "Memory Batch Size:", diff --git a/src/i18n/zh-tw.json b/src/i18n/zh-tw.json index d197716..8298685 100644 --- a/src/i18n/zh-tw.json +++ b/src/i18n/zh-tw.json @@ -44,8 +44,19 @@ "template.settingsModal.advanced.generationMode": "生成模式:", "template.settingsModal.advanced.generationModeOptions.together": "同時生成", "template.settingsModal.advanced.generationModeOptions.separate": "單獨生成", - "template.settingsModal.advanced.generationModeNote": "同時生成:將 RPG 追蹤添加到主要提示詞中一同生成。單獨生成:分開生成 RPG 數據。(就是手動或自動的差別)。", - "template.settingsModal.advanced.contextMessages": "上下文訊息:", + "template.settingsModal.advanced.generationModeNote": "同時生成:將 RPG 追蹤添加到主要提示詞中一同生成。單獨生成:分開生成 RPG 數據。(就是手動或自動的差別)。外部 API:直接連接第三方 API 生成數據。", + "template.settingsModal.advanced.generationModeOptions.external": "外部 API", + "template.settingsModal.advanced.externalApi.title": "外部 API 設定", + "template.settingsModal.advanced.externalApi.baseUrl": "API 基礎 URL", + "template.settingsModal.advanced.externalApi.baseUrlNote": "OpenAI 兼容端點(例如 OpenAI、OpenRouter、本地 LLM 伺服器)", + "template.settingsModal.advanced.externalApi.apiKey": "API 金鑰", + "template.settingsModal.advanced.externalApi.apiKeyNote": "外部服務的 API 金鑰", + "template.settingsModal.advanced.externalApi.model": "模型", + "template.settingsModal.advanced.externalApi.modelNote": "模型識別碼(例如 gpt-4o-mini、claude-3-haiku、mistral-7b)", + "template.settingsModal.advanced.externalApi.maxTokens": "最大 Token", + "template.settingsModal.advanced.externalApi.temperature": "溫度 (Temperature)", + "template.settingsModal.advanced.externalApi.testConnection": "測試連接", + "template.settingsModal.advanced.contextMessages": "上下文訊息:", "template.settingsModal.advanced.contextMessagesNote": "包含的最近訊息數量(僅限單獨生成模式)", "template.settingsModal.advanced.memoryBatchSize": "記憶批次大小:", "template.settingsModal.advanced.memoryBatchSizeNote": "在記憶回憶中每批處理的訊息數量", diff --git a/src/systems/generation/apiClient.js b/src/systems/generation/apiClient.js index 2c74834..fabdee3 100644 --- a/src/systems/generation/apiClient.js +++ b/src/systems/generation/apiClient.js @@ -30,6 +30,118 @@ import { generateAvatarsForCharacters } from '../features/avatarGenerator.js'; // Store the original preset name to restore after tracker generation let originalPresetName = null; +/** + * Generates tracker data using an external OpenAI-compatible API. + * Used when generationMode is 'external'. + * + * @param {Array<{role: string, content: string}>} messages - Array of message objects for the API + * @returns {Promise} The generated response content + * @throws {Error} If the API call fails or configuration is invalid + */ +export async function generateWithExternalAPI(messages) { + const { baseUrl, apiKey, model, maxTokens, temperature } = extensionSettings.externalApiSettings || {}; + + // Validate required settings + if (!baseUrl || !baseUrl.trim()) { + throw new Error('External API base URL is not configured'); + } + if (!apiKey || !apiKey.trim()) { + throw new Error('External API key is not configured'); + } + if (!model || !model.trim()) { + throw new Error('External API model is not configured'); + } + + // Normalize base URL (remove trailing slash if present) + const normalizedBaseUrl = baseUrl.trim().replace(/\/+$/, ''); + const endpoint = `${normalizedBaseUrl}/chat/completions`; + + console.log(`[RPG Companion] Calling external API: ${normalizedBaseUrl} with model: ${model}`); + + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey.trim()}` + }, + body: JSON.stringify({ + model: model.trim(), + messages: messages, + max_tokens: maxTokens || 2048, + temperature: temperature ?? 0.7 + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + let errorMessage = `External API error: ${response.status} ${response.statusText}`; + try { + const errorJson = JSON.parse(errorText); + if (errorJson.error?.message) { + errorMessage = `External API error: ${errorJson.error.message}`; + } + } catch (e) { + // If parsing fails, use the raw text if it's short enough + if (errorText.length < 200) { + errorMessage = `External API error: ${errorText}`; + } + } + throw new Error(errorMessage); + } + + const data = await response.json(); + + if (!data.choices || !data.choices[0] || !data.choices[0].message) { + throw new Error('Invalid response format from external API'); + } + + const content = data.choices[0].message.content; + console.log('[RPG Companion] External API response received successfully'); + + return content; + } catch (error) { + if (error.name === 'TypeError' && error.message.includes('fetch')) { + throw new Error(`Failed to connect to external API. Check the URL and your network connection.`); + } + throw error; + } +} + +/** + * Tests the external API connection with a simple request. + * @returns {Promise<{success: boolean, message: string, model?: string}>} + */ +export async function testExternalAPIConnection() { + const { baseUrl, apiKey, model } = extensionSettings.externalApiSettings || {}; + + if (!baseUrl || !apiKey || !model) { + return { + success: false, + message: 'Please fill in all required fields (Base URL, API Key, and Model)' + }; + } + + try { + const testMessages = [ + { role: 'user', content: 'Respond with exactly: "Connection successful"' } + ]; + + const response = await generateWithExternalAPI(testMessages); + + return { + success: true, + message: `Connection successful! Model: ${model}`, + model: model + }; + } catch (error) { + return { + success: false, + message: error.message || 'Connection failed' + }; + } +} + /** * Gets the current preset name using the /preset command * @returns {Promise} Current preset name or null if unavailable @@ -100,11 +212,13 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough return; } - if (extensionSettings.generationMode !== 'separate') { - // console.log('[RPG Companion] Not in separate mode, skipping manual update'); + if (extensionSettings.generationMode !== 'separate' && extensionSettings.generationMode !== 'external') { + // console.log('[RPG Companion] Not in separate or external mode, skipping manual update'); return; } + const isExternalMode = extensionSettings.generationMode === 'external'; + try { setIsGenerating(true); @@ -114,13 +228,14 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough $updateBtn.html(` ${updatingText}`).prop('disabled', true); // Save current preset name before switching (if we're going to switch) - if (extensionSettings.useSeparatePreset) { + // Note: Preset switching is only used in separate mode, not external mode + if (!isExternalMode && extensionSettings.useSeparatePreset) { originalPresetName = await getCurrentPresetName(); console.log(`[RPG Companion] Saved original preset: "${originalPresetName}"`); } - // Switch to separate preset if enabled - if (extensionSettings.useSeparatePreset) { + // Switch to separate preset if enabled (separate mode only) + if (!isExternalMode && extensionSettings.useSeparatePreset) { const switched = await switchToPreset('RPG Companion Trackers'); if (!switched) { console.warn('[RPG Companion] Failed to switch to RPG Companion Trackers preset. Using current preset.'); @@ -130,11 +245,19 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough const prompt = await generateSeparateUpdatePrompt(); - // Generate using raw prompt (uses current preset, no chat history) - const response = await generateRaw({ - prompt: prompt, - quietToLoud: false - }); + // Generate response based on mode + let response; + if (isExternalMode) { + // External mode: Use external OpenAI-compatible API directly + console.log('[RPG Companion] Using external API for tracker generation'); + response = await generateWithExternalAPI(prompt); + } else { + // Separate mode: Use SillyTavern's generateRaw + response = await generateRaw({ + prompt: prompt, + quietToLoud: false + }); + } if (response) { // console.log('[RPG Companion] Raw AI response:', response); diff --git a/src/systems/ui/layout.js b/src/systems/ui/layout.js index 3ee1ddb..ef3e7a9 100644 --- a/src/systems/ui/layout.js +++ b/src/systems/ui/layout.js @@ -293,8 +293,14 @@ export function updateGenerationModeUI() { if (extensionSettings.generationMode === 'together') { // In "together" mode, manual update button is hidden $('#rpg-manual-update').hide(); - } else { + $('#rpg-external-api-settings').slideUp(200); + } else if (extensionSettings.generationMode === 'separate') { // In "separate" mode, manual update button is visible $('#rpg-manual-update').show(); + $('#rpg-external-api-settings').slideUp(200); + } else if (extensionSettings.generationMode === 'external') { + // In "external" mode, manual update button is visible AND external settings are shown + $('#rpg-manual-update').show(); + $('#rpg-external-api-settings').slideDown(200); } } diff --git a/template.html b/template.html index bfe41b6..4268719 100644 --- a/template.html +++ b/template.html @@ -73,16 +73,19 @@
@@ -90,79 +93,103 @@ -