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>

55a07d445daf7b37ec5ad2bcb39feab5ddac4362

Ben <44178664+cxmplex@users.noreply.github.com>

Signed
28 files changed, +1049 -33Showing whitespace changes
default/content/presets/openai/Default.json+2 -0
@@ -8,6 +8,8 @@
88 "openrouter_sort_models": "alphabetically",
99 "ai21_model": "jamba-large",
1010 "mistralai_model": "mistral-large-latest",
11+ "chutes_model": "deepseek-ai/DeepSeek-V3-0324",
12+ "chutes_sort_models": "alphabetically",
1113 "electronhub_model": "gpt-4o-mini",
1214 "electronhub_sort_models": "alphabetically",
1315 "electronhub_group_models": false,
public/img/chutes.svg+4 -0
@@ -0,0 +1,4 @@
1+<svg width="246" height="153" viewBox="0 0 246 153" fill="none" xmlns="http://www.w3.org/2000/svg">
2+<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" />
3+<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" />
4+</svg>
public/index.html+59 -16
@@ -678,6 +678,9 @@
678678 <div data-source="electronhub">
679679 <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="electronhub_max_prompt_cost" data-i18n="Unknown">Unknown</span>
680680 </div>
681+ <div data-source="chutes">
682+ <span data-i18n="Max prompt cost:">Max prompt cost:</span> <span id="chutes_max_prompt_cost" data-i18n="Unknown">Unknown</span>
683+ </div>
681684 <hr>
682685 <div class="range-block">
683686 <label for="stream_toggle" title="Enable OpenAI completion streaming" data-i18n="[title]Enable OpenAI completion streaming" class="checkbox_label widthFreeExpand">
@@ -693,7 +696,7 @@
693696 </span>
694697 </div>
695698 </div>
696699 <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">
697700 <div class="range-block-title" data-i18n="Temperature">
698701 Temperature
699702 </div>
@@ -706,7 +709,7 @@
706709 </div>
707710 </div>
708711 </div>
709712 <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">
710713 <div class="range-block-title" data-i18n="Frequency Penalty">
711714 Frequency Penalty
712715 </div>
@@ -719,7 +722,7 @@
719722 </div>
720723 </div>
721724 </div>
722725 <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">
723726 <div class="range-block-title" data-i18n="Presence Penalty">
724727 Presence Penalty
725728 </div>
@@ -732,7 +735,7 @@
732735 </div>
733736 </div>
734737 </div>
735738 <div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity,electronhub,chutes,nanogpt">
736739 <div class="range-block-title" data-i18n="Top K">
737740 Top K
738741 </div>
@@ -745,7 +748,7 @@
745748 </div>
746749 </div>
747750 </div>
748751 <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">
749752 <div class="range-block-title" data-i18n="Top P">
750753 Top P
751754 </div>
@@ -758,7 +761,7 @@
758761 </div>
759762 </div>
760763 </div>
761764 <div class="range-block" data-source="openrouter,nanogpt,chutes">
762765 <div class="range-block-title" data-i18n="Repetition Penalty">
763766 Repetition Penalty
764767 </div>
@@ -771,7 +774,7 @@
771774 </div>
772775 </div>
773776 </div>
774777 <div class="range-block" data-source="openrouter,nanogpt,chutes">
775778 <div class="range-block-title" data-i18n="Min P">
776779 Min P
777780 </div>
@@ -982,7 +985,7 @@
982985 </div>
983986 </div>
984987 </div>
985988 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,electronhub,chutes,nanogpt,xai,pollinations,aimlapi,makersuite,vertexai,azure_openai">
986989 <div class="range-block-title justifyLeft" data-i18n="Seed">
987990 Seed
988991 </div>
@@ -1956,7 +1959,7 @@
19561959 </span>
19571960 </div>
19581961 </div>
19591962 <div class="range-block" data-source="makersuite,vertexai,aimlapi,openrouter,claude,xai,electronhub,chutes,nanogpt">
19601963 <label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">
19611964 <input id="openai_enable_web_search" type="checkbox" />
19621965 <span data-i18n="Enable web search">Enable web search</span>
@@ -1970,7 +1973,7 @@
19701973 </b>
19711974 </div>
19721975 </div>
19731976 <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">
19741977 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
19751978 <input id="openai_function_calling" type="checkbox" />
19761979 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -1985,7 +1988,7 @@
19851988 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>
19861989 </div>
19871990 </div>
19881991 <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">
19891992 <label for="openai_media_inlining" class="checkbox_label flexWrap widthFreeExpand">
19901993 <input id="openai_media_inlining" type="checkbox" />
19911994 <span data-i18n="Send inline media">Send inline media</span>
@@ -2009,7 +2012,7 @@
20092012 <strong data-source="makersuite,vertexai" data-i18n="video_inlining_hint_4">Videos must be less than 20 MB and under 1 minute long.</strong>
20102013 <strong data-source="makersuite,vertexai" data-i18n="audio_inlining_hint_2">Audio must be less than 20 MB.</strong>
20112014 </div>
20122015 <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">
20132016 <div class="flex-container oneline-dropdown">
20142017 <label for="openai_inline_image_quality" data-i18n="Inline Image Quality">
20152018 Inline Image Quality
@@ -2070,7 +2073,7 @@
20702073 </div>
20712074 </div>
20722075 </div>
20732076 <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">
20742077 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
20752078 <input id="openai_show_thoughts" type="checkbox" />
20762079 <span data-i18n="Request model reasoning">Request model reasoning</span>
@@ -2084,7 +2087,7 @@
20842087 </strong>
20852088 </div>
20862089 </div>
20872090 <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">
20882091 <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;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.">
20892092 <label for="openai_reasoning_effort">
20902093 <span data-i18n="Reasoning Effort">Reasoning Effort</span>
@@ -2098,7 +2101,7 @@
20982101 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>
20992102 <option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>
21002103 </select>
21012104 <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.">
21022105 OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.
21032106 </div>
21042107 <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 @@
21442147 </div>
21452148 </div>
21462149 </div>
21472150 <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,custom,electronhub,azure_openai,chutes">
21482151 <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias">
21492152 Logit Bias
21502153 </div>
@@ -2803,6 +2806,7 @@
28032806 <option value="ai21">AI21</option>
28042807 <option value="aimlapi">AI/ML API</option>
28052808 <option value="azure_openai">Azure OpenAI</option>
2809+ <option value="chutes">Chutes</option>
28062810 <option value="claude">Claude</option>
28072811 <option value="cohere">Cohere</option>
28082812 <!-- Temporarily disabled. -->
@@ -3479,6 +3483,45 @@
34793483 </div>
34803484 </div>
34813485 </div>
3486+ <div id="chutes_form" data-source="chutes">
3487+ <h4 data-i18n="Chutes API Key">Chutes API Key</h4>
3488+ <div id="chutes_credits_display">
3489+ <a href="https://chutes.ai/app/api/billing-balance" target="_blank" data-i18n="View Billing/Balance">View Billing/Balance</a>
3490+ </div>
3491+ <div class="flex-container">
3492+ <input id="api_key_chutes" name="api_key_chutes" class="text_pole flex1" value="" type="text" autocomplete="off">
3493+ <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>
3494+ </div>
3495+ <div data-for="api_key_chutes" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
3496+ For privacy reasons, your API key will be hidden after you click 'Connect'.
3497+ </div>
3498+ <div>
3499+ <h4 data-i18n="Chutes Model">Chutes Model</h4>
3500+ <select id="model_chutes_select">
3501+ <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
3502+ </select>
3503+ </div>
3504+ <div class="marginTopBot5">
3505+ <div class="inline-drawer wide100p">
3506+ <div class="inline-drawer-toggle inline-drawer-header">
3507+ <b data-i18n="Chutes Model Sorting">Chutes Model Sorting</b>
3508+ <div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
3509+ </div>
3510+ <div class="inline-drawer-content m-b-1">
3511+ <div class="marginTopBot5">
3512+ <label for="chutes_sort_models" class="checkbox_label">
3513+ <select id="chutes_sort_models">
3514+ <option data-i18n="Alphabetically" value="alphabetically">Alphabetically</option>
3515+ <option data-i18n="Input Price" value="pricing.input">Input Price (cheapest)</option>
3516+ <option data-i18n="Output Price" value="pricing.output">Output Price (cheapest)</option>
3517+ <option data-i18n="Context Size" value="context_length">Context Size</option>
3518+ </select>
3519+ </label>
3520+ </div>
3521+ </div>
3522+ </div>
3523+ </div>
3524+ </div>
34823525 <div id="nanogpt_form" data-source="nanogpt">
34833526 <h4 data-i18n="NanoGPT API Key">NanoGPT API Key</h4>
34843527 <div class="flex-container">
public/script.js+2 -1
@@ -1431,7 +1431,7 @@ export async function printMessages() {
14311431 delay(debounce_timeout.short).then(() => scrollOnMediaLoad());
14321432}
14331433
14341434export function scrollOnMediaLoad() {
14351435 const started = Date.now();
14361436 const media = chatElement.find('.mes_block img, .mes_block video, .mes_block audio').toArray();
14371437 let mediaLoaded = 0;
@@ -5928,6 +5928,7 @@ export function extractJsonFromData(data, { mainApi = null, chatCompletionSource
59285928 case chat_completion_sources.COHERE:
59295929 case chat_completion_sources.XAI:
59305930 case chat_completion_sources.ELECTRONHUB:
5931+ case chat_completion_sources.CHUTES:
59315932 case chat_completion_sources.AZURE_OPENAI:
59325933 case chat_completion_sources.ZAI:
59335934 default:
public/scripts/RossAscends-mods.js+1 -0
@@ -397,6 +397,7 @@ function RA_autoconnect(PrevApi) {
397397 || (secret_state[SECRET_KEYS.COHERE] && oai_settings.chat_completion_source == chat_completion_sources.COHERE)
398398 || (secret_state[SECRET_KEYS.PERPLEXITY] && oai_settings.chat_completion_source == chat_completion_sources.PERPLEXITY)
399399 || (secret_state[SECRET_KEYS.GROQ] && oai_settings.chat_completion_source == chat_completion_sources.GROQ)
400+ || (secret_state[SECRET_KEYS.CHUTES] && oai_settings.chat_completion_source == chat_completion_sources.CHUTES)
400401 || (secret_state[SECRET_KEYS.SILICONFLOW] && oai_settings.chat_completion_source == chat_completion_sources.SILICONFLOW)
401402 || (secret_state[SECRET_KEYS.ELECTRONHUB] && oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB)
402403 || (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT)
public/scripts/extensions/caption/index.js+4 -1
@@ -10,7 +10,7 @@ import { SlashCommand } from '../../slash-commands/SlashCommand.js';
1010import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
1111import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
1212import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';
1313import { debounce_timeout, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR } from '../../constants.js';
1414export { MODULE_NAME };
1515
1616const MODULE_NAME = 'caption';
@@ -211,6 +211,7 @@ async function sendCaptionedMessage(caption, image, mimeType) {
211211 context.addOneMessage(message);
212212 await eventSource.emit(event_types.USER_MESSAGE_RENDERED, messageId);
213213 await context.saveChat();
214+ setTimeout(() => context.scrollOnMediaLoad(), debounce_timeout.short);
214215}
215216
216217/**
@@ -503,6 +504,7 @@ jQuery(async function () {
503504 'aimlapi': SECRET_KEYS.AIMLAPI,
504505 'moonshot': SECRET_KEYS.MOONSHOT,
505506 'nanogpt': SECRET_KEYS.NANOGPT,
507+ 'chutes': SECRET_KEYS.CHUTES,
506508 'electronhub': SECRET_KEYS.ELECTRONHUB,
507509 'zai': SECRET_KEYS.ZAI,
508510 };
@@ -616,6 +618,7 @@ jQuery(async function () {
616618 await processEndpoint('aimlapi', '/api/backends/chat-completions/multimodal-models/aimlapi');
617619 await processEndpoint('pollinations', '/api/backends/chat-completions/multimodal-models/pollinations');
618620 await processEndpoint('nanogpt', '/api/backends/chat-completions/multimodal-models/nanogpt');
621+ await processEndpoint('chutes', '/api/backends/chat-completions/multimodal-models/chutes');
619622 await processEndpoint('electronhub', '/api/backends/chat-completions/multimodal-models/electronhub');
620623 await processEndpoint('mistral', '/api/backends/chat-completions/multimodal-models/mistral');
621624 await processEndpoint('xai', '/api/backends/chat-completions/multimodal-models/xai');
public/scripts/extensions/caption/settings.html+1 -0
@@ -18,6 +18,7 @@
1818 <label for="caption_multimodal_api" data-i18n="API">API</label>
1919 <select id="caption_multimodal_api" class="flex1 text_pole">
2020 <option value="aimlapi">AI/ML API</option>
21+ <option value="chutes">Chutes</option>
2122 <option value="anthropic">Claude</option>
2223 <option value="cohere">Cohere</option>
2324 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
public/scripts/extensions/shared.js+4 -0
@@ -257,6 +257,10 @@ function throwIfInvalidModel(useReverseProxy) {
257257 throw new Error('Electron Hub API key is not set.');
258258 }
259259
260+ if (multimodalApi === 'chutes' && !secret_state[SECRET_KEYS.CHUTES]) {
261+ throw new Error('Chutes API key is not set.');
262+ }
263+
260264 if (multimodalApi === 'zai' && !secret_state[SECRET_KEYS.ZAI]) {
261265 throw new Error('Z.AI API key is not set.');
262266 }
public/scripts/extensions/stable-diffusion/index.js+73 -0
@@ -81,6 +81,7 @@ const sources = {
8181 pollinations: 'pollinations',
8282 stability: 'stability',
8383 huggingface: 'huggingface',
84+ chutes: 'chutes',
8485 electronhub: 'electronhub',
8586 nanogpt: 'nanogpt',
8687 bfl: 'bfl',
@@ -1319,6 +1320,7 @@ async function onModelChange() {
13191320 sources.falai,
13201321 sources.xai,
13211322 sources.google,
1323+ sources.chutes,
13221324 ];
13231325
13241326 if (cloudSources.includes(extension_settings.sd.source)) {
@@ -1531,6 +1533,9 @@ async function loadSamplers() {
15311533 case sources.huggingface:
15321534 samplers = ['N/A'];
15331535 break;
1536+ case sources.chutes:
1537+ samplers = ['N/A'];
1538+ break;
15341539 case sources.electronhub:
15351540 samplers = ['N/A'];
15361541 break;
@@ -1730,6 +1735,9 @@ async function loadModels() {
17301735 case sources.huggingface:
17311736 models = [{ value: '', text: t`<Enter Model ID above>` }];
17321737 break;
1738+ case sources.chutes:
1739+ models = await loadChutesModels();
1740+ break;
17331741 case sources.electronhub:
17341742 models = await loadElectronHubModels();
17351743 break;
@@ -1905,6 +1913,27 @@ async function loadTogetherAIModels() {
19051913 return [];
19061914}
19071915
1916+async function loadChutesModels() {
1917+ if (!secret_state[SECRET_KEYS.CHUTES]) {
1918+ console.debug('Chutes API key is not set.');
1919+ return [];
1920+ }
1921+
1922+ const result = await fetch('/api/sd/chutes/models', {
1923+ method: 'POST',
1924+ headers: getRequestHeaders(),
1925+ });
1926+
1927+ if (result.ok) {
1928+ const models = await result.json();
1929+ console.debug('Loaded Chutes image models:', models);
1930+ return models;
1931+ }
1932+
1933+ console.warn('Failed to load Chutes models:', result.status);
1934+ return [];
1935+}
1936+
19081937async function loadElectronHubModels() {
19091938 if (!secret_state[SECRET_KEYS.ELECTRONHUB]) {
19101939 console.debug('Electron Hub API key is not set.');
@@ -2275,6 +2304,9 @@ async function loadSchedulers() {
22752304 case sources.huggingface:
22762305 schedulers = ['N/A'];
22772306 break;
2307+ case sources.chutes:
2308+ schedulers = ['N/A'];
2309+ break;
22782310 case sources.electronhub:
22792311 schedulers = ['N/A'];
22802312 break;
@@ -2375,6 +2407,9 @@ async function loadVaes() {
23752407 case sources.huggingface:
23762408 vaes = ['N/A'];
23772409 break;
2410+ case sources.chutes:
2411+ vaes = ['N/A'];
2412+ break;
23782413 case sources.electronhub:
23792414 vaes = ['N/A'];
23802415 break;
@@ -2967,6 +3002,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
29673002 case sources.huggingface:
29683003 result = await generateHuggingFaceImage(prefixedPrompt, signal);
29693004 break;
3005+ case sources.chutes:
3006+ result = await generateChutesImage(prefixedPrompt, negativePrompt, signal);
3007+ break;
29703008 case sources.electronhub:
29713009 result = await generateElectronHubImage(prefixedPrompt, signal);
29723010 break;
@@ -3799,6 +3837,38 @@ async function generateHuggingFaceImage(prompt, signal) {
37993837}
38003838
38013839/**
3840+ * Generates an image using the Chutes API.
3841+ * @param {string} prompt - The main instruction used to guide the image generation.
3842+ * @param {string} negativePrompt - The instruction used to restrict the image generation.
3843+ * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
3844+ * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
3845+ */
3846+async function generateChutesImage(prompt, negativePrompt, signal) {
3847+ const result = await fetch('/api/sd/chutes/generate', {
3848+ method: 'POST',
3849+ headers: getRequestHeaders(),
3850+ signal: signal,
3851+ body: JSON.stringify({
3852+ model: extension_settings.sd.model,
3853+ prompt: prompt,
3854+ negative_prompt: negativePrompt,
3855+ width: extension_settings.sd.width,
3856+ height: extension_settings.sd.height,
3857+ steps: extension_settings.sd.steps,
3858+ guidance_scale: extension_settings.sd.scale,
3859+ }),
3860+ });
3861+
3862+ if (result.ok) {
3863+ const data = await result.json();
3864+ return { format: 'jpg', data: data.image };
3865+ } else {
3866+ const text = await result.text();
3867+ throw new Error(text);
3868+ }
3869+}
3870+
3871+/**
38023872 * Generates an image using the Electron Hub API.
38033873 * @param {string} prompt - The main instruction used to guide the image generation.
38043874 * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
@@ -4207,6 +4277,7 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref
42074277 context.addOneMessage(message);
42084278 await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, messageId, 'extension');
42094279 await context.saveChat();
4280+ setTimeout(() => context.scrollOnMediaLoad(), debounce_timeout.short);
42104281}
42114282
42124283/**
@@ -4313,6 +4384,8 @@ function isValidState() {
43134384 return secret_state[SECRET_KEYS.STABILITY];
43144385 case sources.huggingface:
43154386 return secret_state[SECRET_KEYS.HUGGINGFACE];
4387+ case sources.chutes:
4388+ return secret_state[SECRET_KEYS.CHUTES];
43164389 case sources.electronhub:
43174390 return secret_state[SECRET_KEYS.ELECTRONHUB];
43184391 case sources.nanogpt:
public/scripts/extensions/stable-diffusion/settings.html+4 -0
@@ -39,6 +39,7 @@
3939 <select id="sd_source">
4040 <option value="aimlapi">AI/ML API</option>
4141 <option value="bfl">BFL (Black Forest Labs)</option>
42+ <option value="chutes">Chutes</option>
4243 <option value="comfy">ComfyUI</option>
4344 <option value="drawthings">DrawThings HTTP API</option>
4445 <option value="electronhub">Electron Hub</option>
@@ -94,6 +95,9 @@
9495 <label for="sd_huggingface_model_id" data-i18n="Model ID">Model ID</label>
9596 <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="" />
9697 </div>
98+ <div data-sd-source="chutes">
99+ <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>
100+ </div>
97101 <div data-sd-source="electronhub">
98102 <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>
99103 <div class="flex-container" id="sd_electronhub_quality_row">
public/scripts/extensions/tts/chutes.js+233 -0
@@ -0,0 +1,233 @@
1+import { event_types, eventSource, getRequestHeaders } from '../../../script.js';
2+import { SECRET_KEYS, secret_state } from '../../secrets.js';
3+import { getPreviewString, saveTtsProviderSettings } from './index.js';
4+
5+export { ChutesTtsProvider };
6+
7+class ChutesTtsProvider {
8+ settings;
9+ voices = [];
10+ models = [];
11+ separator = ' . ';
12+
13+ defaultSettings = {
14+ voiceMap: {},
15+ model: 'kokoro',
16+ speed: 1,
17+ };
18+
19+ get settingsHtml() {
20+ let html = `
21+ <div class="flex-container alignItemsCenter">
22+ <div class="flex1">Chutes TTS API</div>
23+ <div id="chutes_tts_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_chutes">
24+ <i class="fa-solid fa-key"></i>
25+ <span>API Key</span>
26+ </div>
27+ </div>
28+ <div class="flex-container flexFlowColumn">
29+ <div class="flex1">
30+ <label for="chutes_tts_model">Model</label>
31+ <select id="chutes_tts_model" class="text_pole"></select>
32+ </div>
33+ <div>
34+ <label for="chutes_tts_speed">Speed <span id="chutes_tts_speed_output"></span></label>
35+ <input type="range" id="chutes_tts_speed" value="1" min="0.25" max="4" step="0.05">
36+ </div>
37+ </div>`;
38+ return html;
39+ }
40+
41+ constructor() {
42+ this.handler = async function (/** @type {string} */ key) {
43+ if (key !== SECRET_KEYS.CHUTES) return;
44+ $('#chutes_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.CHUTES]);
45+ await this.onRefreshClick();
46+ }.bind(this);
47+ }
48+
49+ dispose() {
50+ [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
51+ eventSource.removeListener(event, this.handler);
52+ });
53+ }
54+
55+ onSettingsChange() {
56+ this.settings.model = $('#chutes_tts_model').val();
57+ this.settings.speed = $('#chutes_tts_speed').val();
58+ saveTtsProviderSettings();
59+ }
60+
61+ async loadSettings(settings) {
62+ if (Object.keys(settings).length === 0) {
63+ Object.assign(settings, this.defaultSettings);
64+ }
65+
66+ this.settings = settings;
67+
68+ if (!this.settings.voiceMap) {
69+ this.settings.voiceMap = {};
70+ }
71+
72+ // Update UI
73+ $('#chutes_tts_model').val(this.settings.model);
74+ $('#chutes_tts_speed').val(this.settings.speed);
75+ $('#chutes_tts_speed_output').text(this.settings.speed);
76+
77+ $('#chutes_tts_key').toggleClass('success', !!secret_state[SECRET_KEYS.CHUTES]);
78+ [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
79+ eventSource.on(event, this.handler);
80+ });
81+
82+ await this.checkReady();
83+
84+ $('#chutes_tts_model').on('change', () => this.onSettingsChange());
85+ $('#chutes_tts_speed').on('input', function () {
86+ const value = $(this).val();
87+ $('#chutes_tts_speed_output').text(String(value));
88+ });
89+ $('#chutes_tts_speed').on('change', () => this.onSettingsChange());
90+ }
91+
92+ async checkReady() {
93+ await this.updateModels();
94+ if (this.models.length === 0) {
95+ // No models available
96+ }
97+ await this.updateVoices();
98+ }
99+
100+ async onRefreshClick() {
101+ return await this.checkReady();
102+ }
103+
104+ async updateModels() {
105+ // For Chutes TTS, we always use the Kokoro model currently.
106+ this.models = ['kokoro'];
107+
108+ $('#chutes_tts_model').empty();
109+ $('#chutes_tts_model').append($('<option>').val('kokoro').text('Kokoro'));
110+ $('#chutes_tts_model').val('kokoro');
111+
112+ this.settings.model = 'kokoro';
113+ }
114+
115+ async updateVoices() {
116+ // Kokoro voices list
117+ const kokoroVoices = [
118+ { id: 'af_alloy', name: 'Alloy (Female)', lang: 'en-US' },
119+ { id: 'af_aoede', name: 'Aoede (Female)', lang: 'en-US' },
120+ { id: 'af_bella', name: 'Bella (Female)', lang: 'en-US' },
121+ { id: 'af_heart', name: 'Heart (Female) - Default', lang: 'en-US' },
122+ { id: 'af_jessica', name: 'Jessica (Female)', lang: 'en-US' },
123+ { id: 'af_kore', name: 'Kore (Female)', lang: 'en-US' },
124+ { id: 'af_nicole', name: 'Nicole (Female)', lang: 'en-US' },
125+ { id: 'af_nova', name: 'Nova (Female)', lang: 'en-US' },
126+ { id: 'af_river', name: 'River (Female)', lang: 'en-US' },
127+ { id: 'af_sarah', name: 'Sarah (Female)', lang: 'en-US' },
128+ { id: 'af_sky', name: 'Sky (Female)', lang: 'en-US' },
129+ { id: 'am_adam', name: 'Adam (Male)', lang: 'en-US' },
130+ { id: 'am_echo', name: 'Echo (Male)', lang: 'en-US' },
131+ { id: 'am_eric', name: 'Eric (Male)', lang: 'en-US' },
132+ { id: 'am_fenrir', name: 'Fenrir (Male)', lang: 'en-US' },
133+ { id: 'am_liam', name: 'Liam (Male)', lang: 'en-US' },
134+ { id: 'am_michael', name: 'Michael (Male)', lang: 'en-US' },
135+ { id: 'am_onyx', name: 'Onyx (Male)', lang: 'en-US' },
136+ { id: 'am_puck', name: 'Puck (Male)', lang: 'en-US' },
137+ { id: 'am_santa', name: 'Santa (Male)', lang: 'en-US' },
138+ { id: 'bf_alice', name: 'Alice (British Female)', lang: 'en-GB' },
139+ { id: 'bf_emma', name: 'Emma (British Female)', lang: 'en-GB' },
140+ { id: 'bf_isabella', name: 'Isabella (British Female)', lang: 'en-GB' },
141+ { id: 'bf_lily', name: 'Lily (British Female)', lang: 'en-GB' },
142+ { id: 'bm_daniel', name: 'Daniel (British Male)', lang: 'en-GB' },
143+ { id: 'bm_fable', name: 'Fable (British Male)', lang: 'en-GB' },
144+ { id: 'bm_george', name: 'George (British Male)', lang: 'en-GB' },
145+ { id: 'bm_lewis', name: 'Lewis (British Male)', lang: 'en-GB' },
146+ { id: 'ef_dora', name: 'Dora (European Female)', lang: 'es-ES' },
147+ { id: 'em_alex', name: 'Alex (European Male)', lang: 'es-ES' },
148+ { id: 'em_santa', name: 'Santa (European Male)', lang: 'es-ES' },
149+ { id: 'ff_siwis', name: 'Siwis (French Female)', lang: 'fr-FR' },
150+ { id: 'hf_alpha', name: 'Alpha (Hindi Female)', lang: 'hi-IN' },
151+ { id: 'hf_beta', name: 'Beta (Hindi Female)', lang: 'hi-IN' },
152+ { id: 'hm_omega', name: 'Omega (Hindi Male)', lang: 'hi-IN' },
153+ { id: 'hm_psi', name: 'Psi (Hindi Male)', lang: 'hi-IN' },
154+ { id: 'if_sara', name: 'Sara (Italian Female)', lang: 'it-IT' },
155+ { id: 'im_nicola', name: 'Nicola (Italian Male)', lang: 'it-IT' },
156+ { id: 'jf_alpha', name: 'Alpha (Japanese Female)', lang: 'ja-JP' },
157+ { id: 'jf_gongitsune', name: 'Gongitsune (Japanese Female)', lang: 'ja-JP' },
158+ { id: 'jf_nezumi', name: 'Nezumi (Japanese Female)', lang: 'ja-JP' },
159+ { id: 'jf_tebukuro', name: 'Tebukuro (Japanese Female)', lang: 'ja-JP' },
160+ { id: 'jm_kumo', name: 'Kumo (Japanese Male)', lang: 'ja-JP' },
161+ { id: 'pf_dora', name: 'Dora (Portuguese Female)', lang: 'pt-PT' },
162+ { id: 'pm_alex', name: 'Alex (Portuguese Male)', lang: 'pt-PT' },
163+ { id: 'pm_santa', name: 'Santa (Portuguese Male)', lang: 'pt-PT' },
164+ { id: 'zf_xiaobei', name: 'Xiaobei (Chinese Female)', lang: 'zh-CN' },
165+ { id: 'zf_xiaoni', name: 'Xiaoni (Chinese Female)', lang: 'zh-CN' },
166+ { id: 'zf_xiaoxiao', name: 'Xiaoxiao (Chinese Female)', lang: 'zh-CN' },
167+ { id: 'zf_xiaoyi', name: 'Xiaoyi (Chinese Female)', lang: 'zh-CN' },
168+ { id: 'zm_yunjian', name: 'Yunjian (Chinese Male)', lang: 'zh-CN' },
169+ { id: 'zm_yunxi', name: 'Yunxi (Chinese Male)', lang: 'zh-CN' },
170+ { id: 'zm_yunxia', name: 'Yunxia (Chinese Male)', lang: 'zh-CN' },
171+ { id: 'zm_yunyang', name: 'Yunyang (Chinese Male)', lang: 'zh-CN' },
172+ ];
173+
174+ this.voices = kokoroVoices.map(v => ({
175+ name: v.name,
176+ voice_id: v.id,
177+ lang: v.lang,
178+ }));
179+ }
180+
181+ async getVoice(voiceName) {
182+ if (this.voices.length === 0) {
183+ await this.updateVoices();
184+ }
185+ const voice = this.voices.find(v => v.name === voiceName || v.voice_id === voiceName);
186+ return voice || this.voices.find(v => v.voice_id === 'af_heart');
187+ }
188+
189+ async generateTts(text, voiceId) {
190+ const response = await this.fetchTtsGeneration(text, voiceId);
191+ return response;
192+ }
193+
194+ async fetchTtsGeneration(text, voiceId) {
195+ const apiKey = secret_state[SECRET_KEYS.CHUTES];
196+
197+ if (!apiKey) {
198+ throw new Error('No Chutes API key found');
199+ }
200+
201+ const response = await fetch('/api/openai/chutes/generate-voice', {
202+ method: 'POST',
203+ headers: getRequestHeaders(),
204+ body: JSON.stringify({
205+ input: text,
206+ voice: voiceId || 'af_heart',
207+ speed: this.settings.speed || 1,
208+ }),
209+ });
210+
211+ if (!response.ok) {
212+ const error = await response.text();
213+ throw new Error(`Chutes TTS failed: ${error}`);
214+ }
215+
216+ return response;
217+ }
218+
219+ async fetchTtsVoiceObjects() {
220+ if (this.voices.length === 0) {
221+ await this.updateVoices();
222+ }
223+
224+ const voiceIds = this.voices
225+ .map(voice => ({ name: voice.name, voice_id: voice.voice_id, preview_url: false }));
226+ return voiceIds;
227+ }
228+
229+ async previewTtsVoice(voiceId) {
230+ const text = getPreviewString(voiceId);
231+ await this.generateTts(text, voiceId);
232+ }
233+}
public/scripts/extensions/tts/index.js+3 -3
@@ -34,6 +34,7 @@ import { TtsWebuiProvider } from './tts-webui.js';
3434import { PollinationsTtsProvider } from './pollinations.js';
3535import { MiniMaxTtsProvider } from './minimax.js';
3636import { ElectronHubTtsProvider } from './electronhub.js';
37+import { ChutesTtsProvider } from './chutes.js';
3738
3839const UPDATE_INTERVAL = 1000;
3940const wrapper = new ModuleWorkerWrapper(moduleWorker);
@@ -120,6 +121,7 @@ const ttsProviders = {
120121 AllTalk: AllTalkTtsProvider,
121122 Azure: AzureTtsProvider,
122123 Chatterbox: ChatterboxTtsProvider,
124+ Chutes: ChutesTtsProvider,
123125 Coqui: CoquiTtsProvider,
124126 'CosyVoice (Unofficial)': CosyVoiceProvider,
125127 Edge: EdgeTtsProvider,
@@ -239,9 +241,7 @@ function isTtsProcessing() {
239241
240242/**
241243 * Splits a message into lines and adds each non-empty line to the TTS job queue.
242244 * @param {ObjectChatMessage} message - The message object to be processed.
243- * @param {string} message.mes - The text of the message to be split into lines.
244- * @param {string} message.name - The name associated with the message.
245245 * @returns {void}
246246 */
247247function processAndQueueTtsMessage(message) {
public/scripts/extensions/vectors/index.js+48 -0
@@ -69,6 +69,7 @@ const settings = {
6969 vllm_model: '',
7070 webllm_model: '',
7171 google_model: 'text-embedding-005',
72+ chutes_model: 'chutes-qwen-qwen3-embedding-8b',
7273 summarize: false,
7374 summarize_sent: false,
7475 summary_source: 'main',
@@ -829,6 +830,9 @@ function getVectorsRequestBody(args = {}) {
829830 body.vertexai_region = oai_settings.vertexai_region;
830831 body.vertexai_express_project_id = oai_settings.vertexai_express_project_id;
831832 break;
833+ case 'chutes':
834+ body.model = extension_settings.vectors.chutes_model;
835+ break;
832836 default:
833837 break;
834838 }
@@ -913,6 +917,7 @@ async function insertVectorItems(collectionId, items) {
913917function throwIfSourceInvalid() {
914918 if (settings.source === 'openai' && !secret_state[SECRET_KEYS.OPENAI] ||
915919 settings.source === 'electronhub' && !secret_state[SECRET_KEYS.ELECTRONHUB] ||
920+ settings.source === 'chutes' && !secret_state[SECRET_KEYS.CHUTES] ||
916921 settings.source === 'openrouter' && !secret_state[SECRET_KEYS.OPENROUTER] ||
917922 settings.source === 'palm' && !secret_state[SECRET_KEYS.MAKERSUITE] ||
918923 settings.source === 'vertexai' && !secret_state[SECRET_KEYS.VERTEXAI] && !secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT] ||
@@ -1127,6 +1132,7 @@ function toggleSettings() {
11271132 $('#together_vectorsModel').toggle(settings.source === 'togetherai');
11281133 $('#openai_vectorsModel').toggle(settings.source === 'openai');
11291134 $('#electronhub_vectorsModel').toggle(settings.source === 'electronhub');
1135+ $('#chutes_vectorsModel').toggle(settings.source === 'chutes');
11301136 $('#openrouter_vectorsModel').toggle(settings.source === 'openrouter');
11311137 $('#cohere_vectorsModel').toggle(settings.source === 'cohere');
11321138 $('#ollama_vectorsModel').toggle(settings.source === 'ollama');
@@ -1147,9 +1153,46 @@ function toggleSettings() {
11471153 case 'openrouter':
11481154 loadOpenRouterModels();
11491155 break;
1156+ case 'chutes':
1157+ loadChutesModels();
1158+ break;
11501159 }
11511160}
11521161
1162+async function loadChutesModels() {
1163+ try {
1164+ const response = await fetch('/api/openai/chutes/models/embedding', {
1165+ method: 'POST',
1166+ headers: getRequestHeaders(),
1167+ });
1168+ if (!response.ok) {
1169+ throw new Error(`HTTP ${response.status}`);
1170+ }
1171+ /** @type {Array<any>} */
1172+ const data = await response.json();
1173+ const models = Array.isArray(data) ? data : [];
1174+ populateChutesModelSelect(models);
1175+ } catch (err) {
1176+ console.warn('Chutes models fetch failed', err);
1177+ populateChutesModelSelect([]);
1178+ }
1179+}
1180+
1181+function populateChutesModelSelect(models) {
1182+ const select = $('#vectors_chutes_model');
1183+ select.empty();
1184+ for (const m of models) {
1185+ const option = document.createElement('option');
1186+ option.value = m.slug;
1187+ option.text = m.name;
1188+ select.append(option);
1189+ }
1190+ if (!settings.chutes_model && models.length) {
1191+ settings.chutes_model = models[0].slug;
1192+ }
1193+ $('#vectors_chutes_model').val(settings.chutes_model);
1194+}
1195+
11531196async function loadElectronHubModels() {
11541197 try {
11551198 const response = await fetch('/api/openai/electronhub/models', {
@@ -1630,6 +1673,11 @@ jQuery(async () => {
16301673 Object.assign(extension_settings.vectors, settings);
16311674 saveSettingsDebounced();
16321675 });
1676+ $('#vectors_chutes_model').val(settings.chutes_model).on('change', () => {
1677+ settings.chutes_model = String($('#vectors_chutes_model').val());
1678+ Object.assign(extension_settings.vectors, settings);
1679+ saveSettingsDebounced();
1680+ });
16331681 $('#vectors_openrouter_model').val(settings.openrouter_model).on('change', () => {
16341682 settings.openrouter_model = String($('#vectors_openrouter_model').val());
16351683 Object.assign(extension_settings.vectors, settings);
public/scripts/extensions/vectors/settings.html+10 -0
@@ -10,6 +10,7 @@
1010 Vectorization Source
1111 </label>
1212 <select id="vectors_source" class="text_pole">
13+ <option value="chutes">Chutes</option>
1314 <option value="cohere">Cohere</option>
1415 <option value="electronhub">Electron Hub</option>
1516 <option value="extras">Extras (deprecated)</option>
@@ -28,6 +29,15 @@
2829 <option value="webllm" data-i18n="WebLLM Extension">WebLLM Extension</option>
2930 </select>
3031 </div>
32+ <div class="flex-container flexFlowColumn" id="chutes_vectorsModel">
33+ <label for="vectors_chutes_model" data-i18n="Vectorization Model">
34+ Vectorization Model
35+ </label>
36+ <select id="vectors_chutes_model" class="text_pole"></select>
37+ <i data-i18n="Hint: Set your Chutes API key in API Connections.">
38+ Hint: Set your Chutes API key in API Connections.
39+ </i>
40+ </div>
3141 <div class="flex-container flexFlowColumn" id="electronhub_vectorsModel">
3242 <label for="vectors_electronhub_model" data-i18n="Vectorization Model">
3343 Vectorization Model
public/scripts/openai.js+193 -3
@@ -184,6 +184,7 @@ export const chat_completion_sources = {
184184 PERPLEXITY: 'perplexity',
185185 GROQ: 'groq',
186186 ELECTRONHUB: 'electronhub',
187+ CHUTES: 'chutes',
187188 NANOGPT: 'nanogpt',
188189 DEEPSEEK: 'deepseek',
189190 AIMLAPI: 'aimlapi',
@@ -293,6 +294,8 @@ export const settingsToUpdate = {
293294 cohere_model: ['#model_cohere_select', 'cohere_model', false, true],
294295 perplexity_model: ['#model_perplexity_select', 'perplexity_model', false, true],
295296 groq_model: ['#model_groq_select', 'groq_model', false, true],
297+ chutes_model: ['#model_chutes_select', 'chutes_model', false, true],
298+ chutes_sort_models: ['#chutes_sort_models', 'chutes_sort_models', false, true],
296299 siliconflow_model: ['#model_siliconflow_select', 'siliconflow_model', false, true],
297300 electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true],
298301 electronhub_sort_models: ['#electronhub_sort_models', 'electronhub_sort_models', false, true],
@@ -400,6 +403,8 @@ const default_settings = {
400403 cohere_model: 'command-r-plus',
401404 perplexity_model: 'sonar-pro',
402405 groq_model: 'llama-3.3-70b-versatile',
406+ chutes_model: 'deepseek-ai/DeepSeek-V3-0324',
407+ chutes_sort_models: 'alphabetically',
403408 siliconflow_model: 'deepseek-ai/DeepSeek-V3',
404409 electronhub_model: 'gpt-4o-mini',
405410 electronhub_sort_models: 'alphabetically',
@@ -1592,6 +1597,8 @@ export function getChatCompletionModel(source = null) {
15921597 return oai_settings.siliconflow_model;
15931598 case chat_completion_sources.ELECTRONHUB:
15941599 return oai_settings.electronhub_model;
1600+ case chat_completion_sources.CHUTES:
1601+ return oai_settings.chutes_model;
15951602 case chat_completion_sources.NANOGPT:
15961603 return oai_settings.nanogpt_model;
15971604 case chat_completion_sources.DEEPSEEK:
@@ -1717,6 +1724,72 @@ function calculateElectronHubCost() {
17171724 $('#electronhub_max_prompt_cost').text(cost);
17181725}
17191726
1727+function getChutesModelTemplate(option) {
1728+ const model = model_list.find(x => x.id === option?.element?.value);
1729+
1730+ if (!option.id || !model) {
1731+ return option.text;
1732+ }
1733+
1734+ const inputPrice = model.pricing?.input;
1735+ const outputPrice = model.pricing?.output;
1736+
1737+ let price = 'Unknown';
1738+ if (inputPrice !== undefined && outputPrice !== undefined) {
1739+ // Check if both prices are 0 (free model)
1740+ if (inputPrice === 0 && outputPrice === 0) {
1741+ price = 'Free';
1742+ } else {
1743+ price = `$${inputPrice}/$${outputPrice} in/out Mtoken`;
1744+ }
1745+ }
1746+
1747+ const contextLength = model.context_length || model.max_model_len || 'Unknown';
1748+ const visionIcon = model.input_modalities?.includes('image') ? '<i class="fa-solid fa-eye fa-sm" title="This model supports vision"></i>' : '';
1749+ const reasoningIcon = model.supported_features?.includes('reasoning') ? '<i class="fa-solid fa-brain fa-sm" title="This model supports reasoning"></i>' : '';
1750+ const toolCallsIcon = model.supported_features?.includes('structured_outputs') ? '<i class="fa-solid fa-wrench fa-sm" title="This model supports function tools"></i>' : '';
1751+
1752+ const iconsContainer = document.createElement('span');
1753+ iconsContainer.insertAdjacentHTML('beforeend', visionIcon);
1754+ iconsContainer.insertAdjacentHTML('beforeend', reasoningIcon);
1755+ iconsContainer.insertAdjacentHTML('beforeend', toolCallsIcon);
1756+
1757+ const capabilities = (iconsContainer.children.length) ? ` | ${iconsContainer.innerHTML}` : '';
1758+
1759+ return $((`
1760+ <div class="flex-container alignItemsBaseline" title="${DOMPurify.sanitize(model.id)}">
1761+ <strong>${DOMPurify.sanitize(model.id)}</strong> | ${contextLength} ctx | <small>${price}</small>${capabilities}
1762+ </div>
1763+ `));
1764+}
1765+
1766+function calculateChutesCost() {
1767+ if (oai_settings.chat_completion_source !== chat_completion_sources.CHUTES) {
1768+ return;
1769+ }
1770+
1771+ let cost = 'Unknown';
1772+ const model = model_list.find(x => x.id === oai_settings.chutes_model);
1773+
1774+ if (model?.pricing) {
1775+ const outputPrice = model.pricing?.output;
1776+ const inputPrice = model.pricing?.input;
1777+
1778+ if (outputPrice !== undefined && inputPrice !== undefined) {
1779+ const outputCost = Number(outputPrice / 1000000);
1780+ const inputCost = Number(inputPrice / 1000000);
1781+ const outputTokens = oai_settings.openai_max_tokens;
1782+ const inputTokens = (oai_settings.openai_max_context - outputTokens);
1783+ const totalCost = (outputCost * outputTokens) + (inputCost * inputTokens);
1784+ if (!isNaN(totalCost)) {
1785+ cost = '$' + totalCost.toFixed(4);
1786+ }
1787+ }
1788+ }
1789+
1790+ $('#chutes_max_prompt_cost').text(cost);
1791+}
1792+
17201793function saveModelList(data) {
17211794 model_list = data.map((model) => ({ ...model }));
17221795 model_list.sort((a, b) => a?.id && b?.id && a.id.localeCompare(b.id));
@@ -1814,6 +1887,27 @@ function saveModelList(data) {
18141887 $('#model_electronhub_select').val(oai_settings.electronhub_model).trigger('change');
18151888 }
18161889
1890+ if (oai_settings.chat_completion_source == chat_completion_sources.CHUTES) {
1891+ model_list = model_list.filter(model => !model.id.toLowerCase().includes('affine'));
1892+
1893+ model_list = chutesSortBy(model_list, oai_settings.chutes_sort_models);
1894+
1895+ $('#model_chutes_select').empty();
1896+
1897+ for (const model of model_list) {
1898+ const option = $('<option>').val(model.id).text(model.id);
1899+ option.attr('data-model', JSON.stringify(model));
1900+ $('#model_chutes_select').append(option);
1901+ }
1902+
1903+ const selectedModel = model_list.find(model => model.id === oai_settings.chutes_model);
1904+ if (model_list.length > 0 && (!selectedModel || !oai_settings.chutes_model)) {
1905+ oai_settings.chutes_model = model_list[0].id;
1906+ }
1907+
1908+ $('#model_chutes_select').val(oai_settings.chutes_model).trigger('change');
1909+ }
1910+
18171911 if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) {
18181912 $('#model_nanogpt_select').empty();
18191913 model_list.forEach((model) => {
@@ -2083,6 +2177,24 @@ function openRouterGroupByVendor(array) {
20832177 }, new Map());
20842178}
20852179
2180+function chutesSortBy(data, property = 'alphabetically') {
2181+ return data.sort((a, b) => {
2182+ if (property === 'context_length') {
2183+ return b.context_length - a.context_length;
2184+ } else if (property === 'pricing.input') {
2185+ const aPrice = parseFloat(a.pricing?.input || 0);
2186+ const bPrice = parseFloat(b.pricing?.input || 0);
2187+ return aPrice - bPrice;
2188+ } else if (property === 'pricing.output') {
2189+ const aPrice = parseFloat(a.pricing?.output || 0);
2190+ const bPrice = parseFloat(b.pricing?.output || 0);
2191+ return aPrice - bPrice;
2192+ } else {
2193+ return a?.id && b?.id && a.id.localeCompare(b.id);
2194+ }
2195+ });
2196+}
2197+
20862198function appendElectronHubOptions(model_list, groupModels = false) {
20872199 const appendOption = (model, parent = null) => {
20882200 (parent || $('#model_electronhub_select')).append(
@@ -2201,6 +2313,7 @@ function getReasoningEffort() {
22012313 chat_completion_sources.PERPLEXITY,
22022314 chat_completion_sources.COMETAPI,
22032315 chat_completion_sources.ELECTRONHUB,
2316+ chat_completion_sources.CHUTES,
22042317 ];
22052318
22062319 if (!reasoningEffortSources.includes(oai_settings.chat_completion_source)) {
@@ -2285,6 +2398,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
22852398 const isDeepSeek = oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK;
22862399 const isAimlapi = oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI;
22872400 const isElectronHub = oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB;
2401+ const isChutes = oai_settings.chat_completion_source == chat_completion_sources.CHUTES;
22882402 const isXAI = oai_settings.chat_completion_source == chat_completion_sources.XAI;
22892403 const isPollinations = oai_settings.chat_completion_source == chat_completion_sources.POLLINATIONS;
22902404 const isMoonshot = oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT;
@@ -2299,7 +2413,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
22992413 const useLogprobs = !!power_user.request_token_probabilities;
23002414 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isAzureOpenAI || isCustom || isXAI || isAimlapi || isMoonshot);
23012415
23022416 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];
23032417 if (oai_settings.bias_preset_selected
23042418 && logitBiasSources.includes(oai_settings.chat_completion_source)
23052419 && Array.isArray(oai_settings.bias_presets[oai_settings.bias_preset_selected])
@@ -2367,7 +2481,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
23672481 }
23682482
23692483 // Add logprobs request (currently OpenAI only, max 5 on their side)
23702484 if (useLogprobs && (isOAI || isAzureOpenAI || isCustom || isDeepSeek || isXAI || isAimlapi || isChutes)) {
23712485 generate_data['logprobs'] = 5;
23722486 }
23732487
@@ -2494,6 +2608,14 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
24942608 generate_data['top_k'] = Number(oai_settings.top_k_openai);
24952609 }
24962610
2611+ if (isChutes) {
2612+ generate_data['min_p'] = Number(oai_settings.min_p_openai);
2613+ generate_data['top_k'] = oai_settings.top_k_openai > 0 ? Number(oai_settings.top_k_openai) : undefined;
2614+ generate_data['repetition_penalty'] = Number(oai_settings.repetition_penalty_openai);
2615+ generate_data['seed'] = oai_settings.seed >= 0 ? oai_settings.seed : undefined;
2616+ generate_data['stop'] = getCustomStoppingStrings();
2617+ }
2618+
24972619 // https://docs.z.ai/api-reference/llm/chat-completion
24982620 if (isZai) {
24992621 generate_data['top_p'] = generate_data.top_p || 0.01;
@@ -2697,7 +2819,7 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov
26972819 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
26982820 }
26992821 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
27002822 } 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)) {
27012823 if (show_thoughts) {
27022824 state.reasoning +=
27032825 data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??
@@ -2733,6 +2855,7 @@ function parseChatCompletionLogprobs(data) {
27332855 case chat_completion_sources.DEEPSEEK:
27342856 case chat_completion_sources.XAI:
27352857 case chat_completion_sources.CUSTOM:
2858+ case chat_completion_sources.CHUTES:
27362859 if (!data.choices?.length) {
27372860 return null;
27382861 }
@@ -4659,6 +4782,26 @@ function getFireworksMaxContext(model, isUnlocked) {
46594782}
46604783
46614784/**
4785+ * Get the maximum context size for the Chutes model
4786+ * @param {string} model Model identifier
4787+ * @param {boolean} isUnlocked Whether context limits are unlocked
4788+ * @returns {number} Maximum context size in tokens
4789+ */
4790+function getChutesMaxContext(model, isUnlocked) {
4791+ if (isUnlocked) {
4792+ return unlocked_max;
4793+ }
4794+
4795+ if (Array.isArray(model_list)) {
4796+ const modelInfo = model_list.find(m => m.id === model);
4797+ if (modelInfo?.context_length) {
4798+ return modelInfo.context_length;
4799+ }
4800+ }
4801+ return max_8k;
4802+}
4803+
4804+/**
46624805 * Get the maximum context size for the ElectronHub model
46634806 * @param {string} model Model identifier
46644807 * @param {boolean} isUnlocked Whether context limits are unlocked
@@ -4805,6 +4948,15 @@ async function onModelChange() {
48054948 oai_settings.electronhub_model = value;
48064949 }
48074950
4951+ if ($(this).is('#model_chutes_select')) {
4952+ if (!value || !hasModelsLoaded) {
4953+ console.debug('Null Chutes model selected. Ignoring.');
4954+ return;
4955+ }
4956+ console.log('Chutes model changed to', value);
4957+ oai_settings.chutes_model = value;
4958+ }
4959+
48084960 if ($(this).is('#model_nanogpt_select')) {
48094961 if (!value || !hasModelsLoaded) {
48104962 console.debug('Null NanoGPT model selected. Ignoring.');
@@ -5070,6 +5222,17 @@ async function onModelChange() {
50705222 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
50715223 }
50725224
5225+ if (oai_settings.chat_completion_source == chat_completion_sources.CHUTES) {
5226+ const maxContext = getChutesMaxContext(oai_settings.chutes_model, oai_settings.max_context_unlocked);
5227+ $('#openai_max_context').attr('max', maxContext);
5228+ oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
5229+ $('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
5230+ oai_settings.temp_openai = Math.min(oai_max_temp, oai_settings.temp_openai);
5231+ $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
5232+
5233+ calculateChutesCost();
5234+ }
5235+
50735236 if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
50745237 const maxContext = getElectronHubMaxContext(oai_settings.electronhub_model, oai_settings.max_context_unlocked);
50755238 $('#openai_max_context').attr('max', maxContext);
@@ -5227,6 +5390,10 @@ async function onOpenrouterModelSortChange() {
52275390 await getStatusOpen();
52285391}
52295392
5393+async function onChutesModelSortChange() {
5394+ await getStatusOpen();
5395+}
5396+
52305397async function onElectronHubModelSortChange() {
52315398 await getStatusOpen();
52325399}
@@ -5273,6 +5440,7 @@ async function onConnectButtonClick(e) {
52735440 [chat_completion_sources.COMETAPI]: { key: SECRET_KEYS.COMETAPI, selector: '#api_key_cometapi', proxy: false },
52745441 [chat_completion_sources.AZURE_OPENAI]: { key: SECRET_KEYS.AZURE_OPENAI, selector: '#api_key_azure_openai', proxy: false },
52755442 [chat_completion_sources.ZAI]: { key: SECRET_KEYS.ZAI, selector: '#api_key_zai', proxy: false },
5443+ [chat_completion_sources.CHUTES]: { key: SECRET_KEYS.CHUTES, selector: '#api_key_chutes', proxy: false },
52765444 };
52775445
52785446 // Vertex AI Express version - use API key
@@ -5345,6 +5513,9 @@ function toggleChatCompletionForms() {
53455513 else if (oai_settings.chat_completion_source == chat_completion_sources.GROQ) {
53465514 $('#model_groq_select').trigger('change');
53475515 }
5516+ else if (oai_settings.chat_completion_source == chat_completion_sources.CHUTES) {
5517+ $('#model_chutes_select').trigger('change');
5518+ }
53485519 else if (oai_settings.chat_completion_source == chat_completion_sources.SILICONFLOW) {
53495520 $('#model_siliconflow_select').trigger('change');
53505521 }
@@ -5542,6 +5713,8 @@ export function isImageInliningSupported() {
55425713 return visionSupportedModels.some(model => oai_settings.xai_model.includes(model));
55435714 case chat_completion_sources.AIMLAPI:
55445715 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.aimlapi_model)?.features?.includes('openai/chat-completion.vision'));
5716+ case chat_completion_sources.CHUTES:
5717+ return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.chutes_model)?.input_modalities?.includes('image'));
55455718 case chat_completion_sources.ELECTRONHUB:
55465719 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.electronhub_model)?.metadata?.vision);
55475720 case chat_completion_sources.POLLINATIONS:
@@ -5972,6 +6145,7 @@ export function initOpenAI() {
59726145 $('#openai_max_context_counter').val(`${$(this).val()}`);
59736146 calculateOpenRouterCost();
59746147 calculateElectronHubCost();
6148+ calculateChutesCost();
59756149 saveSettingsDebounced();
59766150 });
59776151
@@ -5979,6 +6153,7 @@ export function initOpenAI() {
59796153 oai_settings.openai_max_tokens = Number($(this).val());
59806154 calculateOpenRouterCost();
59816155 calculateElectronHubCost();
6156+ calculateChutesCost();
59826157 saveSettingsDebounced();
59836158 });
59846159
@@ -6178,6 +6353,11 @@ export function initOpenAI() {
61786353 saveSettingsDebounced();
61796354 });
61806355
6356+ $('#chutes_sort_models').on('input', function () {
6357+ oai_settings.chutes_sort_models = String($(this).val());
6358+ saveSettingsDebounced();
6359+ });
6360+
61816361 $('#electronhub_group_models').on('input', function () {
61826362 oai_settings.electronhub_group_models = !!$(this).prop('checked');
61836363 saveSettingsDebounced();
@@ -6377,6 +6557,14 @@ export function initOpenAI() {
63776557 templateResult: getElectronHubModelTemplate,
63786558 matcher: textValueMatcher,
63796559 });
6560+ $('#model_chutes_select').select2({
6561+ placeholder: t`Select a model`,
6562+ searchInputPlaceholder: t`Search models...`,
6563+ searchInputCssClass: 'text_pole',
6564+ width: '100%',
6565+ templateResult: getChutesModelTemplate,
6566+ matcher: textValueMatcher,
6567+ });
63806568 $('#completion_prompt_manager_popup_entry_form_injection_trigger').select2({
63816569 placeholder: t`All types (default)`,
63826570 width: '100%',
@@ -6427,6 +6615,7 @@ export function initOpenAI() {
64276615 $('#model_openrouter_select').on('change', onModelChange);
64286616 $('#openrouter_group_models').on('change', onOpenrouterModelSortChange);
64296617 $('#openrouter_sort_models').on('change', onOpenrouterModelSortChange);
6618+ $('#chutes_sort_models').on('change', onChutesModelSortChange);
64306619 $('#electronhub_group_models').on('change', onElectronHubModelSortChange);
64316620 $('#electronhub_sort_models').on('change', onElectronHubModelSortChange);
64326621 $('#model_ai21_select').on('change', onModelChange);
@@ -6434,6 +6623,7 @@ export function initOpenAI() {
64346623 $('#model_cohere_select').on('change', onModelChange);
64356624 $('#model_perplexity_select').on('change', onModelChange);
64366625 $('#model_groq_select').on('change', onModelChange);
6626+ $('#model_chutes_select').on('change', onModelChange);
64376627 $('#model_siliconflow_select').on('change', onModelChange);
64386628 $('#model_electronhub_select').on('change', onModelChange);
64396629 $('#model_nanogpt_select').on('change', onModelChange);
public/scripts/reasoning.js+1 -0
@@ -125,6 +125,7 @@ export function extractReasoningFromData(data, {
125125 case chat_completion_sources.POLLINATIONS:
126126 case chat_completion_sources.MOONSHOT:
127127 case chat_completion_sources.COMETAPI:
128+ case chat_completion_sources.CHUTES:
128129 case chat_completion_sources.ELECTRONHUB:
129130 case chat_completion_sources.NANOGPT:
130131 case chat_completion_sources.SILICONFLOW:
public/scripts/secrets.js+3 -0
@@ -52,6 +52,7 @@ export const SECRET_KEYS = {
5252 HUGGINGFACE: 'api_key_huggingface',
5353 STABILITY: 'api_key_stability',
5454 CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
55+ CHUTES: 'api_key_chutes',
5556 ELECTRONHUB: 'api_key_electronhub',
5657 NANOGPT: 'api_key_nanogpt',
5758 TAVILY: 'api_key_tavily',
@@ -99,6 +100,7 @@ const FRIENDLY_NAMES = {
99100 [SECRET_KEYS.GROQ]: 'Groq',
100101 [SECRET_KEYS.FEATHERLESS]: 'Featherless',
101102 [SECRET_KEYS.HUGGINGFACE]: 'HuggingFace',
103+ [SECRET_KEYS.CHUTES]: 'Chutes',
102104 [SECRET_KEYS.ELECTRONHUB]: 'Electron Hub',
103105 [SECRET_KEYS.NANOGPT]: 'NanoGPT',
104106 [SECRET_KEYS.GENERIC]: 'Generic (OpenAI-compatible)',
@@ -156,6 +158,7 @@ const INPUT_MAP = {
156158 [SECRET_KEYS.GROQ]: '#api_key_groq',
157159 [SECRET_KEYS.FEATHERLESS]: '#api_key_featherless',
158160 [SECRET_KEYS.HUGGINGFACE]: '#api_key_huggingface',
161+ [SECRET_KEYS.CHUTES]: '#api_key_chutes',
159162 [SECRET_KEYS.ELECTRONHUB]: '#api_key_electronhub',
160163 [SECRET_KEYS.NANOGPT]: '#api_key_nanogpt',
161164 [SECRET_KEYS.GENERIC]: '#api_key_generic',
public/scripts/slash-commands.js+1 -0
@@ -4846,6 +4846,7 @@ function getModelOptions(quiet) {
48464846 { id: 'model_cohere_select', api: 'openai', type: chat_completion_sources.COHERE },
48474847 { id: 'model_perplexity_select', api: 'openai', type: chat_completion_sources.PERPLEXITY },
48484848 { id: 'model_groq_select', api: 'openai', type: chat_completion_sources.GROQ },
4849+ { id: 'model_chutes_select', api: 'openai', type: chat_completion_sources.CHUTES },
48494850 { id: 'model_siliconflow_select', api: 'openai', type: chat_completion_sources.SILICONFLOW },
48504851 { id: 'model_electronhub_select', api: 'openai', type: chat_completion_sources.ELECTRONHUB },
48514852 { id: 'model_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT },
public/scripts/st-context.js+4 -0
@@ -63,6 +63,8 @@ import {
6363 ensureMessageMediaIsArray,
6464 getMediaDisplay,
6565 getMediaIndex,
66+ scrollChatToBottom,
67+ scrollOnMediaLoad,
6668} from '../script.js';
6769import {
6870 extension_settings,
@@ -217,6 +219,8 @@ export function getContext() {
217219 ensureMessageMediaIsArray,
218220 getMediaDisplay,
219221 getMediaIndex,
222+ scrollChatToBottom,
223+ scrollOnMediaLoad,
220224 swipe: {
221225 left: swipe_left,
222226 right: swipe_right,
public/scripts/tokenizers.js+21 -0
@@ -699,6 +699,26 @@ export function getTokenizerModel() {
699699 }
700700 }
701701
702+ if (oai_settings.chat_completion_source == chat_completion_sources.CHUTES && oai_settings.chutes_model) {
703+ const model = oai_settings.chutes_model.toLowerCase();
704+
705+ if (model.includes('deepseek') || model.includes('mai-ds')) {
706+ return deepseekTokenizer;
707+ } else if (model.includes('qwen') || model.includes('qwq') || model.includes('tongyi') || model.includes('kimi')) {
708+ return qwen2Tokenizer;
709+ } else if (model.includes('llama') || model.includes('longcat') || model.includes('hermes')) {
710+ return llama3Tokenizer;
711+ } else if (model.includes('gemma')) {
712+ return gemmaTokenizer;
713+ } else if (model.includes('nemo')) {
714+ return nemoTokenizer;
715+ } else if (model.includes('mistral')) {
716+ return mistralTokenizer;
717+ } else if (model.includes('gpt-oss')) {
718+ return gpt4oTokenizer;
719+ }
720+ }
721+
702722 if (oai_settings.chat_completion_source == chat_completion_sources.COHERE) {
703723 if (oai_settings.cohere_model.includes('command-a')) {
704724 return commandATokenizer;
@@ -1206,3 +1226,4 @@ export async function initTokenizers() {
12061226 await loadTokenCache();
12071227 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);
12081228}
1229+
public/scripts/tool-calling.js+14 -2
@@ -559,6 +559,10 @@ export class ToolManager {
559559 const targetValue = target[key];
560560
561561 if (deltaValue === null || deltaValue === undefined) {
562+ // Don't reset the value if it already exists
563+ if (targetValue) {
564+ continue;
565+ }
562566 target[key] = deltaValue;
563567 continue;
564568 }
@@ -634,10 +638,17 @@ export class ToolManager {
634638 }
635639 }
636640
641+ if (oai_settings.chat_completion_source === chat_completion_sources.CHUTES && Array.isArray(model_list)) {
642+ const currentModel = model_list.find(model => model.id === oai_settings.chutes_model);
643+ if (currentModel) {
644+ return currentModel.supported_features?.includes('tools');
645+ }
646+ }
647+
637648 if (oai_settings.chat_completion_source === chat_completion_sources.ELECTRONHUB && Array.isArray(model_list)) {
638649 const currentModel = model_list.find(model => model.id === oai_settings.electronhub_model);
639650 if (currentModel && currentModel.metadata?.function_call) {
640651 return currentModel.metadata?.function_call;
641652 }
642653 }
643654
@@ -659,6 +670,7 @@ export class ToolManager {
659670 chat_completion_sources.MOONSHOT,
660671 chat_completion_sources.FIREWORKS,
661672 chat_completion_sources.COMETAPI,
673+ chat_completion_sources.CHUTES,
662674 chat_completion_sources.ELECTRONHUB,
663675 chat_completion_sources.AZURE_OPENAI,
664676 chat_completion_sources.ZAI,
src/constants.js+1 -0
@@ -196,6 +196,7 @@ export const CHAT_COMPLETION_SOURCES = {
196196 COHERE: 'cohere',
197197 PERPLEXITY: 'perplexity',
198198 GROQ: 'groq',
199+ CHUTES: 'chutes',
199200 ELECTRONHUB: 'electronhub',
200201 NANOGPT: 'nanogpt',
201202 DEEPSEEK: 'deepseek',
src/endpoints/backends/chat-completions.js+156 -2
@@ -68,6 +68,7 @@ const API_GROQ = 'https://api.groq.com/openai/v1';
6868const API_MAKERSUITE = 'https://generativelanguage.googleapis.com';
6969const API_VERTEX_AI = 'https://us-central1-aiplatform.googleapis.com';
7070const API_AI21 = 'https://api.ai21.com/studio/v1';
71+const API_CHUTES = 'https://llm.chutes.ai/v1';
7172const API_ELECTRONHUB = 'https://api.electronhub.ai/v1';
7273const API_NANOGPT = 'https://nano-gpt.com/api/v1';
7374const API_DEEPSEEK = 'https://api.deepseek.com/beta';
@@ -1355,6 +1356,108 @@ async function sendElectronHubRequest(request, response) {
13551356}
13561357
13571358/**
1359+ * Sends a request to Chutes.
1360+ * @param {express.Request} request Express request
1361+ * @param {express.Response} response Express response
1362+ */
1363+async function sendChutesRequest(request, response) {
1364+ const apiUrl = API_CHUTES;
1365+ const apiKey = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
1366+
1367+ if (!apiKey) {
1368+ console.warn('Chutes key is missing.');
1369+ return response.status(400).send({ error: true });
1370+ }
1371+
1372+ const controller = new AbortController();
1373+ request.socket.removeAllListeners('close');
1374+ request.socket.on('close', function () {
1375+ controller.abort();
1376+ });
1377+
1378+ try {
1379+ let bodyParams = {};
1380+
1381+ if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
1382+ bodyParams['tools'] = request.body.tools;
1383+ bodyParams['tool_choice'] = request.body.tool_choice;
1384+ }
1385+
1386+ if (request.body.logprobs > 0) {
1387+ bodyParams['top_logprobs'] = request.body.logprobs;
1388+ bodyParams['logprobs'] = true;
1389+ }
1390+
1391+ if (request.body.json_schema) {
1392+ bodyParams['response_format'] = {
1393+ type: 'json_schema',
1394+ json_schema: {
1395+ name: request.body.json_schema.name,
1396+ description: request.body.json_schema.description,
1397+ schema: request.body.json_schema.value,
1398+ strict: request.body.json_schema.strict ?? true,
1399+ },
1400+ };
1401+ }
1402+
1403+ const requestBody = {
1404+ 'messages': request.body.messages,
1405+ 'model': request.body.model,
1406+ 'temperature': request.body.temperature,
1407+ 'max_tokens': request.body.max_tokens,
1408+ 'stream': request.body.stream,
1409+ 'presence_penalty': request.body.presence_penalty,
1410+ 'frequency_penalty': request.body.frequency_penalty,
1411+ 'repetition_penalty': request.body.repetition_penalty,
1412+ 'min_p': request.body.min_p,
1413+ 'top_p': request.body.top_p,
1414+ 'top_k': request.body.top_k,
1415+ 'seed': request.body.seed,
1416+ 'stop': request.body.stop,
1417+ 'reasoning_effort': request.body.reasoning_effort,
1418+ 'logit_bias': request.body.logit_bias,
1419+ ...bodyParams,
1420+ };
1421+
1422+ const config = {
1423+ method: 'POST',
1424+ headers: {
1425+ 'Content-Type': 'application/json',
1426+ 'Authorization': 'Bearer ' + apiKey,
1427+ },
1428+ body: JSON.stringify(requestBody),
1429+ signal: controller.signal,
1430+ };
1431+
1432+ console.debug('Chutes request:', requestBody);
1433+
1434+ const generateResponse = await fetch(apiUrl + '/chat/completions', config);
1435+
1436+ if (request.body.stream) {
1437+ forwardFetchResponse(generateResponse, response);
1438+ } else {
1439+ if (!generateResponse.ok) {
1440+ const errorText = await generateResponse.text();
1441+ console.warn('Chutes returned error: ', errorText);
1442+ const errorJson = tryParse(errorText) ?? { error: true };
1443+ return response.status(500).send(errorJson);
1444+ }
1445+ const generateResponseJson = await generateResponse.json();
1446+ console.debug('Chutes response:', generateResponseJson);
1447+ return response.send(generateResponseJson);
1448+ }
1449+ }
1450+ catch (error) {
1451+ console.error('Error communicating with Chutes: ', error);
1452+ if (!response.headersSent) {
1453+ response.send({ error: true });
1454+ } else {
1455+ response.end();
1456+ }
1457+ }
1458+}
1459+
1460+/**
13581461 * Sends a chat completion request to Azure OpenAI.
13591462 * @param {express.Request} request Express request object (contains request.body with all generate_data)
13601463 * @param {express.Response} response Express response object
@@ -1480,6 +1583,10 @@ router.post('/status', async function (request, statusResponse) {
14801583 apiUrl = API_COHERE_V1;
14811584 apiKey = readSecret(request.user.directories, SECRET_KEYS.COHERE);
14821585 headers = {};
1586+ } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CHUTES) {
1587+ apiUrl = API_CHUTES;
1588+ apiKey = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
1589+ headers = {};
14831590 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.ELECTRONHUB) {
14841591 apiUrl = API_ELECTRONHUB;
14851592 apiKey = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);
@@ -1671,6 +1778,22 @@ router.post('/status', async function (request, statusResponse) {
16711778 data = { data: data.map(model => ({ id: model.name, ...model })) };
16721779 }
16731780
1781+ if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CHUTES && Array.isArray(data?.data)) {
1782+ data.data = data.data.map(model => {
1783+ if (model.pricing?.prompt !== undefined && model.pricing?.completion !== undefined) {
1784+ return {
1785+ ...model,
1786+ pricing: {
1787+ ...model.pricing,
1788+ input: model.pricing.prompt,
1789+ output: model.pricing.completion,
1790+ },
1791+ };
1792+ }
1793+ return model;
1794+ });
1795+ }
1796+
16741797 statusResponse.send(data);
16751798
16761799 if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.COHERE && Array.isArray(data?.models)) {
@@ -1831,6 +1954,7 @@ router.post('/generate', function (request, response) {
18311954 case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);
18321955 case CHAT_COMPLETION_SOURCES.AIMLAPI: return sendAimlapiRequest(request, response);
18331956 case CHAT_COMPLETION_SOURCES.XAI: return sendXaiRequest(request, response);
1957+ case CHAT_COMPLETION_SOURCES.CHUTES: return sendChutesRequest(request, response);
18341958 case CHAT_COMPLETION_SOURCES.ELECTRONHUB: return sendElectronHubRequest(request, response);
18351959 case CHAT_COMPLETION_SOURCES.AZURE_OPENAI: return sendAzureOpenAIRequest(request, response);
18361960 }
@@ -2017,8 +2141,7 @@ router.post('/generate', function (request, response) {
20172141 'ttl': cacheTTL,
20182142 };
20192143 }
2020- }
2144+ } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {
2021- else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {
20222145 apiUrl = API_POLLINATIONS;
20232146 apiKey = 'NONE';
20242147 headers = {
@@ -2314,6 +2437,37 @@ multimodalModels.post('/electronhub', async (_req, res) => {
23142437 }
23152438});
23162439
2440+multimodalModels.post('/chutes', async (req, res) => {
2441+ try {
2442+ const key = readSecret(req.user.directories, SECRET_KEYS.CHUTES);
2443+
2444+ if (!key) {
2445+ return res.json([]);
2446+ }
2447+
2448+ const response = await fetch('https://llm.chutes.ai/v1/models', {
2449+ headers: {
2450+ 'Authorization': `Bearer ${key}`,
2451+ },
2452+ });
2453+
2454+ if (!response.ok) {
2455+ return res.json([]);
2456+ }
2457+
2458+ const data = await response.json();
2459+
2460+ const modelsData = /** @type {{object: string, data: Array<{id: string, input_modalities?: string[]}>}} */ (data);
2461+ const multimodalModels = modelsData.data
2462+ .filter(m => m.input_modalities?.includes('image'))
2463+ .map(m => m.id);
2464+ return res.json(multimodalModels);
2465+ } catch (error) {
2466+ console.error(error);
2467+ return res.sendStatus(500);
2468+ }
2469+});
2470+
23172471multimodalModels.post('/mistral', async (req, res) => {
23182472 try {
23192473 const key = readSecret(req.user.directories, SECRET_KEYS.MISTRALAI);
src/endpoints/openai.js+86 -0
@@ -81,6 +81,10 @@ router.post('/caption-image', async (request, response) => {
8181 key = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
8282 }
8383
84+ if (request.body.api === 'chutes') {
85+ key = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
86+ }
87+
8488 if (request.body.api === 'electronhub') {
8589 key = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);
8690 }
@@ -177,6 +181,10 @@ router.post('/caption-image', async (request, response) => {
177181 apiUrl = 'https://nano-gpt.com/api/v1/chat/completions';
178182 }
179183
184+ if (request.body.api === 'chutes') {
185+ apiUrl = 'https://llm.chutes.ai/v1/chat/completions';
186+ }
187+
180188 if (request.body.api === 'electronhub') {
181189 apiUrl = 'https://api.electronhub.ai/v1/chat/completions';
182190 }
@@ -431,6 +439,84 @@ router.post('/electronhub/models', async (request, response) => {
431439 }
432440});
433441
442+// Chutes TTS
443+router.post('/chutes/generate-voice', async (request, response) => {
444+ try {
445+ const key = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
446+
447+ if (!key) {
448+ console.warn('No Chutes key found');
449+ return response.sendStatus(400);
450+ }
451+
452+ const requestBody = {
453+ text: request.body.input,
454+ voice: request.body.voice || 'af_heart',
455+ };
456+
457+ console.debug('Chutes TTS request', requestBody);
458+
459+ const result = await fetch('https://chutes-kokoro.chutes.ai/speak', {
460+ method: 'POST',
461+ headers: {
462+ 'Content-Type': 'application/json',
463+ Authorization: `Bearer ${key}`,
464+ },
465+ body: JSON.stringify(requestBody),
466+ });
467+
468+ if (!result.ok) {
469+ const text = await result.text();
470+ console.warn('Chutes TTS request failed', result.statusText, text);
471+ return response.status(500).send(text);
472+ }
473+
474+ const contentType = result.headers.get('content-type') || 'audio/mpeg';
475+ const buffer = await result.arrayBuffer();
476+ response.setHeader('Content-Type', contentType);
477+ return response.send(Buffer.from(buffer));
478+ } catch (error) {
479+ console.error('Chutes TTS generation failed', error);
480+ response.status(500).send('Internal server error');
481+ }
482+});
483+
484+router.post('/chutes/models/embedding', async (request, response) => {
485+ try {
486+ const key = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
487+
488+ if (!key) {
489+ console.warn('No Chutes key found');
490+ return response.sendStatus(400);
491+ }
492+
493+ const result = await fetch('https://api.chutes.ai/chutes/?template=embedding&include_public=true&limit=999', {
494+ method: 'GET',
495+ headers: {
496+ Authorization: `Bearer ${key}`,
497+ },
498+ });
499+
500+ if (!result.ok) {
501+ const text = await result.text();
502+ console.warn('Chutes embedding models request failed', result.statusText, text);
503+ return response.status(500).send(text);
504+ }
505+
506+ /** @type {any} */
507+ const data = await result.json();
508+
509+ if (!Array.isArray(data?.items)) {
510+ console.warn('Chutes embedding models response invalid', data);
511+ return response.sendStatus(500);
512+ }
513+ return response.json(data.items);
514+ } catch (error) {
515+ console.error('Chutes embedding models fetch failed', error);
516+ response.sendStatus(500);
517+ }
518+});
519+
434520router.post('/generate-image', async (request, response) => {
435521 try {
436522 const key = readSecret(request.user.directories, SECRET_KEYS.OPENAI);
src/endpoints/secrets.js+1 -0
@@ -45,6 +45,7 @@ export const SECRET_KEYS = {
4545 STABILITY: 'api_key_stability',
4646 CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
4747 TAVILY: 'api_key_tavily',
48+ CHUTES: 'api_key_chutes',
4849 ELECTRONHUB: 'api_key_electronhub',
4950 NANOGPT: 'api_key_nanogpt',
5051 BFL: 'api_key_bfl',
src/endpoints/stable-diffusion.js+84 -0
@@ -1083,6 +1083,89 @@ electronhub.post('/sizes', async (request, response) => {
10831083 return response.send({ sizes });
10841084});
10851085
1086+const chutes = express.Router();
1087+
1088+chutes.post('/models', async (request, response) => {
1089+ try {
1090+ const key = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
1091+
1092+ if (!key) {
1093+ console.warn('Chutes key not found.');
1094+ return response.sendStatus(400);
1095+ }
1096+
1097+ const modelsResponse = await fetch('https://api.chutes.ai/chutes/?template=diffusion&include_public=true&limit=999', {
1098+ method: 'GET',
1099+ headers: {
1100+ 'Authorization': `Bearer ${key}`,
1101+ 'Content-Type': 'application/json',
1102+ },
1103+ });
1104+
1105+ if (!modelsResponse.ok) {
1106+ console.warn('Chutes returned an error.');
1107+ return response.sendStatus(500);
1108+ }
1109+
1110+ const data = await modelsResponse.json();
1111+
1112+ const chutesData = /** @type {{items: Array<{name: string}>}} */ (data);
1113+ const models = chutesData.items.map(x => ({ value: x.name, text: x.name })).sort((a, b) => a?.text?.localeCompare(b?.text));
1114+ return response.send(models);
1115+ }
1116+ catch (error) {
1117+ console.error(error);
1118+ return response.sendStatus(500);
1119+ }
1120+});
1121+
1122+chutes.post('/generate', async (request, response) => {
1123+ try {
1124+ const key = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
1125+
1126+ if (!key) {
1127+ console.warn('Chutes key not found.');
1128+ return response.sendStatus(400);
1129+ }
1130+
1131+ const bodyParams = {
1132+ model: request.body.model,
1133+ prompt: request.body.prompt,
1134+ negative_prompt: request.body.negative_prompt,
1135+ guidance_scale: request.body.guidance_scale || 7.0,
1136+ width: request.body.width || 1024,
1137+ height: request.body.height || 1024,
1138+ num_inference_steps: request.body.steps || 10,
1139+ };
1140+
1141+ console.debug('Chutes request:', bodyParams);
1142+
1143+ const result = await fetch('https://image.chutes.ai/generate', {
1144+ method: 'POST',
1145+ headers: {
1146+ 'Authorization': `Bearer ${key}`,
1147+ 'Content-Type': 'application/json',
1148+ },
1149+ body: JSON.stringify(bodyParams),
1150+ });
1151+
1152+ if (!result.ok) {
1153+ const text = await result.text();
1154+ console.warn('Chutes returned an error:', text);
1155+ return response.sendStatus(500);
1156+ }
1157+
1158+ const buffer = await result.arrayBuffer();
1159+ const base64 = Buffer.from(buffer).toString('base64');
1160+
1161+ return response.send({ image: base64 });
1162+ }
1163+ catch (error) {
1164+ console.error(error);
1165+ return response.sendStatus(500);
1166+ }
1167+});
1168+
10861169const nanogpt = express.Router();
10871170
10881171nanogpt.post('/models', async (request, response) => {
@@ -1566,6 +1649,7 @@ router.use('/drawthings', drawthings);
15661649router.use('/pollinations', pollinations);
15671650router.use('/stability', stability);
15681651router.use('/huggingface', huggingface);
1652+router.use('/chutes', chutes);
15691653router.use('/electronhub', electronhub);
15701654router.use('/nanogpt', nanogpt);
15711655router.use('/bfl', bfl);
src/endpoints/vectors.js+10 -0
@@ -36,6 +36,7 @@ const SOURCES = [
3636 'vertexai',
3737 'electronhub',
3838 'openrouter',
39+ 'chutes',
3940];
4041
4142/**
@@ -79,6 +80,8 @@ async function getVector(source, sourceSettings, text, isQuery, directories) {
7980 return sourceSettings.embeddings[text];
8081 case 'koboldcpp':
8182 return sourceSettings.embeddings[text];
83+ case 'chutes':
84+ return getOpenAIVector(text, source, directories, sourceSettings.model);
8285 }
8386
8487 throw new Error(`Unknown vector source ${source}`);
@@ -144,6 +147,9 @@ async function getBatchVector(source, sourceSettings, texts, isQuery, directorie
144147 case 'koboldcpp':
145148 results.push(...texts.map(x => sourceSettings.embeddings[x]));
146149 break;
150+ case 'chutes':
151+ results.push(...await getOpenAIBatchVector(batch, source, directories, sourceSettings.model));
152+ break;
147153 default:
148154 throw new Error(`Unknown vector source ${source}`);
149155 }
@@ -228,6 +234,10 @@ function getSourceSettings(source, request) {
228234 model: String(request.body.model),
229235 embeddings: request.body.embeddings ?? {},
230236 };
237+ case 'chutes':
238+ return {
239+ model: String(request.body.model || 'chutes-qwen-qwen3-embedding-8b'),
240+ };
231241 default:
232242 return {};
233243 }
src/vectors/openai-vectors.js+26 -5
@@ -8,30 +8,44 @@ const SOURCES = {
88 url: 'https://api.together.xyz/v1',
99 model: 'togethercomputer/m2-bert-80M-32k-retrieval',
1010 headers: {},
11+ processBody: () => {},
1112 },
1213 'mistral': {
1314 secretKey: SECRET_KEYS.MISTRALAI,
1415 url: 'https://api.mistral.ai/v1',
1516 model: 'mistral-embed',
1617 headers: {},
18+ processBody: () => {},
1719 },
1820 'openai': {
1921 secretKey: SECRET_KEYS.OPENAI,
2022 url: 'https://api.openai.com/v1',
2123 model: 'text-embedding-ada-002',
2224 headers: {},
25+ processBody: () => {},
2326 },
2427 'electronhub': {
2528 secretKey: SECRET_KEYS.ELECTRONHUB,
2629 url: 'https://api.electronhub.ai/v1',
2730 model: 'text-embedding-3-small',
2831 headers: {},
32+ processBody: () => {},
2933 },
3034 'openrouter': {
3135 secretKey: SECRET_KEYS.OPENROUTER,
3236 url: 'https://openrouter.ai/api/v1',
3337 model: 'openai/text-embedding-3-large',
3438 headers: { ...OPENROUTER_HEADERS },
39+ processBody: () => {},
40+ },
41+ 'chutes': {
42+ secretKey: SECRET_KEYS.CHUTES,
43+ url: 'https://{{MODEL}}.chutes.ai/v1',
44+ model: 'chutes-qwen-qwen3-embedding-8b',
45+ headers: {},
46+ processBody: (body) => {
47+ body.model = null;
48+ },
3549 },
3650};
3751
@@ -58,7 +72,17 @@ export async function getOpenAIBatchVector(texts, source, directories, model = '
5872 throw new Error('No API key found');
5973 }
6074
6175 const urlmodelName = model || config.urlmodel;
76+ const url = config.url.replace('{{MODEL}}', modelName);
77+ const body = {
78+ input: texts,
79+ model: modelName,
80+ };
81+
82+ if (typeof config.processBody === 'function') {
83+ config.processBody(body);
84+ }
85+
6286 const response = await fetch(`${url}/embeddings`, {
6387 method: 'POST',
6488 headers: {
@@ -66,10 +90,7 @@ export async function getOpenAIBatchVector(texts, source, directories, model = '
6690 'Authorization': `Bearer ${key}`,
6791 ...config.headers,
6892 },
6993 body: JSON.stringify({body),
70- input: texts,
71- model: model || config.model,
72- }),
7394 });
7495
7596 if (!response.ok) {