mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			243 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			243 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import { DOMPurify } from '../lib.js';
 | |
| import { callPopup, getRequestHeaders } from '../script.js';
 | |
| 
 | |
| export const SECRET_KEYS = {
 | |
|     HORDE: 'api_key_horde',
 | |
|     MANCER: 'api_key_mancer',
 | |
|     VLLM: 'api_key_vllm',
 | |
|     APHRODITE: 'api_key_aphrodite',
 | |
|     TABBY: 'api_key_tabby',
 | |
|     OPENAI: 'api_key_openai',
 | |
|     NOVEL: 'api_key_novel',
 | |
|     CLAUDE: 'api_key_claude',
 | |
|     OPENROUTER: 'api_key_openrouter',
 | |
|     SCALE: 'api_key_scale',
 | |
|     AI21: 'api_key_ai21',
 | |
|     SCALE_COOKIE: 'scale_cookie',
 | |
|     MAKERSUITE: 'api_key_makersuite',
 | |
|     SERPAPI: 'api_key_serpapi',
 | |
|     MISTRALAI: 'api_key_mistralai',
 | |
|     TOGETHERAI: 'api_key_togetherai',
 | |
|     INFERMATICAI: 'api_key_infermaticai',
 | |
|     DREAMGEN: 'api_key_dreamgen',
 | |
|     CUSTOM: 'api_key_custom',
 | |
|     OOBA: 'api_key_ooba',
 | |
|     NOMICAI: 'api_key_nomicai',
 | |
|     KOBOLDCPP: 'api_key_koboldcpp',
 | |
|     LLAMACPP: 'api_key_llamacpp',
 | |
|     COHERE: 'api_key_cohere',
 | |
|     PERPLEXITY: 'api_key_perplexity',
 | |
|     GROQ: 'api_key_groq',
 | |
|     AZURE_TTS: 'api_key_azure_tts',
 | |
|     FEATHERLESS: 'api_key_featherless',
 | |
|     ZEROONEAI: 'api_key_01ai',
 | |
|     HUGGINGFACE: 'api_key_huggingface',
 | |
|     STABILITY: 'api_key_stability',
 | |
|     BLOCKENTROPY: 'api_key_blockentropy',
 | |
|     CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
 | |
|     NANOGPT: 'api_key_nanogpt',
 | |
|     TAVILY: 'api_key_tavily',
 | |
|     BFL: 'api_key_bfl',
 | |
|     GENERIC: 'api_key_generic',
 | |
|     DEEPSEEK: 'api_key_deepseek',
 | |
|     SERPER: 'api_key_serper',
 | |
|     FALAI: 'api_key_falai',
 | |
| };
 | |
| 
 | |
