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 @@
-
+
RPG Companion Settings
-
+
-
Theme
+
Theme
-
+
-
+
-
+
-
+
-
+
-
+
- Color when stats are at 0%
+ Color when stats are at
+ 0%
-
+
- Color when stats are at 100%
+ Color when stats are at
+ 100%
-
Display Options
-
- Use the Extensions tab to enable/disable the RPG Companion extension.
+
Display Options
+
+ Use the Extensions tab to enable/disable
+ the RPG Companion extension.
-
+
-
+
Smooth transitions for stats, content updates, and dice rolls
- Show Plot Progression Buttons
+ Show Plot
+ Progression Buttons
-
+
Display buttons above chat input for plot progression prompts
@@ -234,7 +270,8 @@
Show Dice Roll Display
-
+
Display the "Last Roll" indicator in the panel.
@@ -242,27 +279,36 @@
Enable Debug Mode
-
- Shows parser logs in a mobile-friendly UI panel. Useful for troubleshooting. Look for the red bug button.
+
+ Shows parser logs in a mobile-friendly UI panel. Useful for troubleshooting. Look for the red bug
+ button.
- Auto-generate Missing Avatars
+ Auto-generate Missing
+ Avatars
-
- Automatically generate avatars for characters without custom images using the Image Generation Plugin
+
+ Automatically generate avatars for characters without custom images using the Image Generation
+ Plugin
-
+
LLM Instruction:
-
-
- The LLM will use character cards, tracker data, and chat context to generate detailed prompts
+
+
+ The LLM will use character cards, tracker data, and chat context to generate detailed
+ prompts
@@ -281,7 +327,8 @@
Chat History Depth:
-
+
Number of recent messages to include in combat initialization
@@ -295,93 +342,205 @@
-
Advanced
+
Advanced
- Generation Mode:
+ Generation Mode:
- Together: Adds RPG tracking to main roleplay. Separate: Generates RPG data separately (manual or auto).
+ Together: Adds RPG
+ tracking to main roleplay. Separate: Generates RPG data separately (manual or auto).
+
+
+
+
+
+
+ External API
+ Configuration
+
+
+
+ API Base URL:
+
+ OpenAI-compatible
+ endpoint (e.g., OpenAI, OpenRouter, local LLM server)
+
- Context Messages:
+ Context Messages:
- Number of recent messages to include (Separate mode only)
+ Number of recent messages
+ to include (Separate mode only)
- Memory Batch Size:
+ Memory Batch Size:
- Number of messages to process per batch in Memory Recollection
+ Number of messages to
+ process per batch in Memory Recollection
- Use model connected to RPG Companion Trackers preset
+ Use model connected to RPG
+ Companion Trackers preset
-
- Separate mode only. When enabled, tracker generation will use the model from the "RPG Companion Trackers" preset instead of your main API model. The preset will be switched automatically during generation and restored afterward. Select the desired model in that preset and make sure the "Bind presets to API connections" toggle is on (next to the import/export preset buttons).
+
+ Separate mode only. When enabled, tracker generation will use the model from the "RPG Companion
+ Trackers" preset instead of your main API model. The preset will be switched automatically during
+ generation and restored afterward. Select the desired model in that preset and make sure the "Bind
+ presets to API connections" toggle is on (next to the import/export preset buttons).
- Skip Injections during Guided Generations:
+ Skip Injections during Guided
+ Generations:
-
- When set, the extension will not inject tracker prompts, examples, or HTML instructions according to the selected mode when a guided generation (via `instruct` or `quiet_prompt`) is detected. Useful when using GuidedGenerations or similar extensions.
+
+ When set, the extension will not inject tracker prompts, examples, or HTML instructions according to
+ the selected mode when a guided generation (via `instruct` or `quiet_prompt`) is detected. Useful
+ when using GuidedGenerations or similar extensions.
- Save Tracker History in Chat
+ Save Tracker History in
+ Chat
-
- When enabled, tracker data is saved in chat history for each message. In Together mode, trackers appear in <trackers> XML tags (hidden from display). In Separate mode, tracker data is stored in message metadata. When disabled, only the most recent trackers are kept.
+
+ When enabled, tracker data is saved in chat history for each message. In Together mode, trackers
+ appear in <trackers> XML tags (hidden from display). In Separate mode, tracker data is stored
+ in message metadata. When disabled, only the most recent trackers are kept.
-
-
- Customize the HTML prompt injected when "Enable Immersive HTML" is enabled. The default prompt is shown above - you can edit it directly or replace it entirely. Click "Restore Default" to reset. This affects all generation modes (together, separate, and plot progression).
+
+ Customize the HTML prompt injected when "Enable Immersive HTML" is enabled. The default prompt
+ is shown above - you can edit it directly or replace it entirely. Click "Restore Default" to
+ reset. This affects all generation modes (together, separate, and plot progression).
-
- Resets all floating action buttons (toggle, refresh, debug) to default top-left positions. Useful if buttons are off-screen.
+
+ Resets all floating action buttons (toggle, refresh, debug) to default top-left positions.
+ Useful if buttons are off-screen.