From 2c7b954a8dbf1f30bfbab561eaac240739000606 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Wed, 8 Nov 2023 00:17:13 +0200 Subject: [PATCH] #1328 New API schema for ooba / mancer / aphrodite --- default/settings.json | 1 - public/index.html | 16 +- public/script.js | 88 ++++--- public/scripts/mancer-settings.js | 22 +- public/scripts/preset-manager.js | 1 + public/scripts/textgen-settings.js | 152 +++++++------ server.js | 354 ++++++++++++----------------- 7 files changed, 293 insertions(+), 341 deletions(-) diff --git a/default/settings.json b/default/settings.json index e81817719..fd4edfda7 100644 --- a/default/settings.json +++ b/default/settings.json @@ -49,7 +49,6 @@ "ban_eos_token": false, "skip_special_tokens": true, "streaming": false, - "streaming_url": "ws://127.0.0.1:5005/api/v1/stream", "mirostat_mode": 0, "mirostat_tau": 5, "mirostat_eta": 0.1, diff --git a/public/index.html b/public/index.html index 79ecb9f4d..7cdff6f3b 100644 --- a/public/index.html +++ b/public/index.html @@ -1793,9 +1793,6 @@

Mancer Model

-

Mancer API URL

- Example: https://neuro.mancer.tech/webui/MODEL/api -
@@ -1808,15 +1805,10 @@
-

Blocking API URL

- Example: http://127.0.0.1:5000/api +

Server URL

+ Example: http://127.0.0.1:5000
-
-

Streaming API URL

- Example: ws://127.0.0.1:5005/api/v1/stream - -
@@ -1835,11 +1827,11 @@

API URL

