Chutes integration (#4844)
* Chutes integration * Fix eslint * Fix key saving * Fix logo coloration * Fix tool checks * Unhide image inlining controls * Fix order of options * Fix type use in TTS extension script * Add Chutes as a vector storage source * Change log levels to debug * Fix streamed reasoning parsing * Skip remote models update * TTS: Fix API key highlight * Sort image models A-Z * TTS: Fixes * Remove unused SD endpoint * Skip setting context size if models list is not yet loaded * remove chutes quota / balance * Fix: streamed tool calling * Hide reasoning effort control * Add image request debug log * Fix: scroll down on media load in extensions * Unhide some samplers * Bring back reasoning effort * This code will never execute * Reformat else if cases * Add stop strings to request * Remove conditional from reasoning_effort body param * Preserve original pricing fields * Unhide logit bias setting * Pass repetition penalty and logit bias to backend * Swap llama tokenizer for llama3 * Pass min_p, remove supported_sampling_parameters checks * Enable logprobs --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
"openrouter_sort_models": "alphabetically",
|
||||
"ai21_model": "jamba-large",
|
||||
"mistralai_model": "mistral-large-latest",
|
||||
"chutes_model": "deepseek-ai/DeepSeek-V3-0324",
|
||||
"chutes_sort_models": "alphabetically",
|
||||
"electronhub_model": "gpt-4o-mini",
|
||||
"electronhub_sort_models": "alphabetically",
|
||||
"electronhub_group_models": false,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="246" height="153" viewBox="0 0 246 153" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M153.334 146.457C149.769 151.894 142.21 152.909 137.336 148.605L115.671 129.479C110.797 125.176 110.872 117.554 115.83 113.348L137.665 94.8248C161.939 74.232 193.912 66.2053 224.344 73.0645L240.772 76.7673C241.359 76.8995 241.902 77.1773 242.353 77.575C244.73 79.6736 243.399 83.5966 240.235 83.8175L239.884 83.842C209.831 85.9395 182.262 102.339 165.298 128.211L153.334 146.457Z" />
|
||||
<path d="M61.7046 134.759C57.1727 139.455 49.5229 139.022 45.5496 133.846L2.20718 77.3823C-1.74588 72.2325 -0.220772 64.7778 5.43762 61.5919L91.8585 12.9343C119.419 -2.58324 152.212 -4.22605 180.759 8.48062L243.451 36.385C244.421 36.8168 245.268 37.4842 245.914 38.3264C249.363 42.8196 245.669 49.2399 240.048 48.5205L202.701 43.7404C172.88 39.9236 142.862 50.6695 121.491 72.8128L61.7046 134.759Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 902 B |
+59
-16
@@ -678,6 +678,9 @@
|
||||
<div data-source="electronhub">
|
||||
<span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="electronhub_max_prompt_cost" data-i18n="Unknown">Unknown</span>
|
||||
</div>
|
||||
<div data-source="chutes">
|
||||
<span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="chutes_max_prompt_cost" data-i18n="Unknown">Unknown</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="range-block">
|
||||
<label for="stream_toggle" title="Enable OpenAI completion streaming" data-i18n="[title]Enable OpenAI completion streaming" class="checkbox_label widthFreeExpand">
|
||||
@@ -693,7 +696,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai">
|
||||
<div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,electronhub,chutes,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai">
|
||||
<div class="range-block-title" data-i18n="Temperature">
|
||||
Temperature
|
||||
</div>
|
||||
@@ -706,7 +709,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,siliconflow,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai">
|
||||
<div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,siliconflow,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,chutes">
|
||||
<div class="range-block-title" data-i18n="Frequency Penalty">
|
||||
Frequency Penalty
|
||||
</div>
|
||||
@@ -719,7 +722,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,siliconflow,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai">
|
||||
<div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,siliconflow,mistralai,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,chutes">
|
||||
<div class="range-block-title" data-i18n="Presence Penalty">
|
||||
Presence Penalty
|
||||
</div>
|
||||
@@ -732,7 +735,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity,electronhub,nanogpt">
|
||||
<div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity,electronhub,chutes,nanogpt">
|
||||
<div class="range-block-title" data-i18n="Top K">
|
||||
Top K
|
||||
</div>
|
||||
@@ -745,7 +748,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,electronhub,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai">
|
||||
<div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,electronhub,chutes,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai">
|
||||
<div class="range-block-title" data-i18n="Top P">
|
||||
Top P
|
||||
</div>
|
||||
@@ -758,7 +761,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openrouter,nanogpt">
|
||||
<div class="range-block" data-source="openrouter,nanogpt,chutes">
|
||||
<div class="range-block-title" data-i18n="Repetition Penalty">
|
||||
Repetition Penalty
|
||||
</div>
|
||||
@@ -771,7 +774,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openrouter,nanogpt">
|
||||
<div class="range-block" data-source="openrouter,nanogpt,chutes">
|
||||
<div class="range-block-title" data-i18n="Min P">
|
||||
Min P
|
||||
</div>
|
||||
@@ -982,7 +985,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai,azure_openai">
|
||||
<div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,chutes,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai,azure_openai">
|
||||
<div class="range-block-title justifyLeft" data-i18n="Seed">
|
||||
Seed
|
||||
</div>
|
||||
@@ -1956,7 +1959,7 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="makersuite,vertexai,aimlapi,openrouter,claude,xai,electronhub,nanogpt">
|
||||
<div class="range-block" data-source="makersuite,vertexai,aimlapi,openrouter,claude,xai,electronhub,chutes,nanogpt">
|
||||
<label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">
|
||||
<input id="openai_enable_web_search" type="checkbox" />
|
||||
<span data-i18n="Enable web search">Enable web search</span>
|
||||
@@ -1970,7 +1973,7 @@
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,siliconflow,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi,electronhub,azure_openai,zai">
|
||||
<div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,siliconflow,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi,electronhub,chutes,azure_openai,zai">
|
||||
<label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
|
||||
<input id="openai_function_calling" type="checkbox" />
|
||||
<span data-i18n="Enable function calling">Enable function calling</span>
|
||||
@@ -1985,7 +1988,7 @@
|
||||
<strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt,electronhub,azure_openai,zai,siliconflow">
|
||||
<div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt,electronhub,azure_openai,zai,siliconflow,chutes">
|
||||
<label for="openai_media_inlining" class="checkbox_label flexWrap widthFreeExpand">
|
||||
<input id="openai_media_inlining" type="checkbox" />
|
||||
<span data-i18n="Send inline media">Send inline media</span>
|
||||
@@ -2009,7 +2012,7 @@
|
||||
<strong data-source="makersuite,vertexai" data-i18n="video_inlining_hint_4">Videos must be less than 20 MB and under 1 minute long.</strong>
|
||||
<strong data-source="makersuite,vertexai" data-i18n="audio_inlining_hint_2">Audio must be less than 20 MB.</strong>
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai,pollinations,cohere,cometapi,nanogpt,moonshot,aimlapi,openrouter,mistralai,electronhub,azure_openai,zai,siliconflow">
|
||||
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,xai,pollinations,cohere,cometapi,nanogpt,moonshot,aimlapi,openrouter,mistralai,electronhub,azure_openai,zai,siliconflow,chutes">
|
||||
<div class="flex-container oneline-dropdown">
|
||||
<label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
|
||||
Inline Image Quality
|
||||
@@ -2070,7 +2073,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi,electronhub,azure_openai,nanogpt,zai">
|
||||
<div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai,pollinations,moonshot,mistralai,fireworks,cometapi,electronhub,chutes,azure_openai,nanogpt,zai">
|
||||
<label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
|
||||
<input id="openai_show_thoughts" type="checkbox" />
|
||||
<span data-i18n="Request model reasoning">Request model reasoning</span>
|
||||
@@ -2084,7 +2087,7 @@
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub,azure_openai">
|
||||
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub,azure_openai,chutes">
|
||||
<div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models. Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response." data-i18n="[title]Constrains effort on reasoning for reasoning models.">
|
||||
<label for="openai_reasoning_effort">
|
||||
<span data-i18n="Reasoning Effort">Reasoning Effort</span>
|
||||
@@ -2098,7 +2101,7 @@
|
||||
<option data-i18n="openai_reasoning_effort_high" value="high">High</option>
|
||||
<option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>
|
||||
</select>
|
||||
<div class="toggle-description justifyLeft marginBot5" data-source="openai,custom,xai,aimlapi,openrouter,perplexity,electronhub,azure_openai" data-i18n="OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.">
|
||||
<div class="toggle-description justifyLeft marginBot5" data-source="openai,custom,xai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub,azure_openai,chutes" data-i18n="OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.">
|
||||
OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.
|
||||
</div>
|
||||
<div class="toggle-description justifyLeft marginBot5" data-source="claude" data-i18n="Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.">
|
||||
@@ -2144,7 +2147,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom,electronhub,azure_openai">
|
||||
<div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom,electronhub,azure_openai,chutes">
|
||||
<div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias">
|
||||
Logit Bias
|
||||
</div>
|
||||
@@ -2803,6 +2806,7 @@
|
||||
<option value="ai21">AI21</option>
|
||||
<option value="aimlapi">AI/ML API</option>
|
||||
<option value="azure_openai">Azure OpenAI</option>
|
||||
<option value="chutes">Chutes</option>
|
||||
<option value="claude">Claude</option>
|
||||
<option value="cohere">Cohere</option>
|
||||
<!-- Temporarily disabled. -->
|
||||
@@ -3479,6 +3483,45 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="chutes_form" data-source="chutes">
|
||||
<h4 data-i18n="Chutes API Key">Chutes API Key</h4>
|
||||
<div id="chutes_credits_display">
|
||||
<a href="https://chutes.ai/app/api/billing-balance" target="_blank" data-i18n="View Billing/Balance">View Billing/Balance</a>
|
||||
</div>
|
||||
<div class="flex-container">
|
||||
<input id="api_key_chutes" name="api_key_chutes" class="text_pole flex1" value="" type="text" autocomplete="off">
|
||||
<div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_chutes"></div>
|
||||
</div>
|
||||
<div data-for="api_key_chutes" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
|
||||
For privacy reasons, your API key will be hidden after you click 'Connect'.
|
||||
</div>
|
||||
<div>
|
||||
<h4 data-i18n="Chutes Model">Chutes Model</h4>
|
||||
<select id="model_chutes_select">
|
||||
<option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="marginTopBot5">
|
||||
<div class="inline-drawer wide100p">
|
||||
<div class="inline-drawer-toggle inline-drawer-header">
|
||||
<b data-i18n="Chutes Model Sorting">Chutes Model Sorting</b>
|
||||
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
|
||||
</div>
|
||||
<div class="inline-drawer-content m-b-1">
|
||||
<div class="marginTopBot5">
|
||||
<label for="chutes_sort_models" class="checkbox_label">
|
||||
<select id="chutes_sort_models">
|
||||
<option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option>
|
||||
<option data-i18n="Input Price" value="pricing.input">Input Price (cheapest)</option>
|
||||
<option data-i18n="Output Price" value="pricing.output">Output Price (cheapest)</option>
|
||||
<option data-i18n="Context Size" value="context_length">Context Size</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="nanogpt_form" data-source="nanogpt">
|
||||
<h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4>
|
||||
<div class="flex-container">
|
||||
|
||||
+2
-1
@@ -1431,7 +1431,7 @@ export async function printMessages() {
|
||||
delay(debounce_timeout.short).then(() => scrollOnMediaLoad());
|
||||
}
|
||||
|
||||
function scrollOnMediaLoad() {
|
||||
export function scrollOnMediaLoad() {
|
||||
const started = Date.now();
|
||||
const media = chatElement.find('.mes_block img, .mes_block video, .mes_block audio').toArray();
|
||||
let mediaLoaded = 0;
|
||||
@@ -5928,6 +5928,7 @@ export function extractJsonFromData(data, { mainApi = null, chatCompletionSource
|
||||
case chat_completion_sources.COHERE:
|
||||
case chat_completion_sources.XAI:
|
||||
case chat_completion_sources.ELECTRONHUB:
|
||||
case chat_completion_sources.CHUTES:
|
||||
case chat_completion_sources.AZURE_OPENAI:
|
||||
case chat_completion_sources.ZAI:
|
||||
default:
|
||||
|
||||
@@ -397,6 +397,7 @@ function RA_autoconnect(PrevApi) {
|
||||
|| (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE)
|
||||
|| (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)
|
||||
|| (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)
|
||||
|| (secret_state[SECRET_KEYS.CHUTES] && oai_settings.chat_completion_source == chat_completion_sources.CHUTES)
|
||||
|| (secret_state[SECRET_KEYS.SILICONFLOW] && oai_settings.chat_completion_source == chat_completion_sources.SILICONFLOW)
|
||||
|| (secret_state[SECRET_KEYS.ELECTRONHUB] && oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB)
|
||||
|| (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT)
|
||||
|
||||
@@ -10,7 +10,7 @@ import { SlashCommand } from '../../slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
|
||||
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';
|
||||
import { MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR } from '../../constants.js';
|
||||
import { debounce_timeout, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR } from '../../constants.js';
|
||||
export { MODULE_NAME };
|
||||
|
||||
const MODULE_NAME = 'caption';
|
||||
@@ -211,6 +211,7 @@ async function sendCaptionedMessage(caption, image, mimeType) {
|
||||
context.addOneMessage(message);
|
||||
await eventSource.emit(event_types.USER_MESSAGE_RENDERED, messageId);
|
||||
await context.saveChat();
|
||||
setTimeout(() => context.scrollOnMediaLoad(), debounce_timeout.short);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -503,6 +504,7 @@ jQuery(async function () {
|
||||
'aimlapi': SECRET_KEYS.AIMLAPI,
|
||||
'moonshot': SECRET_KEYS.MOONSHOT,
|
||||
'nanogpt': SECRET_KEYS.NANOGPT,
|
||||
'chutes': SECRET_KEYS.CHUTES,
|
||||
'electronhub': SECRET_KEYS.ELECTRONHUB,
|
||||
'zai': SECRET_KEYS.ZAI,
|
||||
};
|
||||
@@ -616,6 +618,7 @@ jQuery(async function () {
|
||||
await processEndpoint('aimlapi', '/api/backends/chat-completions/multimodal-models/aimlapi');
|
||||
await processEndpoint('pollinations', '/api/backends/chat-completions/multimodal-models/pollinations');
|
||||
await processEndpoint('nanogpt', '/api/backends/chat-completions/multimodal-models/nanogpt');
|
||||
await processEndpoint('chutes', '/api/backends/chat-completions/multimodal-models/chutes');
|
||||
await processEndpoint('electronhub', '/api/backends/chat-completions/multimodal-models/electronhub');
|
||||
await processEndpoint('mistral', '/api/backends/chat-completions/multimodal-models/mistral');
|
||||
await processEndpoint('xai', '/api/backends/chat-completions/multimodal-models/xai');
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<label for="caption_multimodal_api" data-i18n="API">API</label>
|
||||
<select id="caption_multimodal_api" class="flex1 text_pole">
|
||||
<option value="aimlapi">AI/ML API</option>
|
||||
<option value="chutes">Chutes</option>
|
||||
<option value="anthropic">Claude</option>
|
||||
<option value="cohere">Cohere</option>
|
||||
<option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
|
||||
|
||||
@@ -257,6 +257,10 @@ function throwIfInvalidModel(useReverseProxy) {
|
||||
throw new Error('Electron Hub API key is not set.');
|
||||
}
|
||||
|
||||
if (multimodalApi === 'chutes' && !secret_state[SECRET_KEYS.CHUTES]) {
|
||||
throw new Error('Chutes API key is not set.');
|
||||
}
|
||||
|
||||
if (multimodalApi === 'zai' && !secret_state[SECRET_KEYS.ZAI]) {
|
||||
throw new Error('Z.AI API key is not set.');
|
||||
}
|
||||
|
||||
@@ -81,6 +81,7 @@ const sources = {
|
||||
pollinations: 'pollinations',
|
||||
stability: 'stability',
|
||||
huggingface: 'huggingface',
|
||||
chutes: 'chutes',
|
||||
electronhub: 'electronhub',
|
||||
nanogpt: 'nanogpt',
|
||||
bfl: 'bfl',
|
||||
@@ -1319,6 +1320,7 @@ async function onModelChange() {
|
||||
sources.falai,
|
||||
sources.xai,
|
||||
sources.google,
|
||||
sources.chutes,
|
||||
];
|
||||
|
||||
if (cloudSources.includes(extension_settings.sd.source)) {
|
||||
@@ -1531,6 +1533,9 @@ async function loadSamplers() {
|
||||
case sources.huggingface:
|
||||
samplers = ['N/A'];
|
||||
break;
|
||||
case sources.chutes:
|
||||
samplers = ['N/A'];
|
||||
break;
|
||||
case sources.electronhub:
|
||||
samplers = ['N/A'];
|
||||
break;
|
||||
@@ -1730,6 +1735,9 @@ async function loadModels() {
|
||||
case sources.huggingface:
|
||||
models = [{ value: '', text: t`<Enter Model ID above>` }];
|
||||
break;
|
||||
case sources.chutes:
|
||||
models = await loadChutesModels();
|
||||
break;
|
||||
case sources.electronhub:
|
||||
models = await loadElectronHubModels();
|
||||
break;
|
||||
@@ -1905,6 +1913,27 @@ async function loadTogetherAIModels() {
|
||||
return [];
|
||||
}
|
||||
|
||||
async function loadChutesModels() {
|
||||
if (!secret_state[SECRET_KEYS.CHUTES]) {
|
||||
console.debug('Chutes API key is not set.');
|
||||
return [];
|
||||
}
|
||||
|
||||
const result = await fetch('/api/sd/chutes/models', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
const models = await result.json();
|
||||
console.debug('Loaded Chutes image models:', models);
|
||||
return models;
|
||||
}
|
||||
|
||||
console.warn('Failed to load Chutes models:', result.status);
|
||||
return [];
|
||||
}
|
||||
|
||||
async function loadElectronHubModels() {
|
||||
if (!secret_state[SECRET_KEYS.ELECTRONHUB]) {
|
||||
console.debug('Electron Hub API key is not set.');
|
||||
@@ -2275,6 +2304,9 @@ async function loadSchedulers() {
|
||||
case sources.huggingface:
|
||||
schedulers = ['N/A'];
|
||||
break;
|
||||
case sources.chutes:
|
||||
schedulers = ['N/A'];
|
||||
break;
|
||||
case sources.electronhub:
|
||||
schedulers = ['N/A'];
|
||||
break;
|
||||
@@ -2375,6 +2407,9 @@ async function loadVaes() {
|
||||
case sources.huggingface:
|
||||
vaes = ['N/A'];
|
||||
break;
|
||||
case sources.chutes:
|
||||
vaes = ['N/A'];
|
||||
break;
|
||||
case sources.electronhub:
|
||||
vaes = ['N/A'];
|
||||
break;
|
||||
@@ -2967,6 +3002,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
|
||||
case sources.huggingface:
|
||||
result = await generateHuggingFaceImage(prefixedPrompt, signal);
|
||||
break;
|
||||
case sources.chutes:
|
||||
result = await generateChutesImage(prefixedPrompt, negativePrompt, signal);
|
||||
break;
|
||||
case sources.electronhub:
|
||||
result = await generateElectronHubImage(prefixedPrompt, signal);
|
||||
break;
|
||||
@@ -3798,6 +3836,38 @@ async function generateHuggingFaceImage(prompt, signal) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an image using the Chutes API.
|
||||
* @param {string} prompt - The main instruction used to guide the image generation.
|
||||
* @param {string} negativePrompt - The instruction used to restrict the image generation.
|
||||
* @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
|
||||
* @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
|
||||
*/
|
||||
async function generateChutesImage(prompt, negativePrompt, signal) {
|
||||
const result = await fetch('/api/sd/chutes/generate', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
signal: signal,
|
||||
body: JSON.stringify({
|
||||
model: extension_settings.sd.model,
|
||||
prompt: prompt,
|
||||
negative_prompt: negativePrompt,
|
||||
width: extension_settings.sd.width,
|
||||
height: extension_settings.sd.height,
|
||||
steps: extension_settings.sd.steps,
|
||||
guidance_scale: extension_settings.sd.scale,
|
||||
}),
|
||||
});
|
||||
|
||||
if (result.ok) {
|
||||
const data = await result.json();
|
||||
return { format: 'jpg', data: data.image };
|
||||
} else {
|
||||
const text = await result.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an image using the Electron Hub API.
|
||||
* @param {string} prompt - The main instruction used to guide the image generation.
|
||||
@@ -4207,6 +4277,7 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref
|
||||
context.addOneMessage(message);
|
||||
await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId, 'extension');
|
||||
await context.saveChat();
|
||||
setTimeout(() => context.scrollOnMediaLoad(), debounce_timeout.short);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -4313,6 +4384,8 @@ function isValidState() {
|
||||
return secret_state[SECRET_KEYS.STABILITY];
|
||||
case sources.huggingface:
|
||||
return secret_state[SECRET_KEYS.HUGGINGFACE];
|
||||
case sources.chutes:
|
||||
return secret_state[SECRET_KEYS.CHUTES];
|
||||
case sources.electronhub:
|
||||
return secret_state[SECRET_KEYS.ELECTRONHUB];
|
||||
case sources.nanogpt:
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
<select id="sd_source">
|
||||
<option value="aimlapi">AI/ML API</option>
|
||||
<option value="bfl">BFL (Black Forest Labs)</option>
|
||||
<option value="chutes">Chutes</option>
|
||||
<option value="comfy">ComfyUI</option>
|
||||
<option value="drawthings">DrawThings HTTP API</option>
|
||||
<option value="electronhub">Electron Hub</option>
|
||||
@@ -94,6 +95,9 @@
|
||||
<label for="sd_huggingface_model_id" data-i18n="Model ID">Model ID</label>
|
||||
<input id="sd_huggingface_model_id" type="text" class="text_pole" data-i18n="[placeholder]e.g. black-forest-labs/FLUX.1-dev" placeholder="e.g. black-forest-labs/FLUX.1-dev" value="" />
|
||||
</div>
|
||||
<div data-sd-source="chutes">
|
||||
<i data-i18n="Hint: Save an API key in the Chutes (Chat Completion) API settings to use it here.">Hint: Save an API key in the Chutes (Chat Completion) API settings to use it here.</i>
|
||||
</div>
|
||||
<div data-sd-source="electronhub">
|
||||
<i data-i18n="Hint: Save an API key in the Electron Hub (Chat Completion) API settings to use it here.">Hint: Save an API key in the Electron Hub (Chat Completion) API settings to use it here.</i>
|
||||
<div class="flex-container" id="sd_electronhub_quality_row">
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
import { event_types, eventSource, getRequestHeaders } from '../../../script.js';
|
||||
import { SECRET_KEYS, secret_state } from '../../secrets.js';
|
||||
import { getPreviewString, saveTtsProviderSettings } from './index.js';
|
||||
|
||||
export { ChutesTtsProvider };
|
||||
|
||||
class ChutesTtsProvider {
|
||||
settings;
|
||||
voices = [];
|
||||
models = [];
|
||||
separator = ' . ';
|
||||
|
||||
defaultSettings = {
|
||||
voiceMap: {},
|
||||
model: 'kokoro',
|
||||
speed: 1,
|
||||
};
|
||||
|
||||
get settingsHtml() {
|
||||
let html = `
|
||||
<div class="flex-container alignItemsCenter">
|
||||
<div class="flex1">Chutes TTS API</div>
|
||||
<div id="chutes_tts_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_chutes">
|
||||
<i class="fa-solid fa-key"></i>
|
||||
<span>API Key</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<div class="flex1">
|
||||
<label for="chutes_tts_model">Model</label>
|
||||
<select id="chutes_tts_model" class="text_pole"></select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="chutes_tts_speed">Speed <span id="chutes_tts_speed_output"></span></label>
|
||||
<input type="range" id="chutes_tts_speed" value="1" min="0.25" max="4" step="0.05">
|
||||
</div>
|
||||
</div>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.handler = async function (/** @type {string} */ key) {
|
||||
if (key !== SECRET_KEYS.CHUTES) return;
|
||||
$('#chutes_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.CHUTES]);
|
||||
await this.onRefreshClick();
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
[event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
|
||||
eventSource.removeListener(event, this.handler);
|
||||
});
|
||||
}
|
||||
|
||||
onSettingsChange() {
|
||||
this.settings.model = $('#chutes_tts_model').val();
|
||||
this.settings.speed = $('#chutes_tts_speed').val();
|
||||
saveTtsProviderSettings();
|
||||
}
|
||||
|
||||
async loadSettings(settings) {
|
||||
if (Object.keys(settings).length === 0) {
|
||||
Object.assign(settings, this.defaultSettings);
|
||||
}
|
||||
|
||||
this.settings = settings;
|
||||
|
||||
if (!this.settings.voiceMap) {
|
||||
this.settings.voiceMap = {};
|
||||
}
|
||||
|
||||
// Update UI
|
||||
$('#chutes_tts_model').val(this.settings.model);
|
||||
$('#chutes_tts_speed').val(this.settings.speed);
|
||||
$('#chutes_tts_speed_output').text(this.settings.speed);
|
||||
|
||||
$('#chutes_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.CHUTES]);
|
||||
[event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
|
||||
eventSource.on(event, this.handler);
|
||||
});
|
||||
|
||||
await this.checkReady();
|
||||
|
||||
$('#chutes_tts_model').on('change', () => this.onSettingsChange());
|
||||
$('#chutes_tts_speed').on('input', function () {
|
||||
const value = $(this).val();
|
||||
$('#chutes_tts_speed_output').text(String(value));
|
||||
});
|
||||
$('#chutes_tts_speed').on('change', () => this.onSettingsChange());
|
||||
}
|
||||
|
||||
async checkReady() {
|
||||
await this.updateModels();
|
||||
if (this.models.length === 0) {
|
||||
// No models available
|
||||
}
|
||||
await this.updateVoices();
|
||||
}
|
||||
|
||||
async onRefreshClick() {
|
||||
return await this.checkReady();
|
||||
}
|
||||
|
||||
async updateModels() {
|
||||
// For Chutes TTS, we always use the Kokoro model currently.
|
||||
this.models = ['kokoro'];
|
||||
|
||||
$('#chutes_tts_model').empty();
|
||||
$('#chutes_tts_model').append($('<option>').val('kokoro').text('Kokoro'));
|
||||
$('#chutes_tts_model').val('kokoro');
|
||||
|
||||
this.settings.model = 'kokoro';
|
||||
}
|
||||
|
||||
async updateVoices() {
|
||||
// Kokoro voices list
|
||||
const kokoroVoices = [
|
||||
{ id: 'af_alloy', name: 'Alloy (Female)', lang: 'en-US' },
|
||||
{ id: 'af_aoede', name: 'Aoede (Female)', lang: 'en-US' },
|
||||
{ id: 'af_bella', name: 'Bella (Female)', lang: 'en-US' },
|
||||
{ id: 'af_heart', name: 'Heart (Female) - Default', lang: 'en-US' },
|
||||
{ id: 'af_jessica', name: 'Jessica (Female)', lang: 'en-US' },
|
||||
{ id: 'af_kore', name: 'Kore (Female)', lang: 'en-US' },
|
||||
{ id: 'af_nicole', name: 'Nicole (Female)', lang: 'en-US' },
|
||||
{ id: 'af_nova', name: 'Nova (Female)', lang: 'en-US' },
|
||||
{ id: 'af_river', name: 'River (Female)', lang: 'en-US' },
|
||||
{ id: 'af_sarah', name: 'Sarah (Female)', lang: 'en-US' },
|
||||
{ id: 'af_sky', name: 'Sky (Female)', lang: 'en-US' },
|
||||
{ id: 'am_adam', name: 'Adam (Male)', lang: 'en-US' },
|
||||
{ id: 'am_echo', name: 'Echo (Male)', lang: 'en-US' },
|
||||
{ id: 'am_eric', name: 'Eric (Male)', lang: 'en-US' },
|
||||
{ id: 'am_fenrir', name: 'Fenrir (Male)', lang: 'en-US' },
|
||||
{ id: 'am_liam', name: 'Liam (Male)', lang: 'en-US' },
|
||||
{ id: 'am_michael', name: 'Michael (Male)', lang: 'en-US' },
|
||||
{ id: 'am_onyx', name: 'Onyx (Male)', lang: 'en-US' },
|
||||
{ id: 'am_puck', name: 'Puck (Male)', lang: 'en-US' },
|
||||
{ id: 'am_santa', name: 'Santa (Male)', lang: 'en-US' },
|
||||
{ id: 'bf_alice', name: 'Alice (British Female)', lang: 'en-GB' },
|
||||
{ id: 'bf_emma', name: 'Emma (British Female)', lang: 'en-GB' },
|
||||
{ id: 'bf_isabella', name: 'Isabella (British Female)', lang: 'en-GB' },
|
||||
{ id: 'bf_lily', name: 'Lily (British Female)', lang: 'en-GB' },
|
||||
{ id: 'bm_daniel', name: 'Daniel (British Male)', lang: 'en-GB' },
|
||||
{ id: 'bm_fable', name: 'Fable (British Male)', lang: 'en-GB' },
|
||||
{ id: 'bm_george', name: 'George (British Male)', lang: 'en-GB' },
|
||||
{ id: 'bm_lewis', name: 'Lewis (British Male)', lang: 'en-GB' },
|
||||
{ id: 'ef_dora', name: 'Dora (European Female)', lang: 'es-ES' },
|
||||
{ id: 'em_alex', name: 'Alex (European Male)', lang: 'es-ES' },
|
||||
{ id: 'em_santa', name: 'Santa (European Male)', lang: 'es-ES' },
|
||||
{ id: 'ff_siwis', name: 'Siwis (French Female)', lang: 'fr-FR' },
|
||||
{ id: 'hf_alpha', name: 'Alpha (Hindi Female)', lang: 'hi-IN' },
|
||||
{ id: 'hf_beta', name: 'Beta (Hindi Female)', lang: 'hi-IN' },
|
||||
{ id: 'hm_omega', name: 'Omega (Hindi Male)', lang: 'hi-IN' },
|
||||
{ id: 'hm_psi', name: 'Psi (Hindi Male)', lang: 'hi-IN' },
|
||||
{ id: 'if_sara', name: 'Sara (Italian Female)', lang: 'it-IT' },
|
||||
{ id: 'im_nicola', name: 'Nicola (Italian Male)', lang: 'it-IT' },
|
||||
{ id: 'jf_alpha', name: 'Alpha (Japanese Female)', lang: 'ja-JP' },
|
||||
{ id: 'jf_gongitsune', name: 'Gongitsune (Japanese Female)', lang: 'ja-JP' },
|
||||
{ id: 'jf_nezumi', name: 'Nezumi (Japanese Female)', lang: 'ja-JP' },
|
||||
{ id: 'jf_tebukuro', name: 'Tebukuro (Japanese Female)', lang: 'ja-JP' },
|
||||
{ id: 'jm_kumo', name: 'Kumo (Japanese Male)', lang: 'ja-JP' },
|
||||
{ id: 'pf_dora', name: 'Dora (Portuguese Female)', lang: 'pt-PT' },
|
||||
{ id: 'pm_alex', name: 'Alex (Portuguese Male)', lang: 'pt-PT' },
|
||||
{ id: 'pm_santa', name: 'Santa (Portuguese Male)', lang: 'pt-PT' },
|
||||
{ id: 'zf_xiaobei', name: 'Xiaobei (Chinese Female)', lang: 'zh-CN' },
|
||||
{ id: 'zf_xiaoni', name: 'Xiaoni (Chinese Female)', lang: 'zh-CN' },
|
||||
{ id: 'zf_xiaoxiao', name: 'Xiaoxiao (Chinese Female)', lang: 'zh-CN' },
|
||||
{ id: 'zf_xiaoyi', name: 'Xiaoyi (Chinese Female)', lang: 'zh-CN' },
|
||||
{ id: 'zm_yunjian', name: 'Yunjian (Chinese Male)', lang: 'zh-CN' },
|
||||
{ id: 'zm_yunxi', name: 'Yunxi (Chinese Male)', lang: 'zh-CN' },
|
||||
{ id: 'zm_yunxia', name: 'Yunxia (Chinese Male)', lang: 'zh-CN' },
|
||||
{ id: 'zm_yunyang', name: 'Yunyang (Chinese Male)', lang: 'zh-CN' },
|
||||
];
|
||||
|
||||
this.voices = kokoroVoices.map(v => ({
|
||||
name: v.name,
|
||||
voice_id: v.id,
|
||||
lang: v.lang,
|
||||
}));
|
||||
}
|
||||
|
||||
async getVoice(voiceName) {
|
||||
if (this.voices.length === 0) {
|
||||
await this.updateVoices();
|
||||
}
|
||||
const voice = this.voices.find(v => v.name === voiceName || v.voice_id === voiceName);
|
||||
return voice || this.voices.find(v => v.voice_id === 'af_heart');
|
||||
}
|
||||
|
||||
async generateTts(text, voiceId) {
|
||||
const response = await this.fetchTtsGeneration(text, voiceId);
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchTtsGeneration(text, voiceId) {
|
||||
const apiKey = secret_state[SECRET_KEYS.CHUTES];
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error('No Chutes API key found');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/openai/chutes/generate-voice', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
input: text,
|
||||
voice: voiceId || 'af_heart',
|
||||
speed: this.settings.speed || 1,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Chutes TTS failed: ${error}`);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async fetchTtsVoiceObjects() {
|
||||
if (this.voices.length === 0) {
|
||||
await this.updateVoices();
|
||||
}
|
||||
|
||||
const voiceIds = this.voices
|
||||
.map(voice => ({ name: voice.name, voice_id: voice.voice_id, preview_url: false }));
|
||||
return voiceIds;
|
||||
}
|
||||
|
||||
async previewTtsVoice(voiceId) {
|
||||
const text = getPreviewString(voiceId);
|
||||
await this.generateTts(text, voiceId);
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ import { TtsWebuiProvider } from './tts-webui.js';
|
||||
import { PollinationsTtsProvider } from './pollinations.js';
|
||||
import { MiniMaxTtsProvider } from './minimax.js';
|
||||
import { ElectronHubTtsProvider } from './electronhub.js';
|
||||
import { ChutesTtsProvider } from './chutes.js';
|
||||
|
||||
const UPDATE_INTERVAL = 1000;
|
||||
const wrapper = new ModuleWorkerWrapper(moduleWorker);
|
||||
@@ -120,6 +121,7 @@ const ttsProviders = {
|
||||
AllTalk: AllTalkTtsProvider,
|
||||
Azure: AzureTtsProvider,
|
||||
Chatterbox: ChatterboxTtsProvider,
|
||||
Chutes: ChutesTtsProvider,
|
||||
Coqui: CoquiTtsProvider,
|
||||
'CosyVoice (Unofficial)': CosyVoiceProvider,
|
||||
Edge: EdgeTtsProvider,
|
||||
@@ -239,9 +241,7 @@ function isTtsProcessing() {
|
||||
|
||||
/**
|
||||
* Splits a message into lines and adds each non-empty line to the TTS job queue.
|
||||
* @param {Object} message - The message object to be processed.
|
||||
* @param {string} message.mes - The text of the message to be split into lines.
|
||||
* @param {string} message.name - The name associated with the message.
|
||||
* @param {ChatMessage} message - The message object to be processed.
|
||||
* @returns {void}
|
||||
*/
|
||||
function processAndQueueTtsMessage(message) {
|
||||
|
||||
@@ -69,6 +69,7 @@ const settings = {
|
||||
vllm_model: '',
|
||||
webllm_model: '',
|
||||
google_model: 'text-embedding-005',
|
||||
chutes_model: 'chutes-qwen-qwen3-embedding-8b',
|
||||
summarize: false,
|
||||
summarize_sent: false,
|
||||
summary_source: 'main',
|
||||
@@ -829,6 +830,9 @@ function getVectorsRequestBody(args = {}) {
|
||||
body.vertexai_region = oai_settings.vertexai_region;
|
||||
body.vertexai_express_project_id = oai_settings.vertexai_express_project_id;
|
||||
break;
|
||||
case 'chutes':
|
||||
body.model = extension_settings.vectors.chutes_model;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -913,6 +917,7 @@ async function insertVectorItems(collectionId, items) {
|
||||
function throwIfSourceInvalid() {
|
||||
if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] ||
|
||||
settings.source === 'electronhub' && !secret_state[SECRET_KEYS.ELECTRONHUB] ||
|
||||
settings.source === 'chutes' && !secret_state[SECRET_KEYS.CHUTES] ||
|
||||
settings.source === 'openrouter' && !secret_state[SECRET_KEYS.OPENROUTER] ||
|
||||
settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] ||
|
||||
settings.source === 'vertexai' && !secret_state[SECRET_KEYS.VERTEXAI] && !secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT] ||
|
||||
@@ -1127,6 +1132,7 @@ function toggleSettings() {
|
||||
$('#together_vectorsModel').toggle(settings.source === 'togetherai');
|
||||
$('#openai_vectorsModel').toggle(settings.source === 'openai');
|
||||
$('#electronhub_vectorsModel').toggle(settings.source === 'electronhub');
|
||||
$('#chutes_vectorsModel').toggle(settings.source === 'chutes');
|
||||
$('#openrouter_vectorsModel').toggle(settings.source === 'openrouter');
|
||||
$('#cohere_vectorsModel').toggle(settings.source === 'cohere');
|
||||
$('#ollama_vectorsModel').toggle(settings.source === 'ollama');
|
||||
@@ -1147,9 +1153,46 @@ function toggleSettings() {
|
||||
case 'openrouter':
|
||||
loadOpenRouterModels();
|
||||
break;
|
||||
case 'chutes':
|
||||
loadChutesModels();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChutesModels() {
|
||||
try {
|
||||
const response = await fetch('/api/openai/chutes/models/embedding', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
/** @type {Array<any>} */
|
||||
const data = await response.json();
|
||||
const models = Array.isArray(data) ? data : [];
|
||||
populateChutesModelSelect(models);
|
||||
} catch (err) {
|
||||
console.warn('Chutes models fetch failed', err);
|
||||
populateChutesModelSelect([]);
|
||||
}
|
||||
}
|
||||
|
||||
function populateChutesModelSelect(models) {
|
||||
const select = $('#vectors_chutes_model');
|
||||
select.empty();
|
||||
for (const m of models) {
|
||||
const option = document.createElement('option');
|
||||
option.value = m.slug;
|
||||
option.text = m.name;
|
||||
select.append(option);
|
||||
}
|
||||
if (!settings.chutes_model && models.length) {
|
||||
settings.chutes_model = models[0].slug;
|
||||
}
|
||||
$('#vectors_chutes_model').val(settings.chutes_model);
|
||||
}
|
||||
|
||||
async function loadElectronHubModels() {
|
||||
try {
|
||||
const response = await fetch('/api/openai/electronhub/models', {
|
||||
@@ -1630,6 +1673,11 @@ jQuery(async () => {
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#vectors_chutes_model').val(settings.chutes_model).on('change', () => {
|
||||
settings.chutes_model = String($('#vectors_chutes_model').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#vectors_openrouter_model').val(settings.openrouter_model).on('change', () => {
|
||||
settings.openrouter_model = String($('#vectors_openrouter_model').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
Vectorization Source
|
||||
</label>
|
||||
<select id="vectors_source" class="text_pole">
|
||||
<option value="chutes">Chutes</option>
|
||||
<option value="cohere">Cohere</option>
|
||||
<option value="electronhub">Electron Hub</option>
|
||||
<option value="extras">Extras (deprecated)</option>
|
||||
@@ -28,6 +29,15 @@
|
||||
<option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn" id="chutes_vectorsModel">
|
||||
<label for="vectors_chutes_model" data-i18n="Vectorization Model">
|
||||
Vectorization Model
|
||||
</label>
|
||||
<select id="vectors_chutes_model" class="text_pole"></select>
|
||||
<i data-i18n="Hint: Set your Chutes API key in API Connections.">
|
||||
Hint: Set your Chutes API key in API Connections.
|
||||
</i>
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn" id="electronhub_vectorsModel">
|
||||
<label for="vectors_electronhub_model" data-i18n="Vectorization Model">
|
||||
Vectorization Model
|
||||
|
||||
+193
-3
@@ -184,6 +184,7 @@ export const chat_completion_sources = {
|
||||
PERPLEXITY: 'perplexity',
|
||||
GROQ: 'groq',
|
||||
ELECTRONHUB: 'electronhub',
|
||||
CHUTES: 'chutes',
|
||||
NANOGPT: 'nanogpt',
|
||||
DEEPSEEK: 'deepseek',
|
||||
AIMLAPI: 'aimlapi',
|
||||
@@ -293,6 +294,8 @@ export const settingsToUpdate = {
|
||||
cohere_model: ['#model_cohere_select', 'cohere_model', false, true],
|
||||
perplexity_model: ['#model_perplexity_select', 'perplexity_model', false, true],
|
||||
groq_model: ['#model_groq_select', 'groq_model', false, true],
|
||||
chutes_model: ['#model_chutes_select', 'chutes_model', false, true],
|
||||
chutes_sort_models: ['#chutes_sort_models', 'chutes_sort_models', false, true],
|
||||
siliconflow_model: ['#model_siliconflow_select', 'siliconflow_model', false, true],
|
||||
electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true],
|
||||
electronhub_sort_models: ['#electronhub_sort_models', 'electronhub_sort_models', false, true],
|
||||
@@ -400,6 +403,8 @@ const default_settings = {
|
||||
cohere_model: 'command-r-plus',
|
||||
perplexity_model: 'sonar-pro',
|
||||
groq_model: 'llama-3.3-70b-versatile',
|
||||
chutes_model: 'deepseek-ai/DeepSeek-V3-0324',
|
||||
chutes_sort_models: 'alphabetically',
|
||||
siliconflow_model: 'deepseek-ai/DeepSeek-V3',
|
||||
electronhub_model: 'gpt-4o-mini',
|
||||
electronhub_sort_models: 'alphabetically',
|
||||
@@ -1592,6 +1597,8 @@ export function getChatCompletionModel(source = null) {
|
||||
return oai_settings.siliconflow_model;
|
||||
case chat_completion_sources.ELECTRONHUB:
|
||||
return oai_settings.electronhub_model;
|
||||
case chat_completion_sources.CHUTES:
|
||||
return oai_settings.chutes_model;
|
||||
case chat_completion_sources.NANOGPT:
|
||||
return oai_settings.nanogpt_model;
|
||||
case chat_completion_sources.DEEPSEEK:
|
||||
@@ -1717,6 +1724,72 @@ function calculateElectronHubCost() {
|
||||
$('#electronhub_max_prompt_cost').text(cost);
|
||||
}
|
||||
|
||||
function getChutesModelTemplate(option) {
|
||||
const model = model_list.find(x => x.id === option?.element?.value);
|
||||
|
||||
if (!option.id || !model) {
|
||||
return option.text;
|
||||
}
|
||||
|
||||
const inputPrice = model.pricing?.input;
|
||||
const outputPrice = model.pricing?.output;
|
||||
|
||||
let price = 'Unknown';
|
||||
if (inputPrice !== undefined && outputPrice !== undefined) {
|
||||
// Check if both prices are 0 (free model)
|
||||
if (inputPrice === 0 && outputPrice === 0) {
|
||||
price = 'Free';
|
||||
} else {
|
||||
price = `$${inputPrice}/$${outputPrice} in/out Mtoken`;
|
||||
}
|
||||
}
|
||||
|
||||
const contextLength = model.context_length || model.max_model_len || 'Unknown';
|
||||
const visionIcon = model.input_modalities?.includes('image') ? '<i class="fa-solid fa-eye fa-sm" title="This model supports vision"></i>' : '';
|
||||
const reasoningIcon = model.supported_features?.includes('reasoning') ? '<i class="fa-solid fa-brain fa-sm" title="This model supports reasoning"></i>' : '';
|
||||
const toolCallsIcon = model.supported_features?.includes('structured_outputs') ? '<i class="fa-solid fa-wrench fa-sm" title="This model supports function tools"></i>' : '';
|
||||
|
||||
const iconsContainer = document.createElement('span');
|
||||
iconsContainer.insertAdjacentHTML('beforeend', visionIcon);
|
||||
iconsContainer.insertAdjacentHTML('beforeend', reasoningIcon);
|
||||
iconsContainer.insertAdjacentHTML('beforeend', toolCallsIcon);
|
||||
|
||||
const capabilities = (iconsContainer.children.length) ? ` | ${iconsContainer.innerHTML}` : '';
|
||||
|
||||
return $((`
|
||||
<div class="flex-container alignItemsBaseline" title="${DOMPurify.sanitize(model.id)}">
|
||||
<strong>${DOMPurify.sanitize(model.id)}</strong> | ${contextLength} ctx | <small>${price}</small>${capabilities}
|
||||
</div>
|
||||
`));
|
||||
}
|
||||
|
||||
function calculateChutesCost() {
|
||||
if (oai_settings.chat_completion_source !== chat_completion_sources.CHUTES) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cost = 'Unknown';
|
||||
const model = model_list.find(x => x.id === oai_settings.chutes_model);
|
||||
|
||||
if (model?.pricing) {
|
||||
const outputPrice = model.pricing?.output;
|
||||
const inputPrice = model.pricing?.input;
|
||||
|
||||
if (outputPrice !== undefined && inputPrice !== undefined) {
|
||||
const outputCost = Number(outputPrice / 1000000);
|
||||
const inputCost = Number(inputPrice / 1000000);
|
||||
const outputTokens = oai_settings.openai_max_tokens;
|
||||
const inputTokens = (oai_settings.openai_max_context - outputTokens);
|
||||
const totalCost = (outputCost * outputTokens) + (inputCost * inputTokens);
|
||||
if (!isNaN(totalCost)) {
|
||||
cost = '$' + totalCost.toFixed(4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$('#chutes_max_prompt_cost').text(cost);
|
||||
}
|
||||
|
||||
function saveModelList(data) {
|
||||
model_list = data.map((model) => ({ ...model }));
|
||||
model_list.sort((a, b) => a?.id && b?.id && a.id.localeCompare(b.id));
|
||||
@@ -1814,6 +1887,27 @@ function saveModelList(data) {
|
||||
$('#model_electronhub_select').val(oai_settings.electronhub_model).trigger('change');
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.CHUTES) {
|
||||
model_list = model_list.filter(model => !model.id.toLowerCase().includes('affine'));
|
||||
|
||||
model_list = chutesSortBy(model_list, oai_settings.chutes_sort_models);
|
||||
|
||||
$('#model_chutes_select').empty();
|
||||
|
||||
for (const model of model_list) {
|
||||
const option = $('<option>').val(model.id).text(model.id);
|
||||
option.attr('data-model', JSON.stringify(model));
|
||||
$('#model_chutes_select').append(option);
|
||||
}
|
||||
|
||||
const selectedModel = model_list.find(model => model.id === oai_settings.chutes_model);
|
||||
if (model_list.length > 0 && (!selectedModel || !oai_settings.chutes_model)) {
|
||||
oai_settings.chutes_model = model_list[0].id;
|
||||
}
|
||||
|
||||
$('#model_chutes_select').val(oai_settings.chutes_model).trigger('change');
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) {
|
||||
$('#model_nanogpt_select').empty();
|
||||
model_list.forEach((model) => {
|
||||
@@ -2083,6 +2177,24 @@ function openRouterGroupByVendor(array) {
|
||||
}, new Map());
|
||||
}
|
||||
|
||||
function chutesSortBy(data, property = 'alphabetically') {
|
||||
return data.sort((a, b) => {
|
||||
if (property === 'context_length') {
|
||||
return b.context_length - a.context_length;
|
||||
} else if (property === 'pricing.input') {
|
||||
const aPrice = parseFloat(a.pricing?.input || 0);
|
||||
const bPrice = parseFloat(b.pricing?.input || 0);
|
||||
return aPrice - bPrice;
|
||||
} else if (property === 'pricing.output') {
|
||||
const aPrice = parseFloat(a.pricing?.output || 0);
|
||||
const bPrice = parseFloat(b.pricing?.output || 0);
|
||||
return aPrice - bPrice;
|
||||
} else {
|
||||
return a?.id && b?.id && a.id.localeCompare(b.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function appendElectronHubOptions(model_list, groupModels = false) {
|
||||
const appendOption = (model, parent = null) => {
|
||||
(parent || $('#model_electronhub_select')).append(
|
||||
@@ -2201,6 +2313,7 @@ function getReasoningEffort() {
|
||||
chat_completion_sources.PERPLEXITY,
|
||||
chat_completion_sources.COMETAPI,
|
||||
chat_completion_sources.ELECTRONHUB,
|
||||
chat_completion_sources.CHUTES,
|
||||
];
|
||||
|
||||
if (!reasoningEffortSources.includes(oai_settings.chat_completion_source)) {
|
||||
@@ -2285,6 +2398,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
|
||||
const isDeepSeek = oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK;
|
||||
const isAimlapi = oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI;
|
||||
const isElectronHub = oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB;
|
||||
const isChutes = oai_settings.chat_completion_source == chat_completion_sources.CHUTES;
|
||||
const isXAI = oai_settings.chat_completion_source == chat_completion_sources.XAI;
|
||||
const isPollinations = oai_settings.chat_completion_source == chat_completion_sources.POLLINATIONS;
|
||||
const isMoonshot = oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT;
|
||||
@@ -2299,7 +2413,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
|
||||
const useLogprobs = !!power_user.request_token_probabilities;
|
||||
const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isAzureOpenAI || isCustom || isXAI || isAimlapi || isMoonshot);
|
||||
|
||||
const logitBiasSources = [chat_completion_sources.OPENAI, chat_completion_sources.AZURE_OPENAI, chat_completion_sources.OPENROUTER, chat_completion_sources.ELECTRONHUB, chat_completion_sources.CUSTOM];
|
||||
const logitBiasSources = [chat_completion_sources.OPENAI, chat_completion_sources.AZURE_OPENAI, chat_completion_sources.OPENROUTER, chat_completion_sources.ELECTRONHUB, chat_completion_sources.CHUTES, chat_completion_sources.CUSTOM];
|
||||
if (oai_settings.bias_preset_selected
|
||||
&& logitBiasSources.includes(oai_settings.chat_completion_source)
|
||||
&& Array.isArray(oai_settings.bias_presets[oai_settings.bias_preset_selected])
|
||||
@@ -2367,7 +2481,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
|
||||
}
|
||||
|
||||
// Add logprobs request (currently OpenAI only, max 5 on their side)
|
||||
if (useLogprobs && (isOAI || isAzureOpenAI || isCustom || isDeepSeek || isXAI || isAimlapi)) {
|
||||
if (useLogprobs && (isOAI || isAzureOpenAI || isCustom || isDeepSeek || isXAI || isAimlapi || isChutes)) {
|
||||
generate_data['logprobs'] = 5;
|
||||
}
|
||||
|
||||
@@ -2494,6 +2608,14 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
|
||||
generate_data['top_k'] = Number(oai_settings.top_k_openai);
|
||||
}
|
||||
|
||||
if (isChutes) {
|
||||
generate_data['min_p'] = Number(oai_settings.min_p_openai);
|
||||
generate_data['top_k'] = oai_settings.top_k_openai > 0 ? Number(oai_settings.top_k_openai) : undefined;
|
||||
generate_data['repetition_penalty'] = Number(oai_settings.repetition_penalty_openai);
|
||||
generate_data['seed'] = oai_settings.seed >= 0 ? oai_settings.seed : undefined;
|
||||
generate_data['stop'] = getCustomStoppingStrings();
|
||||
}
|
||||
|
||||
// https://docs.z.ai/api-reference/llm/chat-completion
|
||||
if (isZai) {
|
||||
generate_data['top_p'] = generate_data.top_p || 0.01;
|
||||
@@ -2697,7 +2819,7 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov
|
||||
state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
|
||||
}
|
||||
return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
|
||||
} else if ([chat_completion_sources.CUSTOM, chat_completion_sources.POLLINATIONS, chat_completion_sources.AIMLAPI, chat_completion_sources.MOONSHOT, chat_completion_sources.COMETAPI, chat_completion_sources.ELECTRONHUB, chat_completion_sources.NANOGPT, chat_completion_sources.ZAI, chat_completion_sources.SILICONFLOW].includes(chat_completion_source)) {
|
||||
} else if ([chat_completion_sources.CUSTOM, chat_completion_sources.POLLINATIONS, chat_completion_sources.AIMLAPI, chat_completion_sources.MOONSHOT, chat_completion_sources.COMETAPI, chat_completion_sources.ELECTRONHUB, chat_completion_sources.NANOGPT, chat_completion_sources.ZAI, chat_completion_sources.SILICONFLOW, chat_completion_sources.CHUTES].includes(chat_completion_source)) {
|
||||
if (show_thoughts) {
|
||||
state.reasoning +=
|
||||
data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??
|
||||
@@ -2733,6 +2855,7 @@ function parseChatCompletionLogprobs(data) {
|
||||
case chat_completion_sources.DEEPSEEK:
|
||||
case chat_completion_sources.XAI:
|
||||
case chat_completion_sources.CUSTOM:
|
||||
case chat_completion_sources.CHUTES:
|
||||
if (!data.choices?.length) {
|
||||
return null;
|
||||
}
|
||||
@@ -4658,6 +4781,26 @@ function getFireworksMaxContext(model, isUnlocked) {
|
||||
return max_32k;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum context size for the Chutes model
|
||||
* @param {string} model Model identifier
|
||||
* @param {boolean} isUnlocked Whether context limits are unlocked
|
||||
* @returns {number} Maximum context size in tokens
|
||||
*/
|
||||
function getChutesMaxContext(model, isUnlocked) {
|
||||
if (isUnlocked) {
|
||||
return unlocked_max;
|
||||
}
|
||||
|
||||
if (Array.isArray(model_list)) {
|
||||
const modelInfo = model_list.find(m => m.id === model);
|
||||
if (modelInfo?.context_length) {
|
||||
return modelInfo.context_length;
|
||||
}
|
||||
}
|
||||
return max_8k;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum context size for the ElectronHub model
|
||||
* @param {string} model Model identifier
|
||||
@@ -4805,6 +4948,15 @@ async function onModelChange() {
|
||||
oai_settings.electronhub_model = value;
|
||||
}
|
||||
|
||||
if ($(this).is('#model_chutes_select')) {
|
||||
if (!value || !hasModelsLoaded) {
|
||||
console.debug('Null Chutes model selected. Ignoring.');
|
||||
return;
|
||||
}
|
||||
console.log('Chutes model changed to', value);
|
||||
oai_settings.chutes_model = value;
|
||||
}
|
||||
|
||||
if ($(this).is('#model_nanogpt_select')) {
|
||||
if (!value || !hasModelsLoaded) {
|
||||
console.debug('Null NanoGPT model selected. Ignoring.');
|
||||
@@ -5070,6 +5222,17 @@ async function onModelChange() {
|
||||
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.CHUTES) {
|
||||
const maxContext = getChutesMaxContext(oai_settings.chutes_model, oai_settings.max_context_unlocked);
|
||||
$('#openai_max_context').attr('max', maxContext);
|
||||
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
|
||||
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
|
||||
oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
|
||||
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
|
||||
|
||||
calculateChutesCost();
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
|
||||
const maxContext = getElectronHubMaxContext(oai_settings.electronhub_model, oai_settings.max_context_unlocked);
|
||||
$('#openai_max_context').attr('max', maxContext);
|
||||
@@ -5227,6 +5390,10 @@ async function onOpenrouterModelSortChange() {
|
||||
await getStatusOpen();
|
||||
}
|
||||
|
||||
async function onChutesModelSortChange() {
|
||||
await getStatusOpen();
|
||||
}
|
||||
|
||||
async function onElectronHubModelSortChange() {
|
||||
await getStatusOpen();
|
||||
}
|
||||
@@ -5273,6 +5440,7 @@ async function onConnectButtonClick(e) {
|
||||
[chat_completion_sources.COMETAPI]: { key: SECRET_KEYS.COMETAPI, selector: '#api_key_cometapi', proxy: false },
|
||||
[chat_completion_sources.AZURE_OPENAI]: { key: SECRET_KEYS.AZURE_OPENAI, selector: '#api_key_azure_openai', proxy: false },
|
||||
[chat_completion_sources.ZAI]: { key: SECRET_KEYS.ZAI, selector: '#api_key_zai', proxy: false },
|
||||
[chat_completion_sources.CHUTES]: { key: SECRET_KEYS.CHUTES, selector: '#api_key_chutes', proxy: false },
|
||||
};
|
||||
|
||||
// Vertex AI Express version - use API key
|
||||
@@ -5345,6 +5513,9 @@ function toggleChatCompletionForms() {
|
||||
else if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
|
||||
$('#model_groq_select').trigger('change');
|
||||
}
|
||||
else if (oai_settings.chat_completion_source == chat_completion_sources.CHUTES) {
|
||||
$('#model_chutes_select').trigger('change');
|
||||
}
|
||||
else if (oai_settings.chat_completion_source == chat_completion_sources.SILICONFLOW) {
|
||||
$('#model_siliconflow_select').trigger('change');
|
||||
}
|
||||
@@ -5542,6 +5713,8 @@ export function isImageInliningSupported() {
|
||||
return visionSupportedModels.some(model => oai_settings.xai_model.includes(model));
|
||||
case chat_completion_sources.AIMLAPI:
|
||||
return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.aimlapi_model)?.features?.includes('openai/chat-completion.vision'));
|
||||
case chat_completion_sources.CHUTES:
|
||||
return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.chutes_model)?.input_modalities?.includes('image'));
|
||||
case chat_completion_sources.ELECTRONHUB:
|
||||
return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.electronhub_model)?.metadata?.vision);
|
||||
case chat_completion_sources.POLLINATIONS:
|
||||
@@ -5972,6 +6145,7 @@ export function initOpenAI() {
|
||||
$('#openai_max_context_counter').val(`${$(this).val()}`);
|
||||
calculateOpenRouterCost();
|
||||
calculateElectronHubCost();
|
||||
calculateChutesCost();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
@@ -5979,6 +6153,7 @@ export function initOpenAI() {
|
||||
oai_settings.openai_max_tokens = Number($(this).val());
|
||||
calculateOpenRouterCost();
|
||||
calculateElectronHubCost();
|
||||
calculateChutesCost();
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
@@ -6178,6 +6353,11 @@ export function initOpenAI() {
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#chutes_sort_models').on('input', function () {
|
||||
oai_settings.chutes_sort_models = String($(this).val());
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#electronhub_group_models').on('input', function () {
|
||||
oai_settings.electronhub_group_models = !!$(this).prop('checked');
|
||||
saveSettingsDebounced();
|
||||
@@ -6377,6 +6557,14 @@ export function initOpenAI() {
|
||||
templateResult: getElectronHubModelTemplate,
|
||||
matcher: textValueMatcher,
|
||||
});
|
||||
$('#model_chutes_select').select2({
|
||||
placeholder: t`Select a model`,
|
||||
searchInputPlaceholder: t`Search models...`,
|
||||
searchInputCssClass: 'text_pole',
|
||||
width: '100%',
|
||||
templateResult: getChutesModelTemplate,
|
||||
matcher: textValueMatcher,
|
||||
});
|
||||
$('#completion_prompt_manager_popup_entry_form_injection_trigger').select2({
|
||||
placeholder: t`All types (default)`,
|
||||
width: '100%',
|
||||
@@ -6427,6 +6615,7 @@ export function initOpenAI() {
|
||||
$('#model_openrouter_select').on('change', onModelChange);
|
||||
$('#openrouter_group_models').on('change', onOpenrouterModelSortChange);
|
||||
$('#openrouter_sort_models').on('change', onOpenrouterModelSortChange);
|
||||
$('#chutes_sort_models').on('change', onChutesModelSortChange);
|
||||
$('#electronhub_group_models').on('change', onElectronHubModelSortChange);
|
||||
$('#electronhub_sort_models').on('change', onElectronHubModelSortChange);
|
||||
$('#model_ai21_select').on('change', onModelChange);
|
||||
@@ -6434,6 +6623,7 @@ export function initOpenAI() {
|
||||
$('#model_cohere_select').on('change', onModelChange);
|
||||
$('#model_perplexity_select').on('change', onModelChange);
|
||||
$('#model_groq_select').on('change', onModelChange);
|
||||
$('#model_chutes_select').on('change', onModelChange);
|
||||
$('#model_siliconflow_select').on('change', onModelChange);
|
||||
$('#model_electronhub_select').on('change', onModelChange);
|
||||
$('#model_nanogpt_select').on('change', onModelChange);
|
||||
|
||||
@@ -125,6 +125,7 @@ export function extractReasoningFromData(data, {
|
||||
case chat_completion_sources.POLLINATIONS:
|
||||
case chat_completion_sources.MOONSHOT:
|
||||
case chat_completion_sources.COMETAPI:
|
||||
case chat_completion_sources.CHUTES:
|
||||
case chat_completion_sources.ELECTRONHUB:
|
||||
case chat_completion_sources.NANOGPT:
|
||||
case chat_completion_sources.SILICONFLOW:
|
||||
|
||||
@@ -52,6 +52,7 @@ export const SECRET_KEYS = {
|
||||
HUGGINGFACE: 'api_key_huggingface',
|
||||
STABILITY: 'api_key_stability',
|
||||
CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
|
||||
CHUTES: 'api_key_chutes',
|
||||
ELECTRONHUB: 'api_key_electronhub',
|
||||
NANOGPT: 'api_key_nanogpt',
|
||||
TAVILY: 'api_key_tavily',
|
||||
@@ -99,6 +100,7 @@ const FRIENDLY_NAMES = {
|
||||
[SECRET_KEYS.GROQ]: 'Groq',
|
||||
[SECRET_KEYS.FEATHERLESS]: 'Featherless',
|
||||
[SECRET_KEYS.HUGGINGFACE]: 'HuggingFace',
|
||||
[SECRET_KEYS.CHUTES]: 'Chutes',
|
||||
[SECRET_KEYS.ELECTRONHUB]: 'Electron Hub',
|
||||
[SECRET_KEYS.NANOGPT]: 'NanoGPT',
|
||||
[SECRET_KEYS.GENERIC]: 'Generic (OpenAI-compatible)',
|
||||
@@ -156,6 +158,7 @@ const INPUT_MAP = {
|
||||
[SECRET_KEYS.GROQ]: '#api_key_groq',
|
||||
[SECRET_KEYS.FEATHERLESS]: '#api_key_featherless',
|
||||
[SECRET_KEYS.HUGGINGFACE]: '#api_key_huggingface',
|
||||
[SECRET_KEYS.CHUTES]: '#api_key_chutes',
|
||||
[SECRET_KEYS.ELECTRONHUB]: '#api_key_electronhub',
|
||||
[SECRET_KEYS.NANOGPT]: '#api_key_nanogpt',
|
||||
[SECRET_KEYS.GENERIC]: '#api_key_generic',
|
||||
|
||||
@@ -4846,6 +4846,7 @@ function getModelOptions(quiet) {
|
||||
{ id: 'model_cohere_select', api: 'openai', type: chat_completion_sources.COHERE },
|
||||
{ id: 'model_perplexity_select', api: 'openai', type: chat_completion_sources.PERPLEXITY },
|
||||
{ id: 'model_groq_select', api: 'openai', type: chat_completion_sources.GROQ },
|
||||
{ id: 'model_chutes_select', api: 'openai', type: chat_completion_sources.CHUTES },
|
||||
{ id: 'model_siliconflow_select', api: 'openai', type: chat_completion_sources.SILICONFLOW },
|
||||
{ id: 'model_electronhub_select', api: 'openai', type: chat_completion_sources.ELECTRONHUB },
|
||||
{ id: 'model_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT },
|
||||
|
||||
@@ -63,6 +63,8 @@ import {
|
||||
ensureMessageMediaIsArray,
|
||||
getMediaDisplay,
|
||||
getMediaIndex,
|
||||
scrollChatToBottom,
|
||||
scrollOnMediaLoad,
|
||||
} from '../script.js';
|
||||
import {
|
||||
extension_settings,
|
||||
@@ -217,6 +219,8 @@ export function getContext() {
|
||||
ensureMessageMediaIsArray,
|
||||
getMediaDisplay,
|
||||
getMediaIndex,
|
||||
scrollChatToBottom,
|
||||
scrollOnMediaLoad,
|
||||
swipe: {
|
||||
left: swipe_left,
|
||||
right: swipe_right,
|
||||
|
||||
@@ -699,6 +699,26 @@ export function getTokenizerModel() {
|
||||
}
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.CHUTES && oai_settings.chutes_model) {
|
||||
const model = oai_settings.chutes_model.toLowerCase();
|
||||
|
||||
if (model.includes('deepseek') || model.includes('mai-ds')) {
|
||||
return deepseekTokenizer;
|
||||
} else if (model.includes('qwen') || model.includes('qwq') || model.includes('tongyi') || model.includes('kimi')) {
|
||||
return qwen2Tokenizer;
|
||||
} else if (model.includes('llama') || model.includes('longcat') || model.includes('hermes')) {
|
||||
return llama3Tokenizer;
|
||||
} else if (model.includes('gemma')) {
|
||||
return gemmaTokenizer;
|
||||
} else if (model.includes('nemo')) {
|
||||
return nemoTokenizer;
|
||||
} else if (model.includes('mistral')) {
|
||||
return mistralTokenizer;
|
||||
} else if (model.includes('gpt-oss')) {
|
||||
return gpt4oTokenizer;
|
||||
}
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) {
|
||||
if (oai_settings.cohere_model.includes('command-a')) {
|
||||
return commandATokenizer;
|
||||
@@ -1206,3 +1226,4 @@ export async function initTokenizers() {
|
||||
await loadTokenCache();
|
||||
registerDebugFunction('resetTokenCache', 'Reset token cache', 'Purges the calculated token counts. Use this if you want to force a full re-tokenization of all chats or suspect the token counts are wrong.', resetTokenCache);
|
||||
}
|
||||
|
||||
|
||||
@@ -559,6 +559,10 @@ export class ToolManager {
|
||||
const targetValue = target[key];
|
||||
|
||||
if (deltaValue === null || deltaValue === undefined) {
|
||||
// Don't reset the value if it already exists
|
||||
if (targetValue) {
|
||||
continue;
|
||||
}
|
||||
target[key] = deltaValue;
|
||||
continue;
|
||||
}
|
||||
@@ -634,10 +638,17 @@ export class ToolManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source === chat_completion_sources.CHUTES && Array.isArray(model_list)) {
|
||||
const currentModel = model_list.find(model => model.id === oai_settings.chutes_model);
|
||||
if (currentModel) {
|
||||
return currentModel.supported_features?.includes('tools');
|
||||
}
|
||||
}
|
||||
|
||||
if (oai_settings.chat_completion_source === chat_completion_sources.ELECTRONHUB && Array.isArray(model_list)) {
|
||||
const currentModel = model_list.find(model => model.id === oai_settings.electronhub_model);
|
||||
if (currentModel && currentModel.metadata?.function_call) {
|
||||
return currentModel.metadata.function_call;
|
||||
if (currentModel) {
|
||||
return currentModel.metadata?.function_call;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -659,6 +670,7 @@ export class ToolManager {
|
||||
chat_completion_sources.MOONSHOT,
|
||||
chat_completion_sources.FIREWORKS,
|
||||
chat_completion_sources.COMETAPI,
|
||||
chat_completion_sources.CHUTES,
|
||||
chat_completion_sources.ELECTRONHUB,
|
||||
chat_completion_sources.AZURE_OPENAI,
|
||||
chat_completion_sources.ZAI,
|
||||
|
||||
@@ -196,6 +196,7 @@ export const CHAT_COMPLETION_SOURCES = {
|
||||
COHERE: 'cohere',
|
||||
PERPLEXITY: 'perplexity',
|
||||
GROQ: 'groq',
|
||||
CHUTES: 'chutes',
|
||||
ELECTRONHUB: 'electronhub',
|
||||
NANOGPT: 'nanogpt',
|
||||
DEEPSEEK: 'deepseek',
|
||||
|
||||
@@ -68,6 +68,7 @@ const API_GROQ = 'https://api.groq.com/openai/v1';
|
||||
const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';
|
||||
const API_VERTEX_AI = 'https://us-central1-aiplatform.googleapis.com';
|
||||
const API_AI21 = 'https://api.ai21.com/studio/v1';
|
||||
const API_CHUTES = 'https://llm.chutes.ai/v1';
|
||||
const API_ELECTRONHUB = 'https://api.electronhub.ai/v1';
|
||||
const API_NANOGPT = 'https://nano-gpt.com/api/v1';
|
||||
const API_DEEPSEEK = 'https://api.deepseek.com/beta';
|
||||
@@ -1354,6 +1355,108 @@ async function sendElectronHubRequest(request, response) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to Chutes.
|
||||
* @param {express.Request} request Express request
|
||||
* @param {express.Response} response Express response
|
||||
*/
|
||||
async function sendChutesRequest(request, response) {
|
||||
const apiUrl = API_CHUTES;
|
||||
const apiKey = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
|
||||
|
||||
if (!apiKey) {
|
||||
console.warn('Chutes key is missing.');
|
||||
return response.status(400).send({ error: true });
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
request.socket.removeAllListeners('close');
|
||||
request.socket.on('close', function () {
|
||||
controller.abort();
|
||||
});
|
||||
|
||||
try {
|
||||
let bodyParams = {};
|
||||
|
||||
if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
|
||||
bodyParams['tools'] = request.body.tools;
|
||||
bodyParams['tool_choice'] = request.body.tool_choice;
|
||||
}
|
||||
|
||||
if (request.body.logprobs > 0) {
|
||||
bodyParams['top_logprobs'] = request.body.logprobs;
|
||||
bodyParams['logprobs'] = true;
|
||||
}
|
||||
|
||||
if (request.body.json_schema) {
|
||||
bodyParams['response_format'] = {
|
||||
type: 'json_schema',
|
||||
json_schema: {
|
||||
name: request.body.json_schema.name,
|
||||
description: request.body.json_schema.description,
|
||||
schema: request.body.json_schema.value,
|
||||
strict: request.body.json_schema.strict ?? true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
'messages': request.body.messages,
|
||||
'model': request.body.model,
|
||||
'temperature': request.body.temperature,
|
||||
'max_tokens': request.body.max_tokens,
|
||||
'stream': request.body.stream,
|
||||
'presence_penalty': request.body.presence_penalty,
|
||||
'frequency_penalty': request.body.frequency_penalty,
|
||||
'repetition_penalty': request.body.repetition_penalty,
|
||||
'min_p': request.body.min_p,
|
||||
'top_p': request.body.top_p,
|
||||
'top_k': request.body.top_k,
|
||||
'seed': request.body.seed,
|
||||
'stop': request.body.stop,
|
||||
'reasoning_effort': request.body.reasoning_effort,
|
||||
'logit_bias': request.body.logit_bias,
|
||||
...bodyParams,
|
||||
};
|
||||
|
||||
const config = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + apiKey,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
console.debug('Chutes request:', requestBody);
|
||||
|
||||
const generateResponse = await fetch(apiUrl + '/chat/completions', config);
|
||||
|
||||
if (request.body.stream) {
|
||||
forwardFetchResponse(generateResponse, response);
|
||||
} else {
|
||||
if (!generateResponse.ok) {
|
||||
const errorText = await generateResponse.text();
|
||||
console.warn('Chutes returned error: ', errorText);
|
||||
const errorJson = tryParse(errorText) ?? { error: true };
|
||||
return response.status(500).send(errorJson);
|
||||
}
|
||||
const generateResponseJson = await generateResponse.json();
|
||||
console.debug('Chutes response:', generateResponseJson);
|
||||
return response.send(generateResponseJson);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error communicating with Chutes: ', error);
|
||||
if (!response.headersSent) {
|
||||
response.send({ error: true });
|
||||
} else {
|
||||
response.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a chat completion request to Azure OpenAI.
|
||||
* @param {express.Request} request Express request object (contains request.body with all generate_data)
|
||||
@@ -1480,6 +1583,10 @@ router.post('/status', async function (request, statusResponse) {
|
||||
apiUrl = API_COHERE_V1;
|
||||
apiKey = readSecret(request.user.directories, SECRET_KEYS.COHERE);
|
||||
headers = {};
|
||||
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CHUTES) {
|
||||
apiUrl = API_CHUTES;
|
||||
apiKey = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
|
||||
headers = {};
|
||||
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ELECTRONHUB) {
|
||||
apiUrl = API_ELECTRONHUB;
|
||||
apiKey = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);
|
||||
@@ -1671,6 +1778,22 @@ router.post('/status', async function (request, statusResponse) {
|
||||
data = { data: data.map(model => ({ id: model.name, ...model })) };
|
||||
}
|
||||
|
||||
if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CHUTES && Array.isArray(data?.data)) {
|
||||
data.data = data.data.map(model => {
|
||||
if (model.pricing?.prompt !== undefined && model.pricing?.completion !== undefined) {
|
||||
return {
|
||||
...model,
|
||||
pricing: {
|
||||
...model.pricing,
|
||||
input: model.pricing.prompt,
|
||||
output: model.pricing.completion,
|
||||
},
|
||||
};
|
||||
}
|
||||
return model;
|
||||
});
|
||||
}
|
||||
|
||||
statusResponse.send(data);
|
||||
|
||||
if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COHERE && Array.isArray(data?.models)) {
|
||||
@@ -1831,6 +1954,7 @@ router.post('/generate', function (request, response) {
|
||||
case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);
|
||||
case CHAT_COMPLETION_SOURCES.AIMLAPI: return sendAimlapiRequest(request, response);
|
||||
case CHAT_COMPLETION_SOURCES.XAI: return sendXaiRequest(request, response);
|
||||
case CHAT_COMPLETION_SOURCES.CHUTES: return sendChutesRequest(request, response);
|
||||
case CHAT_COMPLETION_SOURCES.ELECTRONHUB: return sendElectronHubRequest(request, response);
|
||||
case CHAT_COMPLETION_SOURCES.AZURE_OPENAI: return sendAzureOpenAIRequest(request, response);
|
||||
}
|
||||
@@ -2017,8 +2141,7 @@ router.post('/generate', function (request, response) {
|
||||
'ttl': cacheTTL,
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {
|
||||
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {
|
||||
apiUrl = API_POLLINATIONS;
|
||||
apiKey = 'NONE';
|
||||
headers = {
|
||||
@@ -2314,6 +2437,37 @@ multimodalModels.post('/electronhub', async (_req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
multimodalModels.post('/chutes', async (req, res) => {
|
||||
try {
|
||||
const key = readSecret(req.user.directories, SECRET_KEYS.CHUTES);
|
||||
|
||||
if (!key) {
|
||||
return res.json([]);
|
||||
}
|
||||
|
||||
const response = await fetch('https://llm.chutes.ai/v1/models', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${key}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return res.json([]);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
const modelsData = /** @type {{object: string, data: Array<{id: string, input_modalities?: string[]}>}} */ (data);
|
||||
const multimodalModels = modelsData.data
|
||||
.filter(m => m.input_modalities?.includes('image'))
|
||||
.map(m => m.id);
|
||||
return res.json(multimodalModels);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
multimodalModels.post('/mistral', async (req, res) => {
|
||||
try {
|
||||
const key = readSecret(req.user.directories, SECRET_KEYS.MISTRALAI);
|
||||
|
||||
@@ -81,6 +81,10 @@ router.post('/caption-image', async (request, response) => {
|
||||
key = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
|
||||
}
|
||||
|
||||
if (request.body.api === 'chutes') {
|
||||
key = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
|
||||
}
|
||||
|
||||
if (request.body.api === 'electronhub') {
|
||||
key = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);
|
||||
}
|
||||
@@ -177,6 +181,10 @@ router.post('/caption-image', async (request, response) => {
|
||||
apiUrl = 'https://nano-gpt.com/api/v1/chat/completions';
|
||||
}
|
||||
|
||||
if (request.body.api === 'chutes') {
|
||||
apiUrl = 'https://llm.chutes.ai/v1/chat/completions';
|
||||
}
|
||||
|
||||
if (request.body.api === 'electronhub') {
|
||||
apiUrl = 'https://api.electronhub.ai/v1/chat/completions';
|
||||
}
|
||||
@@ -431,6 +439,84 @@ router.post('/electronhub/models', async (request, response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Chutes TTS
|
||||
router.post('/chutes/generate-voice', async (request, response) => {
|
||||
try {
|
||||
const key = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
|
||||
|
||||
if (!key) {
|
||||
console.warn('No Chutes key found');
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
text: request.body.input,
|
||||
voice: request.body.voice || 'af_heart',
|
||||
};
|
||||
|
||||
console.debug('Chutes TTS request', requestBody);
|
||||
|
||||
const result = await fetch('https://chutes-kokoro.chutes.ai/speak', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
const text = await result.text();
|
||||
console.warn('Chutes TTS request failed', result.statusText, text);
|
||||
return response.status(500).send(text);
|
||||
}
|
||||
|
||||
const contentType = result.headers.get('content-type') || 'audio/mpeg';
|
||||
const buffer = await result.arrayBuffer();
|
||||
response.setHeader('Content-Type', contentType);
|
||||
return response.send(Buffer.from(buffer));
|
||||
} catch (error) {
|
||||
console.error('Chutes TTS generation failed', error);
|
||||
response.status(500).send('Internal server error');
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/chutes/models/embedding', async (request, response) => {
|
||||
try {
|
||||
const key = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
|
||||
|
||||
if (!key) {
|
||||
console.warn('No Chutes key found');
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
const result = await fetch('https://api.chutes.ai/chutes/?template=embedding&include_public=true&limit=999', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${key}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
const text = await result.text();
|
||||
console.warn('Chutes embedding models request failed', result.statusText, text);
|
||||
return response.status(500).send(text);
|
||||
}
|
||||
|
||||
/** @type {any} */
|
||||
const data = await result.json();
|
||||
|
||||
if (!Array.isArray(data?.items)) {
|
||||
console.warn('Chutes embedding models response invalid', data);
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
return response.json(data.items);
|
||||
} catch (error) {
|
||||
console.error('Chutes embedding models fetch failed', error);
|
||||
response.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/generate-image', async (request, response) => {
|
||||
try {
|
||||
const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);
|
||||
|
||||
@@ -45,6 +45,7 @@ export const SECRET_KEYS = {
|
||||
STABILITY: 'api_key_stability',
|
||||
CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
|
||||
TAVILY: 'api_key_tavily',
|
||||
CHUTES: 'api_key_chutes',
|
||||
ELECTRONHUB: 'api_key_electronhub',
|
||||
NANOGPT: 'api_key_nanogpt',
|
||||
BFL: 'api_key_bfl',
|
||||
|
||||
@@ -1083,6 +1083,89 @@ electronhub.post('/sizes', async (request, response) => {
|
||||
return response.send({ sizes });
|
||||
});
|
||||
|
||||
const chutes = express.Router();
|
||||
|
||||
chutes.post('/models', async (request, response) => {
|
||||
try {
|
||||
const key = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
|
||||
|
||||
if (!key) {
|
||||
console.warn('Chutes key not found.');
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
const modelsResponse = await fetch('https://api.chutes.ai/chutes/?template=diffusion&include_public=true&limit=999', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${key}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!modelsResponse.ok) {
|
||||
console.warn('Chutes returned an error.');
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
|
||||
const data = await modelsResponse.json();
|
||||
|
||||
const chutesData = /** @type {{items: Array<{name: string}>}} */ (data);
|
||||
const models = chutesData.items.map(x => ({ value: x.name, text: x.name })).sort((a, b) => a?.text?.localeCompare(b?.text));
|
||||
return response.send(models);
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
chutes.post('/generate', async (request, response) => {
|
||||
try {
|
||||
const key = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
|
||||
|
||||
if (!key) {
|
||||
console.warn('Chutes key not found.');
|
||||
return response.sendStatus(400);
|
||||
}
|
||||
|
||||
const bodyParams = {
|
||||
model: request.body.model,
|
||||
prompt: request.body.prompt,
|
||||
negative_prompt: request.body.negative_prompt,
|
||||
guidance_scale: request.body.guidance_scale || 7.0,
|
||||
width: request.body.width || 1024,
|
||||
height: request.body.height || 1024,
|
||||
num_inference_steps: request.body.steps || 10,
|
||||
};
|
||||
|
||||
console.debug('Chutes request:', bodyParams);
|
||||
|
||||
const result = await fetch('https://image.chutes.ai/generate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${key}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(bodyParams),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
const text = await result.text();
|
||||
console.warn('Chutes returned an error:', text);
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
|
||||
const buffer = await result.arrayBuffer();
|
||||
const base64 = Buffer.from(buffer).toString('base64');
|
||||
|
||||
return response.send({ image: base64 });
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
return response.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
const nanogpt = express.Router();
|
||||
|
||||
nanogpt.post('/models', async (request, response) => {
|
||||
@@ -1566,6 +1649,7 @@ router.use('/drawthings', drawthings);
|
||||
router.use('/pollinations', pollinations);
|
||||
router.use('/stability', stability);
|
||||
router.use('/huggingface', huggingface);
|
||||
router.use('/chutes', chutes);
|
||||
router.use('/electronhub', electronhub);
|
||||
router.use('/nanogpt', nanogpt);
|
||||
router.use('/bfl', bfl);
|
||||
|
||||
@@ -36,6 +36,7 @@ const SOURCES = [
|
||||
'vertexai',
|
||||
'electronhub',
|
||||
'openrouter',
|
||||
'chutes',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -79,6 +80,8 @@ async function getVector(source, sourceSettings, text, isQuery, directories) {
|
||||
return sourceSettings.embeddings[text];
|
||||
case 'koboldcpp':
|
||||
return sourceSettings.embeddings[text];
|
||||
case 'chutes':
|
||||
return getOpenAIVector(text, source, directories, sourceSettings.model);
|
||||
}
|
||||
|
||||
throw new Error(`Unknown vector source ${source}`);
|
||||
@@ -144,6 +147,9 @@ async function getBatchVector(source, sourceSettings, texts, isQuery, directorie
|
||||
case 'koboldcpp':
|
||||
results.push(...texts.map(x => sourceSettings.embeddings[x]));
|
||||
break;
|
||||
case 'chutes':
|
||||
results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown vector source ${source}`);
|
||||
}
|
||||
@@ -228,6 +234,10 @@ function getSourceSettings(source, request) {
|
||||
model: String(request.body.model),
|
||||
embeddings: request.body.embeddings ?? {},
|
||||
};
|
||||
case 'chutes':
|
||||
return {
|
||||
model: String(request.body.model || 'chutes-qwen-qwen3-embedding-8b'),
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -8,30 +8,44 @@ const SOURCES = {
|
||||
url: 'https://api.together.xyz/v1',
|
||||
model: 'togethercomputer/m2-bert-80M-32k-retrieval',
|
||||
headers: {},
|
||||
processBody: () => {},
|
||||
},
|
||||
'mistral': {
|
||||
secretKey: SECRET_KEYS.MISTRALAI,
|
||||
url: 'https://api.mistral.ai/v1',
|
||||
model: 'mistral-embed',
|
||||
headers: {},
|
||||
processBody: () => {},
|
||||
},
|
||||
'openai': {
|
||||
secretKey: SECRET_KEYS.OPENAI,
|
||||
url: 'https://api.openai.com/v1',
|
||||
model: 'text-embedding-ada-002',
|
||||
headers: {},
|
||||
processBody: () => {},
|
||||
},
|
||||
'electronhub': {
|
||||
secretKey: SECRET_KEYS.ELECTRONHUB,
|
||||
url: 'https://api.electronhub.ai/v1',
|
||||
model: 'text-embedding-3-small',
|
||||
headers: {},
|
||||
processBody: () => {},
|
||||
},
|
||||
'openrouter': {
|
||||
secretKey: SECRET_KEYS.OPENROUTER,
|
||||
url: 'https://openrouter.ai/api/v1',
|
||||
model: 'openai/text-embedding-3-large',
|
||||
headers: { ...OPENROUTER_HEADERS },
|
||||
processBody: () => {},
|
||||
},
|
||||
'chutes': {
|
||||
secretKey: SECRET_KEYS.CHUTES,
|
||||
url: 'https://{{MODEL}}.chutes.ai/v1',
|
||||
model: 'chutes-qwen-qwen3-embedding-8b',
|
||||
headers: {},
|
||||
processBody: (body) => {
|
||||
body.model = null;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -58,7 +72,17 @@ export async function getOpenAIBatchVector(texts, source, directories, model = '
|
||||
throw new Error('No API key found');
|
||||
}
|
||||
|
||||
const url = config.url;
|
||||
const modelName = model || config.model;
|
||||
const url = config.url.replace('{{MODEL}}', modelName);
|
||||
const body = {
|
||||
input: texts,
|
||||
model: modelName,
|
||||
};
|
||||
|
||||
if (typeof config.processBody === 'function') {
|
||||
config.processBody(body);
|
||||
}
|
||||
|
||||
const response = await fetch(`${url}/embeddings`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -66,10 +90,7 @@ export async function getOpenAIBatchVector(texts, source, directories, model = '
|
||||
'Authorization': `Bearer ${key}`,
|
||||
...config.headers,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
input: texts,
|
||||
model: model || config.model,
|
||||
}),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
Reference in New Issue
Block a user