| const INPUT_MAP = {
 | |
|     [SECRET_KEYS.HORDE]: '#horde_api_key',
 | |
|     [SECRET_KEYS.MANCER]: '#api_key_mancer',
 | |
|     [SECRET_KEYS.OPENAI]: '#api_key_openai',
 | |
|     [SECRET_KEYS.NOVEL]: '#api_key_novel',
 | |
|     [SECRET_KEYS.CLAUDE]: '#api_key_claude',
 | |
|     [SECRET_KEYS.OPENROUTER]: '.api_key_openrouter',
 | |
|     [SECRET_KEYS.SCALE]: '#api_key_scale',
 | |
|     [SECRET_KEYS.AI21]: '#api_key_ai21',
 | |
|     [SECRET_KEYS.SCALE_COOKIE]: '#scale_cookie',
 | |
|     [SECRET_KEYS.MAKERSUITE]: '#api_key_makersuite',
 | |
|     [SECRET_KEYS.VLLM]: '#api_key_vllm',
 | |
|     [SECRET_KEYS.APHRODITE]: '#api_key_aphrodite',
 | |
|     [SECRET_KEYS.TABBY]: '#api_key_tabby',
 | |
|     [SECRET_KEYS.MISTRALAI]: '#api_key_mistralai',
 | |
|     [SECRET_KEYS.CUSTOM]: '#api_key_custom',
 | |
|     [SECRET_KEYS.TOGETHERAI]: '#api_key_togetherai',
 | |
|     [SECRET_KEYS.OOBA]: '#api_key_ooba',
 | |
|     [SECRET_KEYS.INFERMATICAI]: '#api_key_infermaticai',
 | |
|     [SECRET_KEYS.DREAMGEN]: '#api_key_dreamgen',
 | |
|     [SECRET_KEYS.NOMICAI]: '#api_key_nomicai',
 | |
|     [SECRET_KEYS.KOBOLDCPP]: '#api_key_koboldcpp',
 | |
|     [SECRET_KEYS.LLAMACPP]: '#api_key_llamacpp',
 | |
|     [SECRET_KEYS.COHERE]: '#api_key_cohere',
 | |
|     [SECRET_KEYS.PERPLEXITY]: '#api_key_perplexity',
 | |
|     [SECRET_KEYS.GROQ]: '#api_key_groq',
 | |
|     [SECRET_KEYS.FEATHERLESS]: '#api_key_featherless',
 | |
|     [SECRET_KEYS.ZEROONEAI]: '#api_key_01ai',
 | |
|     [SECRET_KEYS.HUGGINGFACE]: '#api_key_huggingface',
 | |
|     [SECRET_KEYS.BLOCKENTROPY]: '#api_key_blockentropy',
 | |
|     [SECRET_KEYS.NANOGPT]: '#api_key_nanogpt',
 | |
|     [SECRET_KEYS.GENERIC]: '#api_key_generic',
 | |
|     [SECRET_KEYS.DEEPSEEK]: '#api_key_deepseek',
 | |
| };
 | |
| 
 | |
| async function clearSecret() {
 | |
|     const key = $(this).data('key');
 | |
|     await writeSecret(key, '');
 | |
|     secret_state[key] = false;
 | |
|     updateSecretDisplay();
 | |
|     $(INPUT_MAP[key]).val('').trigger('input');
 | |
|     $('#main_api').trigger('change');
 | |
| }
 | |
| 
 | |
| export function updateSecretDisplay() {
 | |
|     for (const [secret_key, input_selector] of Object.entries(INPUT_MAP)) {
 | |
|         const validSecret = !!secret_state[secret_key];
 | |
| 
 | |
|         const placeholder = $('#viewSecrets').attr(validSecret ? 'key_saved_text' : 'missing_key_text');
 | |
|         $(input_selector).attr('placeholder', placeholder);
 | |
|     }
 | |
| }
 | |
| 
 | |
