diff --git a/public/index.html b/public/index.html index 99a53c5fe..ad52d580f 100644 --- a/public/index.html +++ b/public/index.html @@ -2029,6 +2029,7 @@ + @@ -2183,6 +2184,27 @@ +
+
+ + featherless.ai + +
+

API key

+
+ + +
+
+ For privacy reasons, your API key will be hidden after you reload the page. +
+ +
diff --git a/public/script.js b/public/script.js index ab9543751..8c9f10c6c 100644 --- a/public/script.js +++ b/public/script.js @@ -22,7 +22,7 @@ import { parseTabbyLogprobs, } from './scripts/textgen-settings.js'; -const { MANCER, TOGETHERAI, OOBA, VLLM, APHRODITE, TABBY, OLLAMA, INFERMATICAI, DREAMGEN, OPENROUTER } = textgen_types; +const { MANCER, TOGETHERAI, OOBA, VLLM, APHRODITE, TABBY, OLLAMA, INFERMATICAI, DREAMGEN, OPENROUTER, FEATHERLESS } = textgen_types; import { world_info, @@ -223,7 +223,7 @@ import { 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, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels } from './scripts/textgen-models.js'; +import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels } from './scripts/textgen-models.js'; import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId } from './scripts/chats.js'; import { initPresetManager } from './scripts/preset-manager.js'; import { MacrosParser, evaluateMacros } from './scripts/macros.js'; @@ -1160,6 +1160,9 @@ async function getStatusTextgen() { } else if (textgen_settings.type === APHRODITE) { loadAphroditeModels(data?.data); setOnlineStatus(textgen_settings.aphrodite_model); + } else if (textgen_settings.type === FEATHERLESS) { + loadFeatherlessModels(data?.data); + setOnlineStatus(textgen_settings.featherless_model); } else { setOnlineStatus(data?.result); } @@ -9476,6 +9479,7 @@ jQuery(async function () { { id: 'api_key_openrouter-tg', secret: SECRET_KEYS.OPENROUTER }, { id: 'api_key_koboldcpp', secret: SECRET_KEYS.KOBOLDCPP }, { id: 'api_key_llamacpp', secret: SECRET_KEYS.LLAMACPP }, + { id: 'api_key_featherless', secret: SECRET_KEYS.FEATHERLESS }, { id: 'api_key_huggingface', secret: SECRET_KEYS.HUGGINGFACE }, ]; diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 3a2761fd3..d8e652465 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -360,6 +360,7 @@ function RA_autoconnect(PrevApi) { || (textgen_settings.type === textgen_types.INFERMATICAI && secret_state[SECRET_KEYS.INFERMATICAI]) || (textgen_settings.type === textgen_types.DREAMGEN && secret_state[SECRET_KEYS.DREAMGEN]) || (textgen_settings.type === textgen_types.OPENROUTER && secret_state[SECRET_KEYS.OPENROUTER]) + || (textgen_settings.type === textgen_types.FEATHERLESS && secret_state[SECRET_KEYS.FEATHERLESS]) ) { $('#api_button_textgenerationwebui').trigger('click'); } diff --git a/public/scripts/preset-manager.js b/public/scripts/preset-manager.js index 28e4089d2..90fe232ba 100644 --- a/public/scripts/preset-manager.js +++ b/public/scripts/preset-manager.js @@ -329,6 +329,7 @@ class PresetManager { 'infermaticai_model', 'dreamgen_model', 'openrouter_model', + 'featherless_model', 'max_tokens_second', 'openrouter_providers', ]; diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js index 4410ed1ee..4b5e63289 100644 --- a/public/scripts/secrets.js +++ b/public/scripts/secrets.js @@ -28,6 +28,7 @@ export const SECRET_KEYS = { 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', }; @@ -58,6 +59,7 @@ const INPUT_MAP = { [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', }; diff --git a/public/scripts/textgen-models.js b/public/scripts/textgen-models.js index 5f663c816..a33c0d542 100644 --- a/public/scripts/textgen-models.js +++ b/public/scripts/textgen-models.js @@ -9,6 +9,7 @@ let infermaticAIModels = []; let dreamGenModels = []; let vllmModels = []; let aphroditeModels = []; +let featherlessModels = []; export let openRouterModels = []; /** @@ -233,6 +234,35 @@ export async function loadAphroditeModels(data) { } } +export async function loadFeatherlessModels(data) { + if (!Array.isArray(data)) { + console.error('Invalid Featherless models data', data); + return; + } + + featherlessModels = data; + + if (!data.find(x => x.id === textgen_settings.featherless_model)) { + textgen_settings.featherless_model = data[0]?.id || ''; + } + + $('#featherless_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.featherless_model; + $('#featherless_model').append(option); + } +} + +function onFeatherlessModelSelect() { + const modelId = String($('#featherless_model').val()); + textgen_settings.featherless_model = modelId; + $('#api_button_textgenerationwebui').trigger('click'); +} + + function onMancerModelSelect() { const modelId = String($('#mancer_model').val()); textgen_settings.mancer_model = modelId; @@ -507,6 +537,7 @@ jQuery(function () { $('#ollama_download_model').on('click', downloadOllamaModel); $('#vllm_model').on('change', onVllmModelSelect); $('#aphrodite_model').on('change', onAphroditeModelSelect); + $('#featherless_model').on('change', onFeatherlessModelSelect); const providersSelect = $('.openrouter_providers'); for (const provider of OPENROUTER_PROVIDERS) { @@ -572,6 +603,12 @@ jQuery(function () { width: '100%', templateResult: getAphroditeModelTemplate, }); + $('#featherless_model').select2({ + placeholder: 'Select a model', + searchInputPlaceholder: 'Search models...', + searchInputCssClass: 'text_pole', + width: '100%', + }); providersSelect.select2({ sorter: data => data.sort((a, b) => a.text.localeCompare(b.text)), placeholder: 'Select providers. No selection = all providers.', diff --git a/public/scripts/textgen-settings.js b/public/scripts/textgen-settings.js index eed2d0ef8..0187676a1 100644 --- a/public/scripts/textgen-settings.js +++ b/public/scripts/textgen-settings.js @@ -38,6 +38,7 @@ export const textgen_types = { INFERMATICAI: 'infermaticai', DREAMGEN: 'dreamgen', OPENROUTER: 'openrouter', + FEATHERLESS: 'featherless', HUGGINGFACE: 'huggingface', }; @@ -55,6 +56,7 @@ const { OPENROUTER, KOBOLDCPP, HUGGINGFACE, + FEATHERLESS, } = textgen_types; const LLAMACPP_DEFAULT_ORDER = [ @@ -90,6 +92,7 @@ let TOGETHERAI_SERVER = 'https://api.together.xyz'; let INFERMATICAI_SERVER = 'https://api.totalgpt.ai'; let DREAMGEN_SERVER = 'https://dreamgen.com'; let OPENROUTER_SERVER = 'https://openrouter.ai/api'; +let FEATHERLESS_SERVER = 'https://api.featherless.ai/v1'; const SERVER_INPUTS = { [textgen_types.OOBA]: '#textgenerationwebui_api_url_text', @@ -281,6 +284,8 @@ export function validateTextGenUrl() { export function getTextGenServer() { switch (settings.type) { + case FEATHERLESS: + return FEATHERLESS_SERVER; case MANCER: return MANCER_SERVER; case TOGETHERAI: @@ -1025,6 +1030,8 @@ export function getTextGenModel() { throw new Error('No Ollama model selected'); } return settings.ollama_model; + case FEATHERLESS: + return settings.featherless_model; case HUGGINGFACE: return 'tgi'; default: diff --git a/src/additional-headers.js b/src/additional-headers.js index 8a188caa6..e77507b0e 100644 --- a/src/additional-headers.js +++ b/src/additional-headers.js @@ -147,6 +147,19 @@ function getKoboldCppHeaders(directories) { }) : {}; } +/** + * Gets the headers for the Featherless API. + * @param {import('./users').UserDirectoryList} directories + * @returns {object} Headers for the request + */ +function getFeatherlessHeaders(directories) { + const apiKey = readSecret(directories, SECRET_KEYS.FEATHERLESS); + + return apiKey ? ({ + 'Authorization': `Bearer ${apiKey}`, + }) : {}; +} + /** * Gets the headers for the HuggingFace API. * @param {import('./users').UserDirectoryList} directories @@ -200,6 +213,7 @@ function setAdditionalHeadersByType(requestHeaders, type, server, directories) { [TEXTGEN_TYPES.OPENROUTER]: getOpenRouterHeaders, [TEXTGEN_TYPES.KOBOLDCPP]: getKoboldCppHeaders, [TEXTGEN_TYPES.LLAMACPP]: getLlamaCppHeaders, + [TEXTGEN_TYPES.FEATHERLESS]: getFeatherlessHeaders, [TEXTGEN_TYPES.HUGGINGFACE]: getHuggingFaceHeaders, }; diff --git a/src/constants.js b/src/constants.js index 01ba9e32e..f74e3591d 100644 --- a/src/constants.js +++ b/src/constants.js @@ -216,6 +216,7 @@ const TEXTGEN_TYPES = { INFERMATICAI: 'infermaticai', DREAMGEN: 'dreamgen', OPENROUTER: 'openrouter', + FEATHERLESS: 'featherless', HUGGINGFACE: 'huggingface', }; @@ -242,6 +243,49 @@ const INFERMATICAI_KEYS = [ 'logprobs', ]; +const FEATHERLESS_KEYS = [ + 'model', + 'prompt', + 'best_of', + 'echo', + 'frequency_penalty', + 'logit_bias', + 'logprobs', + 'max_tokens', + 'n', + 'presence_penalty', + 'seed', + 'stop', + 'stream', + 'suffix', + 'temperature', + 'top_p', + 'user', + + 'use_beam_search', + 'top_k', + 'min_p', + 'repetition_penalty', + 'length_penalty', + 'early_stopping', + 'stop_token_ids', + 'ignore_eos', + 'min_tokens', + 'skip_special_tokens', + 'spaces_between_special_tokens', + 'truncate_prompt_tokens', + + 'include_stop_str_in_output', + 'response_format', + 'guided_json', + 'guided_regex', + 'guided_choice', + 'guided_grammar', + 'guided_decoding_backend', + 'guided_whitespace_pattern', +]; + + // https://dreamgen.com/docs/api#openai-text const DREAMGEN_KEYS = [ 'model', @@ -383,4 +427,5 @@ module.exports = { OPENROUTER_HEADERS, OPENROUTER_KEYS, VLLM_KEYS, + FEATHERLESS_KEYS, }; diff --git a/src/endpoints/backends/text-completions.js b/src/endpoints/backends/text-completions.js index 8ca61ecf3..68fa9ce14 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, OPENROUTER_KEYS, VLLM_KEYS, DREAMGEN_KEYS } = require('../../constants'); +const { TEXTGEN_TYPES, TOGETHERAI_KEYS, OLLAMA_KEYS, INFERMATICAI_KEYS, OPENROUTER_KEYS, VLLM_KEYS, DREAMGEN_KEYS, FEATHERLESS_KEYS } = require('../../constants'); const { forwardFetchResponse, trimV1 } = require('../../util'); const { setAdditionalHeaders } = require('../../additional-headers'); @@ -127,6 +127,9 @@ router.post('/status', jsonParser, async function (request, response) { case TEXTGEN_TYPES.OLLAMA: url += '/api/tags'; break; + case TEXTGEN_TYPES.FEATHERLESS: + url += '/v1/models'; + break; case TEXTGEN_TYPES.HUGGINGFACE: url += '/info'; break; @@ -243,6 +246,7 @@ router.post('/generate', jsonParser, async function (request, response) { } else { switch (request.body.api_type) { case TEXTGEN_TYPES.VLLM: + case TEXTGEN_TYPES.FEATHERLESS: case TEXTGEN_TYPES.APHRODITE: case TEXTGEN_TYPES.OOBA: case TEXTGEN_TYPES.TABBY: @@ -290,6 +294,11 @@ router.post('/generate', jsonParser, async function (request, response) { args.body = JSON.stringify(request.body); } + if (request.body.api_type === TEXTGEN_TYPES.FEATHERLESS) { + request.body = _.pickBy(request.body, (_, key) => FEATHERLESS_KEYS.includes(key)); + args.body = JSON.stringify(request.body); + } + if (request.body.api_type === TEXTGEN_TYPES.DREAMGEN) { request.body = _.pickBy(request.body, (_, key) => DREAMGEN_KEYS.includes(key)); // NOTE: DreamGen sometimes get confused by the unusual formatting in the character cards. diff --git a/src/endpoints/secrets.js b/src/endpoints/secrets.js index 532fb1a32..dedb23096 100644 --- a/src/endpoints/secrets.js +++ b/src/endpoints/secrets.js @@ -40,6 +40,7 @@ const SECRET_KEYS = { 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', };