diff --git a/package-lock.json b/package-lock.json index 74c7d725f..38686d089 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sillytavern", - "version": "1.11.4", + "version": "1.11.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sillytavern", - "version": "1.11.4", + "version": "1.11.5", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index f5100b1e9..fd94aa2f3 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "type": "git", "url": "https://github.com/SillyTavern/SillyTavern.git" }, - "version": "1.11.4", + "version": "1.11.5", "scripts": { "start": "node server.js", "start-multi": "node server.js --disableCsrf", diff --git a/public/index.html b/public/index.html index f7451f973..74862b5b3 100644 --- a/public/index.html +++ b/public/index.html @@ -1915,6 +1915,7 @@ + @@ -1938,6 +1939,30 @@ +
+

OpenRouter API Key

+
+ + Click "Authorize" below or get the key from OpenRouter. +
+ View Remaining Credits +
+
+ + +
+
+ For privacy reasons, your API key will be hidden after you reload the page. +
+
+

OpenRouter Model

+ +
+

InfermaticAI API Key

@@ -2105,6 +2130,7 @@
+
- +
@@ -2505,9 +2531,18 @@

MistralAI Model

@@ -2546,7 +2581,7 @@ - +
diff --git a/public/script.js b/public/script.js index d040f9192..5c46ca183 100644 --- a/public/script.js +++ b/public/script.js @@ -22,7 +22,7 @@ import { parseTabbyLogprobs, } from './scripts/textgen-settings.js'; -const { MANCER, TOGETHERAI, OOBA, APHRODITE, OLLAMA, INFERMATICAI } = textgen_types; +const { MANCER, TOGETHERAI, OOBA, APHRODITE, OLLAMA, INFERMATICAI, OPENROUTER } = textgen_types; import { world_info, @@ -196,7 +196,7 @@ import { createPersona, initPersonas, selectCurrentPersona, setPersonaDescriptio import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; import { hideLoader, showLoader } from './scripts/loader.js'; import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js'; -import { loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels } from './scripts/textgen-models.js'; +import { loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels } from './scripts/textgen-models.js'; import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags } from './scripts/chats.js'; import { initPresetManager } from './scripts/preset-manager.js'; import { evaluateMacros } from './scripts/macros.js'; @@ -1060,6 +1060,9 @@ async function getStatusTextgen() { } else if (textgen_settings.type === INFERMATICAI) { loadInfermaticAIModels(data?.data); online_status = textgen_settings.infermaticai_model; + } else if (textgen_settings.type === OPENROUTER) { + loadOpenRouterModels(data?.data); + online_status = textgen_settings.openrouter_model; } else { online_status = data?.result; } @@ -5964,7 +5967,11 @@ function updateMessage(div) { text = text.trim(); } - const bias = extractMessageBias(text); + const bias = substituteParams(extractMessageBias(text)); + text = substituteParams(text); + if (bias) { + text = removeMacros(text); + } mes['mes'] = text; if (mes['swipe_id'] !== undefined) { mes['swipes'][mes['swipe_id']] = text; @@ -6007,7 +6014,7 @@ function openMessageDelete(fromSlashCommand) { } function messageEditAuto(div) { - const { mesBlock, text, mes } = updateMessage(div); + const { mesBlock, text, mes, bias } = updateMessage(div); mesBlock.find('.mes_text').val(''); mesBlock.find('.mes_text').val(messageFormatting( @@ -6017,6 +6024,8 @@ function messageEditAuto(div) { mes.is_user, this_edit_mes_id, )); + mesBlock.find('.mes_bias').empty(); + mesBlock.find('.mes_bias').append(messageFormatting(bias, '', false, false, -1)); saveChatDebounced(); } @@ -7706,6 +7715,11 @@ const CONNECT_API_MAP = { button: '#api_button_textgenerationwebui', type: textgen_types.INFERMATICAI, }, + 'openrouter-text': { + selected: 'textgenerationwebui', + button: '#api_button_textgenerationwebui', + type: textgen_types.OPENROUTER, + }, }; async function selectContextCallback(_, name) { @@ -8649,6 +8663,11 @@ jQuery(async function () { await writeSecret(SECRET_KEYS.INFERMATICAI, infermaticAIKey); } + const openRouterKey = String($('#api_key_openrouter-tg').val()).trim(); + if (openRouterKey.length) { + await writeSecret(SECRET_KEYS.OPENROUTER, openRouterKey); + } + validateTextGenUrl(); startStatusLoading(); main_api = 'textgenerationwebui'; diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 8198de0e6..73bd5b7bc 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -391,7 +391,8 @@ function RA_autoconnect(PrevApi) { case 'textgenerationwebui': if ((textgen_settings.type === textgen_types.MANCER && secret_state[SECRET_KEYS.MANCER]) || (textgen_settings.type === textgen_types.TOGETHERAI && secret_state[SECRET_KEYS.TOGETHERAI]) - || (textgen_settings.type === textgen_types.INFERMATICAI && secret_state[SECRET_KEYS.INFERMATICAI]) + || (textgen_settings.type === textgen_types.INFERMATICAI && secret_state[SECRET_KEYS.INFERMATICAI] + || (textgen_settings.type === textgen_types.OPENROUTER && secret_state[SECRET_KEYS.OPENROUTER])) ) { $('#api_button_textgenerationwebui').trigger('click'); } diff --git a/public/scripts/openai.js b/public/scripts/openai.js index b7a4eba56..6615c704e 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -216,7 +216,7 @@ const default_settings = { claude_model: 'claude-instant-v1', google_model: 'gemini-pro', ai21_model: 'j2-ultra', - mistralai_model: 'mistral-medium', + mistralai_model: 'mistral-medium-latest', custom_model: '', custom_url: '', custom_include_body: '', @@ -285,7 +285,7 @@ const oai_settings = { claude_model: 'claude-instant-v1', google_model: 'gemini-pro', ai21_model: 'j2-ultra', - mistralai_model: 'mistral-medium', + mistralai_model: 'mistral-medium-latest', custom_model: '', custom_url: '', custom_include_body: '', @@ -3365,8 +3365,16 @@ async function onModelChange() { } if ($(this).is('#model_mistralai_select')) { + // Upgrade old mistral models to new naming scheme + // would have done this in loadOpenAISettings, but it wasn't updating on preset change? + if (value === 'mistral-medium' || value === 'mistral-small' || value === 'mistral-tiny') { + value = value + '-latest'; + } else if (value === '') { + value = default_settings.mistralai_model; + } console.log('MistralAI model changed to', value); oai_settings.mistralai_model = value; + $('#model_mistralai_select').val(oai_settings.mistralai_model); } if (value && $(this).is('#model_custom_select')) { diff --git a/public/scripts/preset-manager.js b/public/scripts/preset-manager.js index fd6ae3b9b..ab9571032 100644 --- a/public/scripts/preset-manager.js +++ b/public/scripts/preset-manager.js @@ -313,6 +313,8 @@ class PresetManager { 'type', 'custom_model', 'bypass_status_check', + 'infermaticai_model', + 'openrouter_model', ]; const settings = Object.assign({}, getSettingsByApiId(this.apiId)); diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js index f1516e999..3ea3c5289 100644 --- a/public/scripts/secrets.js +++ b/public/scripts/secrets.js @@ -27,7 +27,7 @@ const INPUT_MAP = { [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.OPENROUTER]: '.api_key_openrouter', [SECRET_KEYS.SCALE]: '#api_key_scale', [SECRET_KEYS.AI21]: '#api_key_ai21', [SECRET_KEYS.SCALE_COOKIE]: '#scale_cookie', @@ -199,5 +199,5 @@ jQuery(async () => { const warningElement = $(`[data-for="${id}"]`); warningElement.toggle(value.length > 0); }); - $('#openrouter_authorize').on('click', authorizeOpenRouter); + $('.openrouter_authorize').on('click', authorizeOpenRouter); }); diff --git a/public/scripts/textgen-models.js b/public/scripts/textgen-models.js index d7612df5b..a30775af2 100644 --- a/public/scripts/textgen-models.js +++ b/public/scripts/textgen-models.js @@ -1,10 +1,12 @@ import { callPopup, getRequestHeaders, setGenerationParamsFromPreset } from '../script.js'; import { isMobile } from './RossAscends-mods.js'; import { textgenerationwebui_settings as textgen_settings, textgen_types } from './textgen-settings.js'; +import { tokenizers } from './tokenizers.js'; let mancerModels = []; let togetherModels = []; let infermaticAIModels = []; +export let openRouterModels = []; export async function loadOllamaModels(data) { if (!Array.isArray(data)) { @@ -101,6 +103,28 @@ export async function loadMancerModels(data) { } } +export async function loadOpenRouterModels(data) { + if (!Array.isArray(data)) { + console.error('Invalid OpenRouter models data', data); + return; + } + + openRouterModels = data; + + if (!data.find(x => x.id === textgen_settings.openrouter_model)) { + textgen_settings.openrouter_model = data[0]?.id || ''; + } + + $('#openrouter_model').empty(); + for (const model of data) { + const option = document.createElement('option'); + option.value = model.id; + option.text = model.id; + option.selected = model.id === textgen_settings.openrouter_model; + $('#openrouter_model').append(option); + } +} + function onMancerModelSelect() { const modelId = String($('#mancer_model').val()); textgen_settings.mancer_model = modelId; @@ -132,6 +156,14 @@ function onOllamaModelSelect() { $('#api_button_textgenerationwebui').trigger('click'); } +function onOpenRouterModelSelect() { + const modelId = String($('#openrouter_model').val()); + textgen_settings.openrouter_model = modelId; + $('#api_button_textgenerationwebui').trigger('click'); + const model = openRouterModels.find(x => x.id === modelId); + setGenerationParamsFromPreset({ max_length: model.context_length }); +} + function getMancerModelTemplate(option) { const model = mancerModels.find(x => x.id === option?.element?.value); @@ -179,6 +211,25 @@ function getInfermaticAIModelTemplate(option) { `)); } +function getOpenRouterModelTemplate(option) { + const model = openRouterModels.find(x => x.id === option?.element?.value); + + if (!option.id || !model) { + return option.text; + } + + let tokens_dollar = Number(1 / (1000 * model.pricing?.prompt)); + let tokens_rounded = (Math.round(tokens_dollar * 1000) / 1000).toFixed(0); + + const price = 0 === Number(model.pricing?.prompt) ? 'Free' : `${tokens_rounded}k t/$ `; + + return $((` +
+
${DOMPurify.sanitize(model.name)} | ${model.context_length} ctx | ${price}
+
+ `)); +} + async function downloadOllamaModel() { try { const serverUrl = textgen_settings.server_urls[textgen_types.OLLAMA]; @@ -220,11 +271,25 @@ async function downloadOllamaModel() { } } +export function getCurrentOpenRouterModelTokenizer() { + const modelId = textgen_settings.openrouter_model; + const model = openRouterModels.find(x => x.id === modelId); + switch (model?.architecture?.tokenizer) { + case 'Llama2': + return tokenizers.LLAMA; + case 'Mistral': + return tokenizers.MISTRAL; + default: + return tokenizers.OPENAI; + } +} + jQuery(function () { $('#mancer_model').on('change', onMancerModelSelect); $('#model_togetherai_select').on('change', onTogetherModelSelect); $('#model_infermaticai_select').on('change', onInfermaticAIModelSelect); $('#ollama_model').on('change', onOllamaModelSelect); + $('#openrouter_model').on('change', onOpenRouterModelSelect); $('#ollama_download_model').on('click', downloadOllamaModel); if (!isMobile()) { @@ -255,5 +320,12 @@ jQuery(function () { width: '100%', templateResult: getInfermaticAIModelTemplate, }); + $('#openrouter_model').select2({ + placeholder: 'Select a model', + searchInputPlaceholder: 'Search models...', + searchInputCssClass: 'text_pole', + width: '100%', + templateResult: getOpenRouterModelTemplate, + }); } }); diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index bb619959a..3b68b6533 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -14,6 +14,7 @@ import { BIAS_CACHE, createNewLogitBiasEntry, displayLogitBias, getLogitBiasList import { power_user, registerDebugFunction } from './power-user.js'; import EventSourceStream from './sse-stream.js'; +import { getCurrentOpenRouterModelTokenizer } from './textgen-models.js'; import { SENTENCEPIECE_TOKENIZERS, TEXTGEN_TOKENIZERS, getTextTokens, tokenizers } from './tokenizers.js'; import { getSortableDelay, onlyUnique } from './utils.js'; @@ -34,9 +35,10 @@ export const textgen_types = { LLAMACPP: 'llamacpp', OLLAMA: 'ollama', INFERMATICAI: 'infermaticai', + OPENROUTER: 'openrouter', }; -const { MANCER, APHRODITE, TABBY, TOGETHERAI, OOBA, OLLAMA, LLAMACPP, INFERMATICAI } = textgen_types; +const { MANCER, APHRODITE, TABBY, TOGETHERAI, OOBA, OLLAMA, LLAMACPP, INFERMATICAI, OPENROUTER } = textgen_types; const LLAMACPP_DEFAULT_ORDER = [ 'top_k', @@ -69,6 +71,7 @@ const MANCER_SERVER_DEFAULT = 'https://neuro.mancer.tech'; let MANCER_SERVER = localStorage.getItem(MANCER_SERVER_KEY) ?? MANCER_SERVER_DEFAULT; let TOGETHERAI_SERVER = 'https://api.together.xyz'; let INFERMATICAI_SERVER = 'https://api.totalgpt.ai'; +let OPENROUTER_SERVER = 'https://openrouter.ai/api'; const SERVER_INPUTS = { [textgen_types.OOBA]: '#textgenerationwebui_api_url_text', @@ -137,6 +140,7 @@ const settings = { togetherai_model: 'Gryphe/MythoMax-L2-13b', infermaticai_model: '', ollama_model: '', + openrouter_model: 'openrouter/auto', legacy_api: false, sampler_order: KOBOLDCPP_ORDER, logit_bias: [], @@ -240,6 +244,10 @@ export function getTextGenServer() { return INFERMATICAI_SERVER; } + if (settings.type === OPENROUTER) { + return OPENROUTER_SERVER; + } + return settings.server_urls[settings.type] ?? ''; } @@ -264,7 +272,7 @@ async function selectPreset(name) { function formatTextGenURL(value) { try { // Mancer/Together/InfermaticAI doesn't need any formatting (it's hardcoded) - if (settings.type === MANCER || settings.type === TOGETHERAI || settings.type === INFERMATICAI) { + if (settings.type === MANCER || settings.type === TOGETHERAI || settings.type === INFERMATICAI || settings.type === OPENROUTER) { return value; } @@ -297,6 +305,10 @@ function getTokenizerForTokenIds() { return power_user.tokenizer; } + if (settings.type === OPENROUTER) { + return getCurrentOpenRouterModelTokenizer(); + } + return tokenizers.LLAMA; } @@ -922,6 +934,10 @@ function getModel() { return settings.infermaticai_model; } + if (settings.type === OPENROUTER) { + return settings.openrouter_model; + } + if (settings.type === APHRODITE) { return online_status; } diff --git a/public/scripts/tokenizers.js b/public/scripts/tokenizers.js index ac05be8b7..9fcc8f6e9 100644 --- a/public/scripts/tokenizers.js +++ b/public/scripts/tokenizers.js @@ -5,8 +5,9 @@ import { groups, selected_group } from './group-chats.js'; import { getStringHash } from './utils.js'; import { kai_flags } from './kai-settings.js'; import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer } from './textgen-settings.js'; +import { getCurrentOpenRouterModelTokenizer, openRouterModels } from './textgen-models.js'; -const { OOBA, TABBY, KOBOLDCPP, APHRODITE, LLAMACPP } = textgen_types; +const { OOBA, TABBY, KOBOLDCPP, APHRODITE, LLAMACPP, OPENROUTER } = textgen_types; export const CHARACTERS_PER_TOKEN_RATIO = 3.35; const TOKENIZER_WARNING_KEY = 'tokenizationWarningShown'; @@ -202,6 +203,9 @@ export function getTokenizerBestMatch(forApi) { if (forApi === 'textgenerationwebui' && isTokenizerSupported) { return tokenizers.API_TEXTGENERATIONWEBUI; } + if (forApi === 'textgenerationwebui' && textgen_settings.type === OPENROUTER) { + return getCurrentOpenRouterModelTokenizer(); + } } return tokenizers.LLAMA; @@ -349,8 +353,11 @@ export function getTokenizerModel() { } // And for OpenRouter (if not a site model, then it's impossible to determine the tokenizer) - if (oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER && oai_settings.openrouter_model) { - const model = model_list.find(x => x.id === oai_settings.openrouter_model); + if (main_api == 'openai' && oai_settings.chat_completion_source == chat_completion_sources.OPENROUTER && oai_settings.openrouter_model || + main_api == 'textgenerationwebui' && textgen_settings.type === OPENROUTER && textgen_settings.openrouter_model) { + const model = main_api == 'openai' + ? model_list.find(x => x.id === oai_settings.openrouter_model) + : openRouterModels.find(x => x.id === textgen_settings.openrouter_model); if (model?.architecture?.tokenizer === 'Llama2') { return llamaTokenizer; diff --git a/public/scripts/world-info.js b/public/scripts/world-info.js index 9c8295027..5352fce5a 100644 --- a/public/scripts/world-info.js +++ b/public/scripts/world-info.js @@ -1542,7 +1542,7 @@ function getWorldEntry(name, data, entry) { return; } - data.entries[uid].scanDepth = !isEmpty && !isNaN(value) && value >= 0 && value < MAX_SCAN_DEPTH ? Math.floor(value) : null; + data.entries[uid].scanDepth = !isEmpty && !isNaN(value) && value >= 0 && value <= MAX_SCAN_DEPTH ? Math.floor(value) : null; setOriginalDataValue(data, uid, 'extensions.scan_depth', data.entries[uid].scanDepth); saveWorldInfo(name, data); }); diff --git a/src/additional-headers.js b/src/additional-headers.js index 43aa4c44b..c1ae260c7 100644 --- a/src/additional-headers.js +++ b/src/additional-headers.js @@ -1,4 +1,4 @@ -const { TEXTGEN_TYPES } = require('./constants'); +const { TEXTGEN_TYPES, OPENROUTER_HEADERS } = require('./constants'); const { SECRET_KEYS, readSecret } = require('./endpoints/secrets'); const { getConfigValue } = require('./util'); @@ -27,6 +27,13 @@ function getInfermaticAIHeaders() { }) : {}; } +function getOpenRouterHeaders() { + const apiKey = readSecret(SECRET_KEYS.OPENROUTER); + const baseHeaders = { ...OPENROUTER_HEADERS }; + + return apiKey ? Object.assign(baseHeaders, { 'Authorization': `Bearer ${apiKey}` }) : baseHeaders; +} + function getAphroditeHeaders() { const apiKey = readSecret(SECRET_KEYS.APHRODITE); @@ -91,6 +98,9 @@ function setAdditionalHeaders(request, args, server) { case TEXTGEN_TYPES.INFERMATICAI: headers = getInfermaticAIHeaders(); break; + case TEXTGEN_TYPES.OPENROUTER: + headers = getOpenRouterHeaders(); + break; default: headers = server ? getOverrideHeaders((new URL(server))?.host) : {}; break; diff --git a/src/constants.js b/src/constants.js index 08ffdcd70..67939f90f 100644 --- a/src/constants.js +++ b/src/constants.js @@ -177,6 +177,7 @@ const TEXTGEN_TYPES = { LLAMACPP: 'llamacpp', OLLAMA: 'ollama', INFERMATICAI: 'infermaticai', + OPENROUTER: 'openrouter', }; const INFERMATICAI_KEYS = [ @@ -226,6 +227,29 @@ const OLLAMA_KEYS = [ const AVATAR_WIDTH = 400; const AVATAR_HEIGHT = 600; +const OPENROUTER_HEADERS = { + 'HTTP-Referer': 'https://sillytavern.app', + 'X-Title': 'SillyTavern', +}; + +const OPENROUTER_KEYS = [ + 'max_tokens', + 'temperature', + 'top_k', + 'top_p', + 'presence_penalty', + 'frequency_penalty', + 'repetition_penalty', + 'min_p', + 'top_a', + 'seed', + 'logit_bias', + 'model', + 'stream', + 'prompt', + 'stop', +]; + module.exports = { DIRECTORIES, UNSAFE_EXTENSIONS, @@ -239,4 +263,6 @@ module.exports = { TOGETHERAI_KEYS, OLLAMA_KEYS, INFERMATICAI_KEYS, + OPENROUTER_HEADERS, + OPENROUTER_KEYS, }; diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index b82a4391d..e4f936878 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -3,7 +3,7 @@ const fetch = require('node-fetch').default; const { Readable } = require('stream'); const { jsonParser } = require('../../express-common'); -const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY } = require('../../constants'); +const { CHAT_COMPLETION_SOURCES, GEMINI_SAFETY, BISON_SAFETY, OPENROUTER_HEADERS } = require('../../constants'); const { forwardFetchResponse, getConfigValue, tryParse, uuidv4, mergeObjectWithYaml, excludeKeysByYaml, color } = require('../../util'); const { convertClaudePrompt, convertGooglePrompt, convertTextCompletionPrompt } = require('../prompt-converters'); @@ -419,6 +419,9 @@ async function sendMistralAIRequest(request, response) { try { //must send a user role as last message const messages = Array.isArray(request.body.messages) ? request.body.messages : []; + //large seems to be throwing a 500 error if we don't make the first message a user role, most likely a bug since the other models won't do this + if (request.body.model.includes('large')) + messages[0].role = 'user'; const lastMsg = messages[messages.length - 1]; if (messages.length > 0 && lastMsg && (lastMsg.role === 'system' || lastMsg.role === 'assistant')) { if (lastMsg.role === 'assistant' && lastMsg.name) { @@ -514,10 +517,7 @@ router.post('/status', jsonParser, async function (request, response_getstatus_o api_url = 'https://openrouter.ai/api/v1'; api_key_openai = readSecret(SECRET_KEYS.OPENROUTER); // OpenRouter needs to pass the Referer and X-Title: https://openrouter.ai/docs#requests - headers = { - 'HTTP-Referer': 'https://sillytavern.app', - 'X-Title': 'SillyTavern', - }; + headers = { ...OPENROUTER_HEADERS }; } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MISTRALAI) { api_url = new URL(request.body.reverse_proxy || API_MISTRAL).toString(); api_key_openai = request.body.reverse_proxy ? request.body.proxy_password : readSecret(SECRET_KEYS.MISTRALAI); @@ -704,10 +704,7 @@ router.post('/generate', jsonParser, function (request, response) { apiUrl = 'https://openrouter.ai/api/v1'; apiKey = readSecret(SECRET_KEYS.OPENROUTER); // OpenRouter needs to pass the Referer and X-Title: https://openrouter.ai/docs#requests - headers = { - 'HTTP-Referer': 'https://sillytavern.app', - 'X-Title': 'SillyTavern', - }; + headers = { ...OPENROUTER_HEADERS }; bodyParams = { 'transforms': ['middle-out'] }; if (request.body.min_p !== undefined) { diff --git a/src/endpoints/backends/text-completions.js b/src/endpoints/backends/text-completions.js index 6c17883e5..d713a5aea 100644 --- a/src/endpoints/backends/text-completions.js +++ b/src/endpoints/backends/text-completions.js @@ -4,7 +4,7 @@ const _ = require('lodash'); const Readable = require('stream').Readable; const { jsonParser } = require('../../express-common'); -const { TEXTGEN_TYPES, TOGETHERAI_KEYS, OLLAMA_KEYS, INFERMATICAI_KEYS } = require('../../constants'); +const { TEXTGEN_TYPES, TOGETHERAI_KEYS, OLLAMA_KEYS, INFERMATICAI_KEYS, OPENROUTER_KEYS } = require('../../constants'); const { forwardFetchResponse, trimV1 } = require('../../util'); const { setAdditionalHeaders } = require('../../additional-headers'); @@ -107,6 +107,7 @@ router.post('/status', jsonParser, async function (request, response) { case TEXTGEN_TYPES.KOBOLDCPP: case TEXTGEN_TYPES.LLAMACPP: case TEXTGEN_TYPES.INFERMATICAI: + case TEXTGEN_TYPES.OPENROUTER: url += '/v1/models'; break; case TEXTGEN_TYPES.MANCER: @@ -209,6 +210,7 @@ router.post('/generate', jsonParser, async function (request, response) { request.body.api_server = request.body.api_server.replace('localhost', '127.0.0.1'); } + const apiType = request.body.api_type; const baseUrl = request.body.api_server; console.log(request.body); @@ -245,6 +247,9 @@ router.post('/generate', jsonParser, async function (request, response) { case TEXTGEN_TYPES.OLLAMA: url += '/api/generate'; break; + case TEXTGEN_TYPES.OPENROUTER: + url += '/v1/chat/completions'; + break; } } @@ -268,11 +273,17 @@ router.post('/generate', jsonParser, async function (request, response) { args.body = JSON.stringify(request.body); } + if (request.body.api_type === TEXTGEN_TYPES.OPENROUTER) { + request.body = _.pickBy(request.body, (_, key) => OPENROUTER_KEYS.includes(key)); + args.body = JSON.stringify(request.body); + } + if (request.body.api_type === TEXTGEN_TYPES.OLLAMA) { args.body = JSON.stringify({ model: request.body.model, prompt: request.body.prompt, stream: request.body.stream ?? false, + keep_alive: -1, raw: true, options: _.pickBy(request.body, (_, key) => OLLAMA_KEYS.includes(key)), }); @@ -300,7 +311,7 @@ router.post('/generate', jsonParser, async function (request, response) { } // Map InfermaticAI response to OAI completions format - if (completionsReply.url.includes('https://api.totalgpt.ai')) { + if (apiType === TEXTGEN_TYPES.INFERMATICAI) { data['choices'] = (data?.choices || []).map(choice => ({ text: choice.message.content })); }