| async function viewSecrets() {
 | |
|     const response = await fetch('/api/secrets/view', {
 | |
|         method: 'POST',
 | |
|         headers: getRequestHeaders(),
 | |
|     });
 | |
| 
 | |
|     if (response.status == 403) {
 | |
|         callPopup('<h3>Forbidden</h3><p>To view your API keys here, set the value of allowKeysExposure to true in config.yaml file and restart the SillyTavern server.</p>', 'text');
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     if (!response.ok) {
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     $('#dialogue_popup').addClass('wide_dialogue_popup');
 | |
|     const data = await response.json();
 | |
|     const table = document.createElement('table');
 | |
|     table.classList.add('responsiveTable');
 | |
|     $(table).append('<thead><th>Key</th><th>Value</th></thead>');
 | |
| 
 | |
|     for (const [key, value] of Object.entries(data)) {
 | |
|         $(table).append(`<tr><td>${DOMPurify.sanitize(key)}</td><td>${DOMPurify.sanitize(value)}</td></tr>`);
 | |
|     }
 | |
| 
 | |
|     callPopup(table.outerHTML, 'text');
 | |
| }
 | |
| 
 | |
| export let secret_state = {};
 | |
| 
 | |
| export async function writeSecret(key, value) {
 | |
|     try {
 | |
|         const response = await fetch('/api/secrets/write', {
 | |
|             method: 'POST',
 | |
|             headers: getRequestHeaders(),
 | |
|             body: JSON.stringify({ key, value }),
 | |
|         });
 | |
| 
 | |
|         if (response.ok) {
 | |
|             const text = await response.text();
 | |
| 
 | |
|             if (text == 'ok') {
 | |
|                 secret_state[key] = !!value;
 | |
|                 updateSecretDisplay();
 | |
|             }
 | |
|         }
 | |
|     } catch {
 | |
|         console.error('Could not write secret value: ', key);
 | |
|     }
 | |
| }
 | |
| 
 | |
| export async function readSecretState() {
 | |
|     try {
 | |
|         const response = await fetch('/api/secrets/read', {
 | |
|             method: 'POST',
 | |
|             headers: getRequestHeaders(),
 | |
|         });
 | |
| 
 | |
|         if (response.ok) {
 | |
|             secret_state = await response.json();
 | |
|             updateSecretDisplay();
 | |
|             await checkOpenRouterAuth();
 | |
|         }
 | |
|     } catch {
 | |
|         console.error('Could not read secrets file');
 | |
|     }
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Finds a secret value by key.
 | |
|  * @param {string} key Secret key
 | |
|  * @returns {Promise<string | undefined>} Secret value, or undefined if keys are not exposed
 | |
|  */
 | |
| export async function findSecret(key) {
 | |
|     try {
 | |
|         const response = await fetch('/api/secrets/find', {
 | |
|             method: 'POST',
 | |
|             headers: getRequestHeaders(),
 | |
|             body: JSON.stringify({ key }),
 | |
|         });
 | |
| 
 | |
|         if (response.ok) {
 | |
|             const data = await response.json();
 | |
|             return data.value;
 | |
|         }
 | |
|     } catch {
 | |
|         console.error('Could not find secret value: ', key);
 | |
|     }
 | |
| }
 | |
| 
 | |
| function authorizeOpenRouter() {
 | |
|     const openRouterUrl = `https://openrouter.ai/auth?callback_url=${encodeURIComponent(location.origin)}`;
 | |
|     location.href = openRouterUrl;
 | |
| }
 | |
| 
 | |
| async function checkOpenRouterAuth() {
 | |
|     const params = new URLSearchParams(location.search);
 | |
|     if (params.has('code')) {
 | |
|         const code = params.get('code');
 | |
|         try {
 | |
|             const response = await fetch('https://openrouter.ai/api/v1/auth/keys', {
 | |
|                 method: 'POST',
 | |
|                 body: JSON.stringify({ code }),
 | |
|             });
 | |
| 
 | |
|             if (!response.ok) {
 | |
|                 throw new Error('OpenRouter exchange error');
 | |
|             }
 | |
| 
 | |
|             const data = await response.json();
 | |
|             if (!data || !data.key) {
 | |
|                 throw new Error('OpenRouter invalid response');
 | |
|             }
 | |
| 
 | |
|             await writeSecret(SECRET_KEYS.OPENROUTER, data.key);
 | |
| 
 | |
|             if (secret_state[SECRET_KEYS.OPENROUTER]) {
 | |
|                 toastr.success('OpenRouter token saved');
 | |
|                 // Remove the code from the URL
 | |
|                 const currentUrl = window.location.href;
 | |
|                 const urlWithoutSearchParams = currentUrl.split('?')[0];
 | |
|                 window.history.pushState({}, '', urlWithoutSearchParams);
 | |
|             } else {
 | |
|                 throw new Error('OpenRouter token not saved');
 | |
|             }
 | |
|         } catch (err) {
 | |
|             toastr.error('Could not verify OpenRouter token. Please try again.');
 | |
|             return;
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| jQuery(async () => {
 | |
|     $('#viewSecrets').on('click', viewSecrets);
 | |
|     $(document).on('click', '.clear-api-key', clearSecret);
 | |
|     $(document).on('input', Object.values(INPUT_MAP).join(','), function () {
 | |
|         const id = $(this).attr('id');
 | |
|         const value = $(this).val();
 | |
|         const warningElement = $(`[data-for="${id}"]`);
 | |
|         warningElement.toggle(value.length > 0);
 | |
|     });
 | |
|     $('.openrouter_authorize').on('click', authorizeOpenRouter);
 | |
| });
 |