- Example: http://127.0.0.1:5000/api + Example: http://127.0.0.1:5000
- +
diff --git a/public/script.js b/public/script.js index 75aaad7d1..938756992 100644 --- a/public/script.js +++ b/public/script.js @@ -21,6 +21,8 @@ import { isAphrodite, textgen_types, textgenerationwebui_banned_in_macros, + isOoba, + MANCER_SERVER, } from "./scripts/textgen-settings.js"; import { @@ -187,7 +189,7 @@ import { getFriendlyTokenizerName, getTokenCount, getTokenizerModel, initTokeniz import { initPersonas, selectCurrentPersona, setPersonaDescription } from "./scripts/personas.js"; import { getBackgrounds, initBackgrounds } from "./scripts/backgrounds.js"; import { hideLoader, showLoader } from "./scripts/loader.js"; -import {CharacterContextMenu, BulkEditOverlay} from "./scripts/BulkEditOverlay.js"; +import { CharacterContextMenu, BulkEditOverlay } from "./scripts/BulkEditOverlay.js"; //exporting functions and vars for mods export { @@ -884,14 +886,27 @@ async function getStatus() { return; } + const url = main_api == "textgenerationwebui" ? '/api/textgenerationwebui/status' : '/getstatus'; + + let endpoint = api_server; + + if (main_api == "textgenerationwebui") { + endpoint = api_server_textgenerationwebui; + } + + if (main_api == "textgenerationwebui" && isMancer()) { + endpoint = MANCER_SERVER + } + jQuery.ajax({ type: "POST", // - url: "/getstatus", // + url: url, // data: JSON.stringify({ - api_server: main_api == "kobold" ? api_server : api_server_textgenerationwebui, main_api: main_api, + api_server: endpoint, use_mancer: main_api == "textgenerationwebui" ? isMancer() : false, use_aphrodite: main_api == "textgenerationwebui" ? isAphrodite() : false, + use_ooba: main_api == "textgenerationwebui" ? isOoba() : false, }), beforeSend: function () { }, cache: false, @@ -900,8 +915,13 @@ async function getStatus() { contentType: "application/json", //processData: false, success: function (data) { - online_status = data.result; - if (online_status == undefined) { + if (main_api == "textgenerationwebui" && isMancer()) { + online_status = textgenerationwebui_settings.mancer_model; + } else { + online_status = data.result; + } + + if (!online_status) { online_status = "no_connection"; } @@ -914,11 +934,10 @@ async function getStatus() { } // We didn't get a 200 status code, but the endpoint has an explanation. Which means it DID connect, but I digress. - if (online_status == "no_connection" && data.response) { + if (online_status === "no_connection" && data.response) { toastr.error(data.response, "API Error", { timeOut: 5000, preventDuplicates: true }) } - //console.log(online_status); resultCheckStatus(); }, error: function (jqXHR, exception) { @@ -3510,11 +3529,8 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject, //console.log('runGenerate calling showSwipeBtns'); showSwipeButtons(); - if (main_api == 'textgenerationwebui' && isMancer()) { - const errorText = `

Inferencer endpoint is unhappy!

- Returned status ${data.status} with the reason:
- ${data.response}`; - callPopup(errorText, 'text'); + if (data?.response) { + toastr.error(data.response, 'API Error'); } } console.debug('/savechat called by /Generate'); @@ -4025,7 +4041,7 @@ function getGenerateUrl(api) { if (api == 'kobold') { generate_url = '/generate'; } else if (api == 'textgenerationwebui') { - generate_url = '/generate_textgenerationwebui'; + generate_url = '/api/textgenerationwebui/generate'; } else if (api == 'novel') { generate_url = '/api/novelai/generate'; } @@ -4054,7 +4070,7 @@ function extractMessageFromData(data) { case 'koboldhorde': return data.text; case 'textgenerationwebui': - return data.results[0].text; + return data.choices[0].text; case 'novel': return data.output; case 'openai': @@ -5265,7 +5281,6 @@ async function getSettings(type) { api_server_textgenerationwebui = settings.api_server_textgenerationwebui; $("#textgenerationwebui_api_url_text").val(api_server_textgenerationwebui); - $("#mancer_api_url_text").val(api_server_textgenerationwebui); $("#aphrodite_api_url_text").val(api_server_textgenerationwebui); selected_button = settings.selected_button; @@ -7835,36 +7850,37 @@ jQuery(async function () { }); $("#api_button_textgenerationwebui").on('click', async function (e) { + const mancerKey = String($("#api_key_mancer").val()).trim(); + if (mancerKey.length) { + await writeSecret(SECRET_KEYS.MANCER, mancerKey); + } + + const aphroditeKey = String($("#api_key_aphrodite").val()).trim(); + if (aphroditeKey.length) { + await writeSecret(SECRET_KEYS.APHRODITE, aphroditeKey); + } + const urlSourceId = getTextGenUrlSourceId(); - if ($(urlSourceId).val() != "") { - let value = formatTextGenURL(String($(urlSourceId).val()).trim(), isMancer()); + if (urlSourceId && $(urlSourceId).val() !== "") { + let value = formatTextGenURL(String($(urlSourceId).val()).trim()); if (!value) { - callPopup("Please enter a valid URL.
WebUI URLs should end with /api
Enable 'Relaxed API URLs' to allow other paths.", 'text'); + callPopup("Please enter a valid URL.", 'text'); return; } - const mancerKey = String($("#api_key_mancer").val()).trim(); - if (mancerKey.length) { - await writeSecret(SECRET_KEYS.MANCER, mancerKey); - } - - const aphroditeKey = String($("#api_key_aphrodite").val()).trim(); - if (aphroditeKey.length) { - await writeSecret(SECRET_KEYS.APHRODITE, aphroditeKey); - } - $(urlSourceId).val(value); - $("#api_loading_textgenerationwebui").css("display", "inline-block"); - $("#api_button_textgenerationwebui").css("display", "none"); - api_server_textgenerationwebui = value; - main_api = "textgenerationwebui"; - saveSettingsDebounced(); - is_get_status = true; - is_api_button_press = true; - getStatus(); } + + $("#api_loading_textgenerationwebui").css("display", "inline-block"); + $("#api_button_textgenerationwebui").css("display", "none"); + + main_api = "textgenerationwebui"; + saveSettingsDebounced(); + is_get_status = true; + is_api_button_press = true; + getStatus(); }); var button = $('#options_button'); diff --git a/public/scripts/mancer-settings.js b/public/scripts/mancer-settings.js index a6cfd9ca0..991fad87d 100644 --- a/public/scripts/mancer-settings.js +++ b/public/scripts/mancer-settings.js @@ -1,15 +1,9 @@ -import { api_server_textgenerationwebui, getRequestHeaders, setGenerationParamsFromPreset } from "../script.js"; +import { getRequestHeaders, setGenerationParamsFromPreset } from "../script.js"; import { getDeviceInfo } from "./RossAscends-mods.js"; +import { textgenerationwebui_settings } from "./textgen-settings.js"; let models = []; -/** - * @param {string} modelId - */ -export function getMancerModelURL(modelId) { - return `https://neuro.mancer.tech/webui/${modelId}/api`; -} - export async function loadMancerModels() { try { const response = await fetch('/api/mancer/models', { @@ -29,7 +23,7 @@ export async function loadMancerModels() { const option = document.createElement('option'); option.value = model.id; option.text = model.name; - option.selected = api_server_textgenerationwebui === getMancerModelURL(model.id); + option.selected = model.id === textgenerationwebui_settings.mancer_model; $('#mancer_model').append(option); } @@ -40,12 +34,11 @@ export async function loadMancerModels() { function onMancerModelSelect() { const modelId = String($('#mancer_model').val()); - const url = getMancerModelURL(modelId); - $('#mancer_api_url_text').val(url); + textgenerationwebui_settings.mancer_model = modelId; $('#api_button_textgenerationwebui').trigger('click'); - const context = models.find(x => x.id === modelId)?.context; - setGenerationParamsFromPreset({ max_length: context }); + const limits = models.find(x => x.id === modelId)?.limits; + setGenerationParamsFromPreset({ max_length: limits.context, genamt: limits.completion }); } function getMancerModelTemplate(option) { @@ -57,8 +50,7 @@ function getMancerModelTemplate(option) { return $((`
-
${DOMPurify.sanitize(model.name)} | ${model.context} ctx
- ${DOMPurify.sanitize(model.description)} +
${DOMPurify.sanitize(model.name)} | ${model.limits?.context} ctx
`)); } diff --git a/public/scripts/preset-manager.js b/public/scripts/preset-manager.js index 0d75f40b2..4df5ea249 100644 --- a/public/scripts/preset-manager.js +++ b/public/scripts/preset-manager.js @@ -263,6 +263,7 @@ class PresetManager { 'streaming_kobold', "enabled", 'seed', + 'mancer_model', ]; const settings = Object.assign({}, getSettingsByApiId(this.apiId)); diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index 55dedefc9..5fb77b93f 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -3,6 +3,7 @@ import { getRequestHeaders, getStoppingStrings, max_context, + online_status, saveSettingsDebounced, setGenerationParamsFromPreset, } from "../script.js"; @@ -12,7 +13,7 @@ import { power_user, } from "./power-user.js"; import { getTextTokens, tokenizers } from "./tokenizers.js"; -import { delay, onlyUnique } from "./utils.js"; +import { onlyUnique } from "./utils.js"; export { textgenerationwebui_settings, @@ -27,6 +28,9 @@ export const textgen_types = { APHRODITE: 'aphrodite', }; +// Maybe let it be configurable in the future? +export const MANCER_SERVER = 'https://neuro.mancer.tech'; + const textgenerationwebui_settings = { temp: 0.7, temperature_last: true, @@ -58,7 +62,6 @@ const textgenerationwebui_settings = { ban_eos_token: false, skip_special_tokens: true, streaming: false, - streaming_url: 'ws://127.0.0.1:5005/api/v1/stream', mirostat_mode: 0, mirostat_tau: 5, mirostat_eta: 0.1, @@ -74,6 +77,7 @@ const textgenerationwebui_settings = { //log_probs_aphrodite: 0, //prompt_log_probs_aphrodite: 0, type: textgen_types.OOBA, + mancer_model: 'mytholite', }; export let textgenerationwebui_banned_in_macros = []; @@ -109,7 +113,6 @@ const setting_names = [ "ban_eos_token", "skip_special_tokens", "streaming", - "streaming_url", "mirostat_mode", "mirostat_tau", "mirostat_eta", @@ -142,17 +145,12 @@ async function selectPreset(name) { saveSettingsDebounced(); } -function formatTextGenURL(value, use_mancer) { +function formatTextGenURL(value) { try { const url = new URL(value); - if (!power_user.relaxed_api_urls) { - if (use_mancer) { // If Mancer is in use, only require the URL to *end* with `/api`. - if (!url.pathname.endsWith('/api')) { - return null; - } - } else { - url.pathname = '/api'; - } + if (url.pathname === '/api') { + url.pathname = '/'; + toastr.info('Legacy API URL detected, please make sure you updated ooba-webui to the latest version.'); } return url.toString(); } catch { } // Just using URL as a validation check @@ -255,8 +253,6 @@ export function isOoba() { export function getTextGenUrlSourceId() { switch (textgenerationwebui_settings.type) { - case textgen_types.MANCER: - return "#mancer_api_url_text"; case textgen_types.OOBA: return "#textgenerationwebui_api_url_text"; case textgen_types.APHRODITE: @@ -371,33 +367,11 @@ function setSettingByName(i, value, trigger) { } async function generateTextGenWithStreaming(generate_data, signal) { - let streamingUrl = textgenerationwebui_settings.streaming_url; + generate_data.stream = true; - if (isMancer()) { - streamingUrl = api_server_textgenerationwebui.replace("http", "ws") + "/v1/stream"; - } - - if (isAphrodite()) { - streamingUrl = api_server_textgenerationwebui; - } - - if (isMancer() || isOoba()) { - try { - const parsedUrl = new URL(streamingUrl); - if (parsedUrl.protocol !== 'ws:' && parsedUrl.protocol !== 'wss:') { - throw new Error('Invalid protocol'); - } - } catch { - toastr.error('Invalid URL for streaming. Make sure it starts with ws:// or wss://'); - return async function* () { throw new Error('Invalid URL for streaming.'); } - } - } - - const response = await fetch('/generate_textgenerationwebui', { + const response = await fetch('/api/textgenerationwebui/generate', { headers: { ...getRequestHeaders(), - 'X-Response-Streaming': String(true), - 'X-Streaming-URL': streamingUrl, }, body: JSON.stringify(generate_data), method: 'POST', @@ -408,54 +382,93 @@ async function generateTextGenWithStreaming(generate_data, signal) { const decoder = new TextDecoder(); const reader = response.body.getReader(); let getMessage = ''; + let messageBuffer = ""; while (true) { const { done, value } = await reader.read(); - let response = decoder.decode(value); + // We don't want carriage returns in our messages + let response = decoder.decode(value).replace(/\r/g, ""); - if (isAphrodite()) { - const events = response.split('\n\n'); + tryParseStreamingError(response); - for (const event of events) { - if (event.length == 0) { - continue; - } + let eventList = []; - try { - const { results } = JSON.parse(event); + messageBuffer += response; + eventList = messageBuffer.split("\n\n"); + // Last element will be an empty string or a leftover partial message + messageBuffer = eventList.pop(); - if (Array.isArray(results) && results.length > 0) { - getMessage = results[0].text; - yield getMessage; - - // unhang UI thread - await delay(1); - } - } catch { - // Ignore - } + for (let event of eventList) { + if (event.startsWith('event: completion')) { + event = event.split("\n")[1]; } - if (done) { + if (typeof event !== 'string' || !event.length) + continue; + + if (!event.startsWith("data")) + continue; + if (event == "data: [DONE]") { return; } - } else { - - getMessage += response; - - if (done) { - return; - } - + let data = JSON.parse(event.substring(6)); + // the first and last messages are undefined, protect against that + getMessage += data?.choices[0]?.text || ''; yield getMessage; } + + if (done) { + return; + } } } } +/** + * Parses errors in streaming responses and displays them in toastr. + * @param {string} response - Response from the server. + * @returns {void} Nothing. + */ +function tryParseStreamingError(response) { + let data = {}; + + try { + data = JSON.parse(response); + } catch { + // No JSON. Do nothing. + } + + if (data?.error?.message) { + toastr.error(data.error.message, 'API Error'); + throw new Error(data.error.message); + } +} + +function toIntArray(string) { + if (!string) { + return []; + } + + return string.split(',').map(x => parseInt(x)).filter(x => !isNaN(x)); +} + +function getModel() { + if (isMancer()) { + return textgenerationwebui_settings.mancer_model; + } + + if (isAphrodite()) { + return online_status; + } + + return undefined; +} + export function getTextGenGenerationData(finalPrompt, this_amount_gen, isImpersonate, cfgValues) { return { 'prompt': finalPrompt, + 'model': getModel(), 'max_new_tokens': this_amount_gen, + 'max_tokens': this_amount_gen, 'do_sample': textgenerationwebui_settings.do_sample, 'temperature': textgenerationwebui_settings.temp, 'temperature_last': textgenerationwebui_settings.temperature_last, @@ -469,6 +482,7 @@ export function getTextGenGenerationData(finalPrompt, this_amount_gen, isImperso 'presence_penalty': textgenerationwebui_settings.presence_pen, 'top_k': textgenerationwebui_settings.top_k, 'min_length': textgenerationwebui_settings.min_length, + 'min_tokens': textgenerationwebui_settings.min_length, 'no_repeat_ngram_size': textgenerationwebui_settings.no_repeat_ngram_size, 'num_beams': textgenerationwebui_settings.num_beams, 'penalty_alpha': textgenerationwebui_settings.penalty_alpha, @@ -479,6 +493,7 @@ export function getTextGenGenerationData(finalPrompt, this_amount_gen, isImperso 'seed': textgenerationwebui_settings.seed, 'add_bos_token': textgenerationwebui_settings.add_bos_token, 'stopping_strings': getStoppingStrings(isImpersonate), + 'stop': getStoppingStrings(isImpersonate), 'truncation_length': max_context, 'ban_eos_token': textgenerationwebui_settings.ban_eos_token, 'skip_special_tokens': textgenerationwebui_settings.skip_special_tokens, @@ -490,9 +505,11 @@ export function getTextGenGenerationData(finalPrompt, this_amount_gen, isImperso 'mirostat_tau': textgenerationwebui_settings.mirostat_tau, 'mirostat_eta': textgenerationwebui_settings.mirostat_eta, 'grammar_string': textgenerationwebui_settings.grammar_string, - 'custom_token_bans': getCustomTokenBans(), + 'custom_token_bans': isAphrodite() ? toIntArray(getCustomTokenBans()) : getCustomTokenBans(), 'use_mancer': isMancer(), 'use_aphrodite': isAphrodite(), + 'use_ooba': isOoba(), + 'api_server': isMancer() ? MANCER_SERVER : api_server_textgenerationwebui, //'n': textgenerationwebui_settings.n_aphrodite, //'best_of': textgenerationwebui_settings.n_aphrodite, //n must always == best_of and vice versa //'ignore_eos': textgenerationwebui_settings.ignore_eos_token_aphrodite, @@ -502,3 +519,4 @@ export function getTextGenGenerationData(finalPrompt, this_amount_gen, isImperso //'prompt_logprobs': textgenerationwebui_settings.prompt_log_probs_aphrodite, }; } + diff --git a/server.js b/server.js index c721e186f..95d24f6a0 100644 --- a/server.js +++ b/server.js @@ -9,7 +9,6 @@ const path = require('path'); const readline = require('readline'); const util = require('util'); const { Readable } = require('stream'); -const { TextDecoder } = require('util'); // cli/fs related library imports const open = require('open'); @@ -35,7 +34,6 @@ const fetch = require('node-fetch').default; const ipaddr = require('ipaddr.js'); const ipMatching = require('ip-matching'); const json5 = require('json5'); -const WebSocket = require('ws'); // image processing related library imports const encode = require('png-chunks-encode'); @@ -57,7 +55,7 @@ const characterCardParser = require('./src/character-card-parser.js'); const contentManager = require('./src/content-manager'); const statsHelpers = require('./statsHelpers.js'); const { readSecret, migrateSecrets, SECRET_KEYS } = require('./src/secrets'); -const { delay, getVersion, deepMerge} = require('./src/util'); +const { delay, getVersion, deepMerge } = require('./src/util'); const { invalidateThumbnail, ensureThumbnailCache } = require('./src/thumbnails'); const { getTokenizerModel, getTiktokenTokenizer, loadTokenizers, TEXT_COMPLETION_MODELS } = require('./src/tokenizers'); const { convertClaudePrompt } = require('./src/chat-completion'); @@ -150,12 +148,20 @@ let color = { function getMancerHeaders() { const apiKey = readSecret(SECRET_KEYS.MANCER); - return apiKey ? { "X-API-KEY": apiKey } : {}; + + return apiKey ? ({ + "X-API-KEY": apiKey, + "Authorization": `Bearer ${apiKey}`, + }) : {}; } function getAphroditeHeaders() { const apiKey = readSecret(SECRET_KEYS.APHRODITE); - return apiKey ? { "X-API-KEY": apiKey } : {}; + + return apiKey ? ({ + "X-API-KEY": apiKey, + "Authorization": `Bearer ${apiKey}`, + }) : {}; } function getOverrideHeaders(urlHost) { @@ -181,7 +187,7 @@ function setAdditionalHeaders(request, args, server) { } else if (request.body.use_aphrodite) { headers = getAphroditeHeaders(); } else { - headers = server ? getOverrideHeaders((new URL(server))?.host) : ''; + headers = server ? getOverrideHeaders((new URL(server))?.host) : {}; } args.headers = Object.assign(args.headers, headers); @@ -208,7 +214,7 @@ const AVATAR_HEIGHT = 600; const jsonParser = express.json({ limit: '100mb' }); const urlencodedParser = express.urlencoded({ extended: true, limit: '100mb' }); const { DIRECTORIES, UPLOADS_PATH, PALM_SAFETY } = require('./src/constants'); -const {TavernCardValidator} = require("./src/validator/TavernCardValidator"); +const { TavernCardValidator } = require("./src/validator/TavernCardValidator"); // CSRF Protection // if (cliArguments.disableCsrf === false) { @@ -479,215 +485,146 @@ app.post("/generate", jsonParser, async function (request, response_generate) { return response_generate.send({ error: true }); }); -/** - * @param {string} streamingUrlString Streaming URL - * @param {import('express').Request} request Express request - * @param {import('express').Response} response Express response - * @param {AbortController} controller Abort controller - * @returns - */ -async function sendAphroditeStreamingRequest(streamingUrlString, request, response, controller) { - request.body['stream'] = true; - - const args = { - method: 'POST', - body: JSON.stringify(request.body), - headers: { "Content-Type": "application/json" }, - signal: controller.signal, - }; - - setAdditionalHeaders(request, args, streamingUrlString); +//************** Text generation web UI +app.post("/api/textgenerationwebui/status", jsonParser, async function (request, response) { + if (!request.body) return response.sendStatus(400); try { - const generateResponse = await fetch(streamingUrlString + "/v1/generate", args); - // Pipe remote SSE stream to Express response - generateResponse.body.pipe(response); - - request.socket.on('close', function () { - if (generateResponse.body instanceof Readable) generateResponse.body.destroy(); // Close the remote stream - response.end(); // End the Express response - }); - - generateResponse.body.on('end', function () { - console.log("Streaming request finished"); - response.end(); - }); - } catch (error) { - let value = { error: true, status: error.status, response: error.statusText }; - console.log("Aphrodite endpoint error:", error); - - if (!response.headersSent) { - return response.send(value); - } else { - return response.end(); - } - } - -} - -//************** Text generation web UI -app.post("/generate_textgenerationwebui", jsonParser, async function (request, response_generate) { - if (!request.body) return response_generate.sendStatus(400); - - console.log(request.body); - - const controller = new AbortController(); - let isGenerationStopped = false; - request.socket.removeAllListeners('close'); - request.socket.on('close', function () { - isGenerationStopped = true; - controller.abort(); - }); - - if (request.header('X-Response-Streaming')) { - const streamingUrlHeader = request.header('X-Streaming-URL'); - if (streamingUrlHeader === undefined) return response_generate.sendStatus(400); - const streamingUrlString = streamingUrlHeader.replace("localhost", "127.0.0.1"); - - if (request.body.use_aphrodite) { - return sendAphroditeStreamingRequest(streamingUrlString, request, response_generate, controller); + if (request.body.api_server.indexOf('localhost') !== -1) { + request.body.api_server = request.body.api_server.replace('localhost', '127.0.0.1'); } - response_generate.writeHead(200, { - 'Content-Type': 'text/plain;charset=utf-8', - 'Transfer-Encoding': 'chunked', - 'Cache-Control': 'no-transform', - }); + console.log('Trying to connect to API:', request.body); - async function* readWebsocket() { - /** @type {WebSocket} */ - let websocket; - /** @type {URL} */ - let streamingUrl; + const baseUrl = request.body.api_server; - try { - const streamingUrl = new URL(streamingUrlString); - websocket = new WebSocket(streamingUrl); - } catch (error) { - console.log("[SillyTavern] Socket error", error); - return; - } - - websocket.on('open', async function () { - console.log('WebSocket opened'); - - let headers = {}; - - if (request.body.use_mancer) { - headers = getMancerHeaders(); - } else if (request.body.use_aphrodite) { - headers = getAphroditeHeaders(); - } else { - headers = getOverrideHeaders(streamingUrl?.host); - } - - const combined_args = Object.assign( - {}, - headers, - request.body - ); - console.log(combined_args); - - websocket.send(JSON.stringify(combined_args)); - }); - - websocket.on('close', (code, buffer) => { - const reason = new TextDecoder().decode(buffer) - console.log("WebSocket closed (reason: %o)", reason); - }); - - while (true) { - if (isGenerationStopped) { - console.error('Streaming stopped by user. Closing websocket...'); - websocket.close(); - return; - } - - let rawMessage = null; - try { - // This lunacy is because the websocket can fail to connect AFTER we're awaiting 'message'... so 'message' never triggers. - // So instead we need to look for 'error' at the same time to reject the promise. And then remove the listener if we resolve. - // This is awful. - // Welcome to the shenanigan shack. - rawMessage = await new Promise(function (resolve, reject) { - websocket.once('error', reject); - websocket.once('message', (data, isBinary) => { - websocket.removeListener('error', reject); - resolve(data); - }); - }); - } catch (err) { - console.error("Socket error:", err); - websocket.close(); - yield "[SillyTavern] Streaming failed:\n" + err; - return; - } - - const message = json5.parse(rawMessage); - - switch (message.event) { - case 'text_stream': - yield message.text; - break; - case 'stream_end': - if (message.error) { - yield `\n[API Error] ${message.error}\n` - } - websocket.close(); - return; - } - } - } - - let reply = ''; - - try { - for await (const text of readWebsocket()) { - if (typeof text !== 'string') { - break; - } - - let newText = text; - - if (!newText) { - continue; - } - - reply += text; - response_generate.write(newText); - } - - console.log(reply); - } - finally { - response_generate.end(); - } - } - else { const args = { - body: JSON.stringify(request.body), headers: { "Content-Type": "application/json" }, - signal: controller.signal, + timeout: 0, }; - setAdditionalHeaders(request, args, api_server); + setAdditionalHeaders(request, args, baseUrl); - try { - const data = await postAsync(api_server + "/v1/generate", args); - console.log("Endpoint response:", data); - return response_generate.send(data); - } catch (error) { - let retval = { error: true, status: error.status, response: error.statusText }; - console.log("Endpoint error:", error); - try { - retval.response = await error.json(); - retval.response = retval.response.result; - } catch { } - return response_generate.send(retval); + const url = new URL(baseUrl); + + if (request.body.use_ooba) { + url.pathname = "/v1/models"; } + + if (request.body.use_aphrodite) { + url.pathname = "/v1/models"; + } + + if (request.body.use_mancer) { + url.pathname = "/oai/v1/models"; + } + + const modelsReply = await fetch(url, args); + + if (!modelsReply.ok) { + console.log('Models endpoint is offline.'); + return response.status(modelsReply.status); + } + + const data = await modelsReply.json(); + + if (!Array.isArray(data.data)) { + console.log('Models response is not an array.') + return response.status(503); + } + + const modelIds = data.data.map(x => x.id); + console.log('Models available:', modelIds); + + const result = modelIds[0] ?? 'Valid'; + return response.send({ result }); + } catch (error) { + console.error(error); + return response.status(500); } }); +app.post("/api/textgenerationwebui/generate", jsonParser, async function (request, response_generate) { + if (!request.body) return response_generate.sendStatus(400); + + try { + if (request.body.api_server.indexOf('localhost') !== -1) { + request.body.api_server = request.body.api_server.replace('localhost', '127.0.0.1'); + } + + const baseUrl = request.body.api_server; + console.log(request.body); + + const controller = new AbortController(); + request.socket.removeAllListeners('close'); + request.socket.on('close', function () { + controller.abort(); + }); + + const url = new URL(baseUrl); + + if (request.body.use_aphrodite || request.body.use_ooba) { + url.pathname = "/v1/completions"; + } + + if (request.body.use_mancer) { + url.pathname = "/oai/v1/completions"; + } + + const args = { + method: 'POST', + body: JSON.stringify(request.body), + headers: { "Content-Type": "application/json" }, + signal: controller.signal, + timeout: 0, + }; + + setAdditionalHeaders(request, args, baseUrl); + + if (request.body.stream) { + const completionsStream = await fetch(url, args); + // Pipe remote SSE stream to Express response + completionsStream.body.pipe(response_generate); + + request.socket.on('close', function () { + if (completionsStream.body instanceof Readable) completionsStream.body.destroy(); // Close the remote stream + response_generate.end(); // End the Express response + }); + + completionsStream.body.on('end', function () { + console.log("Streaming request finished"); + response_generate.end(); + }); + } + else { + const completionsReply = await fetch(url, args); + + if (completionsReply.ok) { + const data = await completionsReply.json(); + console.log("Endpoint response:", data); + return response_generate.send(data); + } else { + const text = await completionsReply.text(); + const errorBody = { error: true, status: completionsReply.status, response: text }; + + if (!response_generate.headersSent) { + return response_generate.send(errorBody); + } + + return response_generate.end(); + } + } + } catch (error) { + let value = { error: true, status: error?.status, response: error?.statusText }; + console.log("Endpoint error:", error); + + if (!response_generate.headersSent) { + return response_generate.send(value); + } + + return response_generate.end(); + } +}); app.post("/savechat", jsonParser, function (request, response) { try { @@ -740,7 +677,7 @@ app.post("/getchat", jsonParser, function (request, response) { app.post("/api/mancer/models", jsonParser, async function (_req, res) { try { - const response = await fetch('https://mancer.tech/internal/api/models'); + const response = await fetch('https://neuro.mancer.tech/oai/v1/models'); const data = await response.json(); if (!response.ok) { @@ -748,15 +685,12 @@ app.post("/api/mancer/models", jsonParser, async function (_req, res) { return res.json([]); } - if (!Array.isArray(data.models)) { + if (!Array.isArray(data.data)) { console.log('Mancer models response is not an array.') return res.json([]); } - const modelIds = data.models.map(x => x.id); - console.log('Mancer models available:', modelIds); - - return res.json(data.models); + return res.json(data.data); } catch (error) { console.error(error); return res.json([]); @@ -1184,7 +1118,7 @@ app.post("/v2/editcharacterattribute", jsonParser, async function (request, resp const avatarPath = path.join(charactersPath, update.avatar); try { - let character = JSON.parse(await charaRead(avatarPath)); + let character = JSON.parse(await charaRead(avatarPath)); character = deepMerge(character, update); const validator = new TavernCardValidator(character); @@ -1200,10 +1134,10 @@ app.post("/v2/editcharacterattribute", jsonParser, async function (request, resp ); } else { console.log(validator.lastValidationError) - response.status(400).send({message: `Validation failed for ${character.name}`, error: validator.lastValidationError}); + response.status(400).send({ message: `Validation failed for ${character.name}`, error: validator.lastValidationError }); } } catch (exception) { - response.status(500).send({message: 'Unexpected error while saving character.', error: exception.toString()}); + response.status(500).send({ message: 'Unexpected error while saving character.', error: exception.toString() }); } });