Merge pull request #4135 from D1m7asis/release feat: Added AI/ML API Provider Support

fece612f09ef66fef15aa7eeed889aef70c20e11

Dmitry <tumaskow@yandex.ru>

Signed
19 files changed, +542 -69Ignore whitespace
public/img/aimlapi.svg+4 -0
@@ -0,0 +1,4 @@
1+<svg width="24" height="21" viewBox="0 0 24 21" fill="none" xmlns="http://www.w3.org/2000/svg">
2+ <path d="M22.8059 9.20726C23.2424 9.96331 23.2424 10.8948 22.8059 11.6508L18.2791 19.4915C17.8426 20.2475 17.0359 20.7133 16.1629 20.7133H7.10934C6.23633 20.7133 5.42964 20.2475 4.99313 19.4915L0.466342 11.6508C0.0298381 10.8948 0.02984 9.96331 0.466344 9.20726L4.99313 1.36663C5.42964 0.61058 6.23633 0.144836 7.10934 0.144836H16.1629C17.0359 0.144836 17.8426 0.610581 18.2791 1.36663L22.8059 9.20726Z" />
3+ <path fill-rule="evenodd" clip-rule="evenodd" d="M6.66791 13.8818C6.49765 13.8818 6.32633 13.8275 6.18162 13.7168C5.83259 13.4476 5.76662 12.9464 6.03583 12.5974L9.22068 8.45807C9.3505 8.28888 9.5431 8.17928 9.75379 8.15268C9.96874 8.12501 10.1794 8.1846 10.3465 8.31761L13.3472 10.6746L15.9724 7.28756C16.2427 6.93748 16.7428 6.87257 17.0918 7.14498C17.4408 7.41526 17.5047 7.91645 17.2344 8.26441L14.1166 12.2867C13.9868 12.4548 13.7952 12.5644 13.5845 12.59C13.3717 12.6187 13.161 12.557 12.9929 12.4261L9.99428 10.0702L7.30105 13.57C7.14356 13.7743 6.90733 13.8818 6.66791 13.8818Z" fill="black"/>
4+</svg>
public/index.html+31 -15
@@ -652,7 +652,7 @@
652652 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="65536">
653653 </div>
654654 </div>
655655 <div class="range-block" data-source="openai,custom,xai,aimlapi">
656656 <div class="range-block-title" data-i18n="Multiple swipes per generation">
657657 Multiple swipes per generation
658658 </div>
@@ -691,7 +691,7 @@
691691 </span>
692692 </div>
693693 </div>
694694 <div class="range-block" data-source="openai,claude,windowai,aimlapi,openrouter,ai21,scale,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai,pollinations">
695695 <div class="range-block-title" data-i18n="Temperature">
696696 Temperature
697697 </div>
@@ -704,7 +704,7 @@
704704 </div>
705705 </div>
706706 </div>
707707 <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai,pollinations">
708708 <div class="range-block-title" data-i18n="Frequency Penalty">
709709 Frequency Penalty
710710 </div>
@@ -717,7 +717,7 @@
717717 </div>
718718 </div>
719719 </div>
720720 <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai,pollinations">
721721 <div class="range-block-title" data-i18n="Presence Penalty">
722722 Presence Penalty
723723 </div>
@@ -730,7 +730,7 @@
730730 </div>
731731 </div>
732732 </div>
733733 <div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity">
734734 <div class="range-block-title" data-i18n="Top K">
735735 Top K
736736 </div>
@@ -743,7 +743,7 @@
743743 </div>
744744 </div>
745745 </div>
746746 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,scale,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai,pollinations">
747747 <div class="range-block-title" data-i18n="Top P">
748748 Top P
749749 </div>
@@ -980,7 +980,7 @@
980980 </div>
981981 </div>
982982 </div>
983983 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,nanogpt,xai,pollinations,aimlapi">
984984 <div class="range-block-title justifyLeft" data-i18n="Seed">
985985 Seed
986986 </div>
@@ -1969,7 +1969,7 @@
19691969 </span>
19701970 </div>
19711971 </div>
19721972 <div class="range-block" data-source="makersuite,vertexai,aimlapi,openrouter,claude">
19731973 <label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">
19741974 <input id="openai_enable_web_search" type="checkbox" />
19751975 <span data-i18n="Enable web search">Enable web search</span>
@@ -1983,7 +1983,7 @@
19831983 </b>
19841984 </div>
19851985 </div>
19861986 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,deepseek,makersuite,vertexai,ai21,xai,pollinations">
19871987 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
19881988 <input id="openai_function_calling" type="checkbox" />
19891989 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -1994,7 +1994,7 @@
19941994 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>
19951995 </div>
19961996 </div>
19971997 <div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,01ai,xai,pollinations">
19981998 <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
19991999 <input id="openai_image_inlining" type="checkbox" />
20002000 <span data-i18n="Send inline images">Send inline images</span>
@@ -2064,7 +2064,7 @@
20642064 </span>
20652065 </div>
20662066 </div>
20672067 <div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai">
20682068 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
20692069 <input id="openai_show_thoughts" type="checkbox" />
20702070 <span data-i18n="Request model reasoning">Request model reasoning</span>
@@ -2078,7 +2078,7 @@
20782078 </span>
20792079 </div>
20802080 </div>
20812081 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations">
20822082 <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.">
20832083 <label for="openai_reasoning_effort">
20842084 <span data-i18n="Reasoning Effort">Reasoning Effort</span>
@@ -2092,7 +2092,7 @@
20922092 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>
20932093 <option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>
20942094 </select>
20952095 <div class="toggle-description justifyLeft marginBot5" data-source="openai,custom,xai,aimlapi,openrouter" data-i18n="OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.">
20962096 OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.
20972097 </div>
20982098 <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.">
@@ -2131,7 +2131,7 @@
21312131 </div>
21322132 </div>
21332133 </div>
21342134 <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,scale,custom">
21352135 <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias">
21362136 Logit Bias
21372137 </div>
@@ -2376,7 +2376,7 @@
23762376 <input id="api_key_openrouter-tg" name="api_key_openrouter" class="text_pole flex1 api_key_openrouter" value="" type="text" autocomplete="off">
23772377 <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_openrouter"></div>
23782378 </div>
23792379 <div data-for="api_key_openrouter-tg" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
23802380 For privacy reasons, your API key will be hidden after you click 'Connect'.
23812381 </div>
23822382 <div>
@@ -2789,6 +2789,7 @@
27892789 <optgroup>
27902790 <option value="01ai">01.AI (Yi)</option>
27912791 <option value="ai21">AI21</option>
2792+ <option value="aimlapi">AI/ML API</option>
27922793 <option value="claude">Claude</option>
27932794 <option value="cohere">Cohere</option>
27942795 <option value="deepseek">DeepSeek</option>
@@ -3632,6 +3633,21 @@
36323633 <option value="grok-beta">grok-beta</option>
36333634 </select>
36343635 </div>
3636+ <div id="aimlapi_form" data-source="aimlapi">
3637+ <h4>
3638+ <span>AI/ML API Key</span>
3639+ </h4>
3640+ <div class="flex-container">
3641+ <input id="api_key_aimlapi" name="api_key_aimlapi" class="text_pole flex1" value="" type="text" autocomplete="off">
3642+ <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_aimlapi"></div>
3643+ </div>
3644+ <div data-for="api_key_aimlapi" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
3645+ For privacy reasons, your API key will be hidden after you click 'Connect'.
3646+ </div>
3647+ <h4>AI/ML Model</h4>
3648+ <select id="model_aimlapi_select">
3649+ </select>
3650+ </div>
36353651 <div id="pollinations_form" data-source="pollinations">
36363652 <h4 data-i18n="Pollinations Model">Pollinations Model</h4>
36373653 <select id="model_pollinations_select">
public/script.js+24 -3
@@ -250,7 +250,20 @@ import {
250250import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
251251import { hideLoader, showLoader } from './scripts/loader.js';
252252import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';
253-import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels, loadTabbyModels, loadGenericModels } from './scripts/textgen-models.js';
253+import {
254+ loadFeatherlessModels,
255+ loadMancerModels,
256+ loadOllamaModels,
257+ loadTogetherAIModels,
258+ loadInfermaticAIModels,
259+ loadOpenRouterModels,
260+ loadVllmModels,
261+ loadAphroditeModels,
262+ loadDreamGenModels,
263+ initTextGenModels,
264+ loadTabbyModels,
265+ loadGenericModels,
266+} from './scripts/textgen-models.js';
254267import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities } from './scripts/chats.js';
255268import { getPresetManager, initPresetManager } from './scripts/preset-manager.js';
256269import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js';
@@ -542,6 +555,10 @@ export const event_types = {
542555 TOOL_CALLS_PERFORMED: 'tool_calls_performed',
543556 TOOL_CALLS_RENDERED: 'tool_calls_rendered',
544557 CHARACTER_MANAGEMENT_DROPDOWN: 'charManagementDropdown',
558+ SECRET_WRITTEN: 'secret_written',
559+ SECRET_DELETED: 'secret_deleted',
560+ SECRET_ROTATED: 'secret_rotated',
561+ SECRET_EDITED: 'secret_edited',
545562};
546563
547564export const eventSource = new EventEmitter([event_types.APP_READY]);
@@ -2726,7 +2743,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
27262743
27272744/**
27282745 * Returns the URL of the avatar for the given character Id.
27292746 * @param {number|string} characterId Character Id
27302747 * @returns {string} Avatar URL
27312748 */
27322749export function getCharacterAvatar(characterId) {
@@ -6108,7 +6125,11 @@ export function extractMessageFromData(data, activeApi = null) {
61086125 case 'koboldhorde':
61096126 return data.text;
61106127 case 'textgenerationwebui':
61116128 return data.choices?.[0]?.text ?? data.content ?? data.response ?? '';
6129+ ?? data.choices?.[0]?.message?.content
6130+ ?? data.content
6131+ ?? data.response
6132+ ?? '';
61126133 case 'novel':
61136134 return data.output;
61146135 case 'openai':
public/scripts/RossAscends-mods.js+1 -0
@@ -412,6 +412,7 @@ function RA_autoconnect(PrevApi) {
412412 || (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT)
413413 || (secret_state[SECRET_KEYS.DEEPSEEK] && oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK)
414414 || (secret_state[SECRET_KEYS.XAI] && oai_settings.chat_completion_source == chat_completion_sources.XAI)
415+ || (secret_state[SECRET_KEYS.AIMLAPI] && oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI)
415416 || (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS)
416417 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
417418 ) {
public/scripts/extensions.js+1 -1
@@ -1535,7 +1535,7 @@ export async function runGenerationInterceptors(chat, contextSize, type) {
15351535
15361536/**
15371537 * Writes a field to the character's data extensions object.
15381538 * @param {number|string} characterId Index in the character array
15391539 * @param {string} key Field name
15401540 * @param {any} value Field value
15411541 * @returns {Promise<void>} When the field is written
public/scripts/extensions/caption/index.js+1 -0
@@ -433,6 +433,7 @@ jQuery(async function () {
433433 'zerooneai': SECRET_KEYS.ZEROONEAI,
434434 'groq': SECRET_KEYS.GROQ,
435435 'cohere': SECRET_KEYS.COHERE,
436+ 'aimlapi': SECRET_KEYS.AIMLAPI,
436437 };
437438
438439 if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {
public/scripts/extensions/caption/settings.html+19 -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="zerooneai">01.AI (Yi)</option>
21+ <option value="aimlapi">AI/ML API</option>
2122 <option value="anthropic">Anthropic</option>
2223 <option value="cohere">Cohere</option>
2324 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
@@ -127,6 +128,24 @@
127128 <option data-type="groq" value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
128129 <option data-type="groq" value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
129130 <option data-type="groq" value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>
131+ <option data-type="aimlapi" value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
132+ <option data-type="aimlapi" value="gpt-4o-2024-08-06">gpt-4o-2024-08-06</option>
133+ <option data-type="aimlapi" value="gpt-4o">gpt-4o</option>
134+ <option data-type="aimlapi" value="gpt-4o-mini">gpt-4o-mini</option>
135+ <option data-type="aimlapi" value="gpt-4o-mini-2024-07-18">gpt-4o-mini-2024-07-18</option>
136+ <option data-type="aimlapi" value="chatgpt-4o-latest">chatgpt-4o-latest</option>
137+ <option data-type="aimlapi" value="gpt-4-turbo">gpt-4-turbo</option>
138+ <option data-type="aimlapi" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
139+ <option data-type="aimlapi" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
140+ <option data-type="aimlapi" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
141+ <option data-type="aimlapi" value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>
142+ <option data-type="aimlapi" value="google/gemini-1.5-flash">google/gemini-1.5-flash</option>
143+ <option data-type="aimlapi" value="google/gemini-1.5-pro">google/gemini-1.5-pro</option>
144+ <option data-type="aimlapi" value="google/gemini-2.0-flash">google/gemini-2.0-flash</option>
145+ <option data-type="aimlapi" value="google/gemini-2.0-flash-exp">google/gemini-2.0-flash-exp</option>
146+ <option data-type="aimlapi" value="google/gemini-2.5-flash-preview">google/gemini-2.5-flash-preview</option>
147+ <option data-type="aimlapi" value="meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo">meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo</option>
148+ <option data-type="aimlapi" value="meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo">meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo</option>
130149 <option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
131150 <option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
132151 <option data-type="openrouter" value="openai/gpt-4o-2024-05-13">openai/gpt-4o-2024-05-13</option>
public/scripts/extensions/shared.js+4 -0
@@ -233,6 +233,10 @@ function throwIfInvalidModel(useReverseProxy) {
233233 if (multimodalApi === 'custom' && !oai_settings.custom_url) {
234234 throw new Error('Custom API URL is not set.');
235235 }
236+
237+ if (multimodalApi === 'aimlapi' && !secret_state[SECRET_KEYS.AIMLAPI]) {
238+ throw new Error('AI/ML API key is not set.');
239+ }
236240}
237241
238242/**
public/scripts/extensions/stable-diffusion/index.js+92 -41
@@ -41,7 +41,7 @@ import {
4141 stringFormat,
4242} from '../../utils.js';
4343import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js';
4444import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
4545import { getNovelAnlas, getNovelUnlimitedImageGeneration, loadNovelSubscriptionData } from '../../nai-settings.js';
4646import { getMultimodalCaption } from '../shared.js';
4747import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
@@ -53,7 +53,7 @@ import {
5353} from '../../slash-commands/SlashCommandArgument.js';
5454import { debounce_timeout } from '../../constants.js';
5555import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
5656import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';
5757import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
5858import { ToolManager } from '../../tool-calling.js';
5959import { MacrosParser } from '../../macros.js';
@@ -74,6 +74,7 @@ const sources = {
7474 novel: 'novel',
7575 vlad: 'vlad',
7676 openai: 'openai',
77+ aimlapi: 'aimlapi',
7778 comfy: 'comfy',
7879 togetherai: 'togetherai',
7980 drawthings: 'drawthings',
@@ -1144,42 +1145,6 @@ function onComfyWorkflowChange() {
11441145 saveSettingsDebounced();
11451146}
11461147
1147-async function onApiKeyClick(popupText, secretKey) {
1148- const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, '', {
1149- customButtons: [{
1150- text: 'Remove Key',
1151- appendAtEnd: true,
1152- result: POPUP_RESULT.NEGATIVE,
1153- action: async () => {
1154- await writeSecret(secretKey, '');
1155- toastr.success('API Key removed');
1156- await loadSettingOptions();
1157- },
1158- }],
1159- });
1160-
1161- if (!key) {
1162- return;
1163- }
1164-
1165- await writeSecret(secretKey, String(key));
1166-
1167- toastr.success('API Key saved');
1168- await loadSettingOptions();
1169-}
1170-
1171-async function onStabilityKeyClick() {
1172- return onApiKeyClick('Stability AI API Key:', SECRET_KEYS.STABILITY);
1173-}
1174-
1175-async function onBflKeyClick() {
1176- return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL);
1177-}
1178-
1179-async function onFalaiKeyClick() {
1180- return onApiKeyClick('FALAI API Key:', SECRET_KEYS.FALAI);
1181-}
1182-
11831148function onBflUpsamplingInput() {
11841149 extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked');
11851150 saveSettingsDebounced();
@@ -1303,6 +1268,7 @@ async function onModelChange() {
13031268 sources.horde,
13041269 sources.novel,
13051270 sources.openai,
1271+ sources.aimlapi,
13061272 sources.togetherai,
13071273 sources.pollinations,
13081274 sources.stability,
@@ -1505,6 +1471,9 @@ async function loadSamplers() {
15051471 case sources.openai:
15061472 samplers = ['N/A'];
15071473 break;
1474+ case sources.aimlapi:
1475+ samplers = ['N/A'];
1476+ break;
15081477 case sources.comfy:
15091478 samplers = await loadComfySamplers();
15101479 break;
@@ -1695,6 +1664,9 @@ async function loadModels() {
16951664 case sources.openai:
16961665 models = await loadOpenAiModels();
16971666 break;
1667+ case sources.aimlapi:
1668+ models = await loadAimlapiModels();
1669+ break;
16981670 case sources.comfy:
16991671 models = await loadComfyModels();
17001672 break;
@@ -1959,6 +1931,23 @@ async function loadOpenAiModels() {
19591931 ];
19601932}
19611933
1934+async function loadAimlapiModels() {
1935+ $('#sd_aimlapi_key').toggleClass('success', !!secret_state[SECRET_KEYS.AIMLAPI]);
1936+
1937+ const result = await fetch('/api/sd/aimlapi/models', {
1938+ method: 'POST',
1939+ headers: getRequestHeaders(),
1940+ });
1941+
1942+ if (!result.ok) {
1943+ return [];
1944+ }
1945+
1946+ const json = await result.json();
1947+
1948+ return (json.data || []);
1949+}
1950+
19621951async function loadVladModels() {
19631952 if (!extension_settings.sd.vlad_url) {
19641953 return [];
@@ -2086,6 +2075,9 @@ async function loadSchedulers() {
20862075 case sources.openai:
20872076 schedulers = ['N/A'];
20882077 break;
2078+ case sources.aimlapi:
2079+ schedulers = ['N/A'];
2080+ break;
20892081 case sources.togetherai:
20902082 schedulers = ['N/A'];
20912083 break;
@@ -2177,6 +2169,9 @@ async function loadVaes() {
21772169 case sources.openai:
21782170 vaes = ['N/A'];
21792171 break;
2172+ case sources.aimlapi:
2173+ vaes = ['N/A'];
2174+ break;
21802175 case sources.togetherai:
21812176 vaes = ['N/A'];
21822177 break;
@@ -2749,6 +2744,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
27492744 case sources.openai:
27502745 result = await generateOpenAiImage(prefixedPrompt, signal);
27512746 break;
2747+ case sources.aimlapi:
2748+ result = await generateAimlapiImage(prefixedPrompt, signal);
2749+ break;
27522750 case sources.comfy:
27532751 result = await generateComfyImage(prefixedPrompt, negativePrompt, signal);
27542752 break;
@@ -3334,6 +3332,47 @@ async function generateOpenAiImage(prompt, signal) {
33343332}
33353333
33363334/**
3335+ * Universal image generation via AIMLAPI:
3336+ * - Builds the right request body for any model (OpenAI vs SD/Flux/Recraft).
3337+ * - Extracts the URL or base64 response.
3338+ * - If it’s a URL, fetches the image and converts to base64.
3339+ * - Returns { format: 'png', data: '<base64 string>' }, ready for saveBase64AsFile().
3340+ */
3341+async function generateAimlapiImage(prompt, signal) {
3342+ const model = extension_settings.sd.model.toLowerCase();
3343+ const isSdLike =
3344+ model.startsWith('flux/') ||
3345+ model.startsWith('stable') ||
3346+ model === 'recraft-v3' ||
3347+ model === 'triposr';
3348+
3349+ const body = { prompt, model };
3350+ if (isSdLike) {
3351+ body.steps = clamp(extension_settings.sd.steps, 1, 50);
3352+ body.guidance = clamp(extension_settings.sd.scale, 1.5, 5);
3353+ body.width = clamp(extension_settings.sd.width, 256, 1440);
3354+ body.height = clamp(extension_settings.sd.height, 256, 1440);
3355+ if (extension_settings.sd.seed >= 0) body.seed = extension_settings.sd.seed;
3356+ } else {
3357+ body.n = 1;
3358+ body.size = `${extension_settings.sd.width}x${extension_settings.sd.height}`;
3359+ body.quality = extension_settings.sd.openai_quality;
3360+ body.style = extension_settings.sd.openai_style;
3361+ }
3362+
3363+ const res = await fetch('/api/sd/aimlapi/generate-image', {
3364+ method: 'POST',
3365+ headers: getRequestHeaders(),
3366+ signal,
3367+ body: JSON.stringify(body),
3368+ });
3369+ if (!res.ok) throw new Error(await res.text());
3370+
3371+ const { format, data } = await res.json();
3372+ return { format, data };
3373+}
3374+
3375+/**
33373376 * Generates an image in ComfyUI using the provided prompt and configuration settings.
33383377 *
33393378 * @param {string} prompt - The main instruction used to guide the image generation.
@@ -3849,6 +3888,8 @@ function isValidState() {
38493888 return secret_state[SECRET_KEYS.NOVEL];
38503889 case sources.openai:
38513890 return secret_state[SECRET_KEYS.OPENAI];
3891+ case sources.aimlapi:
3892+ return secret_state[SECRET_KEYS.AIMLAPI];
38523893 case sources.comfy:
38533894 return true;
38543895 case sources.togetherai:
@@ -4522,13 +4563,10 @@ jQuery(async () => {
45224563 $('#sd_interactive_visible').on('input', onInteractiveVisibleInput);
45234564 $('#sd_tool_visible').on('input', onToolVisibleInput);
45244565 $('#sd_swap_dimensions').on('click', onSwapDimensionsClick);
4525- $('#sd_stability_key').on('click', onStabilityKeyClick);
45264566 $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);
45274567 $('#sd_huggingface_model_id').on('input', onHFModelInput);
45284568 $('#sd_function_tool').on('input', onFunctionToolInput);
4529- $('#sd_bfl_key').on('click', onBflKeyClick);
45304569 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);
4531- $('#sd_falai_key').on('click', onFalaiKeyClick);
45324570
45334571 if (!CSS.supports('field-sizing', 'content')) {
45344572 $('.sd_settings .inline-drawer-toggle').on('click', function () {
@@ -4556,6 +4594,19 @@ jQuery(async () => {
45564594
45574595 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
45584596
4597+ [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
4598+ eventSource.on(event, async (/** @type {string} */ key) => {
4599+ switch (key) {
4600+ case SECRET_KEYS.BFL:
4601+ case SECRET_KEYS.FALAI:
4602+ case SECRET_KEYS.STABILITY:
4603+ case SECRET_KEYS.AIMLAPI:
4604+ await loadSettingOptions();
4605+ break;
4606+ }
4607+ });
4608+ });
4609+
45594610 await loadSettings();
45604611 $('body').addClass('sd');
45614612
public/scripts/extensions/stable-diffusion/settings.html+14 -4
@@ -37,6 +37,7 @@
3737 </label>
3838 <label for="sd_source" data-i18n="Source">Source</label>
3939 <select id="sd_source">
40+ <option value="aimlapi">AI/ML API</option>
4041 <option value="bfl">BFL (Black Forest Labs)</option>
4142 <option value="comfy">ComfyUI</option>
4243 <option value="drawthings">DrawThings HTTP API</option>
@@ -139,7 +140,16 @@
139140 </div>
140141 <i>Hint: Save an API key in the NovelAI API settings to use it here.</i>
141142 </div>
142143 <div data-sd-source="openaiaimlapi">
144+ <div class="flex-container flexnowrap alignItemsBaseline marginBot5">
145+ <strong class="flex1" data-i18n="API Key">API Key</strong>
146+ <div id="sd_aimlapi_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_aimlapi">
147+ <i class="fa-fw fa-solid fa-key"></i>
148+ <span data-i18n="Click to set">Click to set</span>
149+ </div>
150+ </div>
151+ </div>
152+ <div data-sd-source="openai,aimlapi">
143153 <small data-i18n="These settings only apply to DALL-E 3">These settings only apply to DALL-E 3</small>
144154 <div class="flex-container">
145155 <div class="flex1">
@@ -200,7 +210,7 @@
200210 <div data-sd-source="stability">
201211 <div class="flex-container flexnowrap alignItemsBaseline marginBot5">
202212 <strong class="flex1" data-i18n="API Key">API Key</strong>
203213 <div id="sd_stability_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_stability">
204214 <i class="fa-fw fa-solid fa-key"></i>
205215 <span data-i18n="Click to set">Click to set</span>
206216 </div>
@@ -244,7 +254,7 @@
244254 <i class="fa-solid fa-share-from-square"></i>
245255 </a>
246256 <span class="expander"></span>
247257 <div id="sd_bfl_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_bfl">
248258 <i class="fa-fw fa-solid fa-key"></i>
249259 <span data-i18n="Click to set">Click to set</span>
250260 </div>
@@ -264,7 +274,7 @@
264274 <i class="fa-solid fa-share-from-square"></i>
265275 </a>
266276 <span class="expander"></span>
267277 <div id="sd_falai_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_falai">
268278 <i class="fa-fw fa-solid fa-key"></i>
269279 <span data-i18n="Click to set">Click to set</span>
270280 </div>
public/scripts/openai.js+137 -3
@@ -185,6 +185,7 @@ export const chat_completion_sources = {
185185 ZEROONEAI: '01ai',
186186 NANOGPT: 'nanogpt',
187187 DEEPSEEK: 'deepseek',
188+ AIMLAPI: 'aimlapi',
188189 XAI: 'xai',
189190 POLLINATIONS: 'pollinations',
190191};
@@ -273,6 +274,7 @@ export const settingsToUpdate = {
273274 groq_model: ['#model_groq_select', 'groq_model', false, true],
274275 nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true],
275276 deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true],
277+ aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true],
276278 zerooneai_model: ['#model_01ai_select', 'zerooneai_model', false, true],
277279 xai_model: ['#model_xai_select', 'xai_model', false, true],
278280 pollinations_model: ['#model_pollinations_select', 'pollinations_model', false, true],
@@ -369,6 +371,7 @@ const default_settings = {
369371 nanogpt_model: 'gpt-4o-mini',
370372 zerooneai_model: 'yi-large',
371373 deepseek_model: 'deepseek-chat',
374+ aimlapi_model: 'gpt-4o-mini-2024-07-18',
372375 xai_model: 'grok-3-beta',
373376 pollinations_model: 'openai',
374377 custom_model: '',
@@ -456,6 +459,7 @@ const oai_settings = {
456459 nanogpt_model: 'gpt-4o-mini',
457460 zerooneai_model: 'yi-large',
458461 deepseek_model: 'deepseek-chat',
462+ aimlapi_model: 'gpt-4-turbo',
459463 xai_model: 'grok-3-beta',
460464 pollinations_model: 'openai',
461465 custom_model: '',
@@ -1712,6 +1716,8 @@ export function getChatCompletionModel(source = null) {
17121716 return oai_settings.nanogpt_model;
17131717 case chat_completion_sources.DEEPSEEK:
17141718 return oai_settings.deepseek_model;
1719+ case chat_completion_sources.AIMLAPI:
1720+ return oai_settings.aimlapi_model;
17151721 case chat_completion_sources.XAI:
17161722 return oai_settings.xai_model;
17171723 case chat_completion_sources.POLLINATIONS:
@@ -1836,6 +1842,19 @@ function saveModelList(data) {
18361842 $('#model_01ai_select').val(oai_settings.zerooneai_model).trigger('change');
18371843 }
18381844
1845+ if (oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI) {
1846+ $('#model_aimlapi_select').empty();
1847+ const chatModels = model_list.filter(m => m.type === 'chat-completion');
1848+
1849+ appendAimlapiOptions(aimlapiGroupByVendor(chatModels));
1850+
1851+ if (!oai_settings.aimlapi_model && chatModels.length > 0) {
1852+ oai_settings.aimlapi_model = chatModels[0].id;
1853+ }
1854+
1855+ $('#model_aimlapi_select').val(oai_settings.aimlapi_model).trigger('change');
1856+ }
1857+
18391858 if (oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) {
18401859 /** @type {HTMLSelectElement} */
18411860 const mistralModelSelect = document.querySelector('#model_mistralai_select');
@@ -2011,6 +2030,56 @@ function openRouterGroupByVendor(array) {
20112030 }, new Map());
20122031}
20132032
2033+function aimlapiGroupByVendor(array) {
2034+ return array.reduce((acc, curr) => {
2035+ const vendor = curr.info.developer;
2036+
2037+ if (!acc.has(vendor)) {
2038+ acc.set(vendor, []);
2039+ }
2040+
2041+ acc.get(vendor).push(curr);
2042+
2043+ return acc;
2044+ }, new Map());
2045+}
2046+
2047+function appendAimlapiOptions(model_list) {
2048+ const appendOption = (model, parent = null) => {
2049+ (parent || $('#model_aimlapi_select')).append(
2050+ $('<option>', {
2051+ value: model.id,
2052+ text: model.info?.name || model.name || model.id,
2053+ }));
2054+ };
2055+
2056+ model_list.forEach((models, vendor) => {
2057+ const optgroup = $(`<optgroup label="${vendor}">`);
2058+
2059+ models.forEach((model) => {
2060+ appendOption(model, optgroup);
2061+ });
2062+
2063+ $('#model_aimlapi_select').append(optgroup);
2064+ });
2065+}
2066+
2067+function getAimlapiModelTemplate(option) {
2068+ const model = model_list.find(x => x.id === option?.element?.value);
2069+
2070+ if (!option.id || !model) {
2071+ return option.text;
2072+ }
2073+
2074+ const vendor = model.id.split('/')[0];
2075+
2076+ return $((`
2077+ <div class="flex-container flexFlowColumn" title="${DOMPurify.sanitize(model.id)}">
2078+ <div><strong>${DOMPurify.sanitize(model.info?.name || model.name || model.id)}</strong> | ${vendor}</div>
2079+ </div>
2080+ `));
2081+}
2082+
20142083async function sendAltScaleRequest(messages, logit_bias, signal, type) {
20152084 const generate_url = '/api/backends/scale-alt/generate';
20162085
@@ -2065,6 +2134,7 @@ function getReasoningEffort() {
20652134 chat_completion_sources.OPENAI,
20662135 chat_completion_sources.CUSTOM,
20672136 chat_completion_sources.XAI,
2137+ chat_completion_sources.AIMLAPI,
20682138 chat_completion_sources.OPENROUTER,
20692139 chat_completion_sources.POLLINATIONS,
20702140 ];
@@ -2122,6 +2192,7 @@ async function sendOpenAIRequest(type, messages, signal) {
21222192 const is01AI = oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI;
21232193 const isNano = oai_settings.chat_completion_source == chat_completion_sources.NANOGPT;
21242194 const isDeepSeek = oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK;
2195+ const isAimlapi = oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI;
21252196 const isXAI = oai_settings.chat_completion_source == chat_completion_sources.XAI;
21262197 const isPollinations = oai_settings.chat_completion_source == chat_completion_sources.POLLINATIONS;
21272198 const isTextCompletion = isOAI && textCompletionModels.includes(oai_settings.openai_model);
@@ -2130,7 +2201,7 @@ async function sendOpenAIRequest(type, messages, signal) {
21302201 const isContinue = type === 'continue';
21312202 const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isOAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model));
21322203 const useLogprobs = !!power_user.request_token_probabilities;
21332204 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom || isXAI || isAimlapi);
21342205
21352206 // If we're using the window.ai extension, use that instead
21362207 // Doesn't support logit bias yet
@@ -2196,7 +2267,7 @@ async function sendOpenAIRequest(type, messages, signal) {
21962267 }
21972268
21982269 // Add logprobs request (currently OpenAI only, max 5 on their side)
21992270 if (useLogprobs && (isOAI || isCustom || isDeepSeek || isXAI || isAimlapi)) {
22002271 generate_data['logprobs'] = 5;
22012272 }
22022273
@@ -2334,7 +2405,7 @@ async function sendOpenAIRequest(type, messages, signal) {
23342405 delete generate_data.max_tokens;
23352406 }
23362407
23372408 if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano || isXAI || isPollinations || isAimlapi) && oai_settings.seed >= 0) {
23382409 generate_data['seed'] = oai_settings.seed;
23392410 }
23402411
@@ -3461,6 +3532,7 @@ function loadOpenAISettings(data, settings) {
34613532 oai_settings.groq_model = settings.groq_model ?? default_settings.groq_model;
34623533 oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model;
34633534 oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model;
3535+ oai_settings.aimlapi_model = settings.aimlapi_model ?? default_settings.aimlapi_model;
34643536 oai_settings.zerooneai_model = settings.zerooneai_model ?? default_settings.zerooneai_model;
34653537 oai_settings.xai_model = settings.xai_model ?? default_settings.xai_model;
34663538 oai_settings.pollinations_model = settings.pollinations_model ?? default_settings.pollinations_model;
@@ -3558,6 +3630,8 @@ function loadOpenAISettings(data, settings) {
35583630 $('#model_deepseek_select').val(oai_settings.deepseek_model);
35593631 $(`#model_deepseek_select option[value="${oai_settings.deepseek_model}"`).prop('selected', true);
35603632 $('#model_01ai_select').val(oai_settings.zerooneai_model);
3633+ $('#model_aimlapi_select').val(oai_settings.aimlapi_model);
3634+ $(`#model_aimlapi_select option[value="${oai_settings.aimlapi_model}"`).prop('selected', true);
35613635 $('#model_xai_select').val(oai_settings.xai_model);
35623636 $(`#model_xai_select option[value="${oai_settings.xai_model}"`).prop('selected', true);
35633637 $('#model_pollinations_select').val(oai_settings.pollinations_model);
@@ -3861,6 +3935,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
38613935 zerooneai_model: settings.zerooneai_model,
38623936 xai_model: settings.xai_model,
38633937 pollinations_model: settings.pollinations_model,
3938+ aimlapi_model: settings.aimlapi_model,
38643939 custom_model: settings.custom_model,
38653940 custom_url: settings.custom_url,
38663941 custom_include_body: settings.custom_include_body,
@@ -4709,6 +4784,15 @@ async function onModelChange() {
47094784 oai_settings.pollinations_model = value;
47104785 }
47114786
4787+ if ($(this).is('#model_aimlapi_select')) {
4788+ if (!value) {
4789+ console.debug('Null AI/ML model selected. Ignoring.');
4790+ return;
4791+ }
4792+ console.log('AI/ML model changed to', value);
4793+ oai_settings.aimlapi_model = value;
4794+ }
4795+
47124796 if ($(this).is('#model_xai_select')) {
47134797 console.log('XAI model changed to', value);
47144798 oai_settings.xai_model = value;
@@ -4995,6 +5079,30 @@ async function onModelChange() {
49955079 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
49965080 }
49975081
5082+ if (oai_settings.chat_completion_source === chat_completion_sources.AIMLAPI) {
5083+ let maxContext;
5084+ if (oai_settings.max_context_unlocked) {
5085+ maxContext = unlocked_max;
5086+ } else {
5087+ const model = model_list.find(m => m.id === oai_settings.aimlapi_model);
5088+ maxContext = (model?.info?.contextLength ?? model?.context_length) || max_32k;
5089+ console.log('[AI/ML API] Model CTX:', model?.info?.contextLength);
5090+ }
5091+
5092+ $('#openai_max_context')
5093+ .prop('max', maxContext)
5094+ .val(Math.min(Number(oai_settings.openai_max_context), maxContext))
5095+ .trigger('input');
5096+
5097+ $('#temp_openai')
5098+ .prop('max', oai_max_temp)
5099+ .val(Number(oai_settings.temp_openai))
5100+ .trigger('input');
5101+
5102+ oai_settings.openai_max_context = Number($('#openai_max_context').val());
5103+ oai_settings.temp_openai = Number($('#temp_openai').val());
5104+ }
5105+
49985106 if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
49995107 oai_settings.pres_pen_openai = Math.min(Math.max(0, oai_settings.pres_pen_openai), 1);
50005108 $('#pres_pen_openai').attr('max', 1).attr('min', 0).val(oai_settings.pres_pen_openai).trigger('input');
@@ -5268,6 +5376,19 @@ async function onConnectButtonClick(e) {
52685376 }
52695377 }
52705378
5379+ if (oai_settings.chat_completion_source === chat_completion_sources.AIMLAPI) {
5380+ const api_key_aimlapi = String($('#api_key_aimlapi').val()).trim();
5381+
5382+ if (api_key_aimlapi.length) {
5383+ await writeSecret(SECRET_KEYS.AIMLAPI, api_key_aimlapi);
5384+ }
5385+
5386+ if (!secret_state[SECRET_KEYS.AIMLAPI]) {
5387+ console.log('No secret key saved for AI/ML API');
5388+ return;
5389+ }
5390+ }
5391+
52715392 startStatusLoading();
52725393 saveSettingsDebounced();
52735394 await getStatusOpen();
@@ -5329,6 +5450,9 @@ function toggleChatCompletionForms() {
53295450 else if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) {
53305451 $('#model_deepseek_select').trigger('change');
53315452 }
5453+ else if (oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI) {
5454+ $('#model_aimlapi_select').trigger('change');
5455+ }
53325456 else if (oai_settings.chat_completion_source == chat_completion_sources.XAI) {
53335457 $('#model_xai_select').trigger('change');
53345458 }
@@ -5478,6 +5602,8 @@ export function isImageInliningSupported() {
54785602 return visionSupportedModels.some(model => oai_settings.cohere_model.includes(model));
54795603 case chat_completion_sources.XAI:
54805604 return visionSupportedModels.some(model => oai_settings.xai_model.includes(model));
5605+ case chat_completion_sources.AIMLAPI:
5606+ return visionSupportedModels.some(model => oai_settings.aimlapi_model.includes(model));
54815607 case chat_completion_sources.POLLINATIONS:
54825608 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.pollinations_model)?.vision);
54835609 default:
@@ -6214,6 +6340,13 @@ export function initOpenAI() {
62146340 width: '100%',
62156341 templateResult: getOpenRouterModelTemplate,
62166342 });
6343+ $('#model_aimlapi_select').select2({
6344+ placeholder: t`Select a model`,
6345+ searchInputPlaceholder: t`Search models...`,
6346+ searchInputCssClass: 'text_pole',
6347+ width: '100%',
6348+ templateResult: getAimlapiModelTemplate,
6349+ });
62176350 }
62186351
62196352 $('#openrouter_providers_chat').on('change', function () {
@@ -6261,6 +6394,7 @@ export function initOpenAI() {
62616394 $('#model_nanogpt_select').on('change', onModelChange);
62626395 $('#model_deepseek_select').on('change', onModelChange);
62636396 $('#model_01ai_select').on('change', onModelChange);
6397+ $('#model_aimlapi_select').on('change', onModelChange);
62646398 $('#model_custom_select').on('change', onModelChange);
62656399 $('#model_xai_select').on('change', onModelChange);
62666400 $('#model_pollinations_select').on('change', onModelChange);
public/scripts/secrets.js+8 -1
@@ -1,5 +1,5 @@
11import { DOMPurify, moment } from '../lib.js';
22import { event_types, eventSource, getRequestHeaders } from '../script.js';
33import { t } from './i18n.js';
44import { chat_completion_sources } from './openai.js';
55import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
@@ -54,6 +54,7 @@ export const SECRET_KEYS = {
5454 GENERIC: 'api_key_generic',
5555 DEEPSEEK: 'api_key_deepseek',
5656 SERPER: 'api_key_serper',
57+ AIMLAPI: 'api_key_aimlapi',
5758 FALAI: 'api_key_falai',
5859 XAI: 'api_key_xai',
5960 VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',
@@ -102,6 +103,7 @@ const FRIENDLY_NAMES = {
102103 [SECRET_KEYS.SERPER]: 'Serper',
103104 [SECRET_KEYS.FALAI]: 'FAL.AI',
104105 [SECRET_KEYS.AZURE_TTS]: 'Azure TTS',
106+ [SECRET_KEYS.AIMLAPI]: 'AI/ML API',
105107};
106108
107109const INPUT_MAP = {
@@ -137,6 +139,7 @@ const INPUT_MAP = {
137139 [SECRET_KEYS.NANOGPT]: '#api_key_nanogpt',
138140 [SECRET_KEYS.GENERIC]: '#api_key_generic',
139141 [SECRET_KEYS.DEEPSEEK]: '#api_key_deepseek',
142+ [SECRET_KEYS.AIMLAPI]: '#api_key_aimlapi',
140143 [SECRET_KEYS.XAI]: '#api_key_xai',
141144 [SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]: '#vertexai_service_account_json',
142145};
@@ -303,6 +306,7 @@ export async function writeSecret(key, value, label) {
303306 // Clear the input field
304307 $(INPUT_MAP[key]).val('').trigger('input');
305308 await readSecretState();
309+ await eventSource.emit(event_types.SECRET_WRITTEN, key);
306310 return id;
307311 } catch (error) {
308312 console.error(`Could not write secret value: ${key}`, error);
@@ -327,6 +331,7 @@ export async function deleteSecret(key, id) {
327331 await readSecretState();
328332 // Force reconnection to the API with the new key
329333 $('#main_api').trigger('change');
334+ await eventSource.emit(event_types.SECRET_DELETED, key);
330335 }
331336 } catch (error) {
332337 console.error(`Could not delete secret value: ${key}`, error);
@@ -398,6 +403,7 @@ export async function rotateSecret(key, id) {
398403 await readSecretState();
399404 // Force reconnection to the API with the new key
400405 $('#main_api').trigger('change');
406+ await eventSource.emit(event_types.SECRET_ROTATED, key);
401407 }
402408 } catch (error) {
403409 console.error(`Could not rotate secret value: ${key}`, error);
@@ -420,6 +426,7 @@ export async function renameSecret(key, id, label) {
420426
421427 if (response.ok) {
422428 await readSecretState();
429+ await eventSource.emit(event_types.SECRET_EDITED, key);
423430 }
424431 } catch (error) {
425432 console.error(`Could not rename secret value: ${key}`, error);
public/scripts/slash-commands.js+1 -0
@@ -4244,6 +4244,7 @@ function getModelOptions(quiet) {
42444244 { id: 'model_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT },
42454245 { id: 'model_01ai_select', api: 'openai', type: chat_completion_sources.ZEROONEAI },
42464246 { id: 'model_deepseek_select', api: 'openai', type: chat_completion_sources.DEEPSEEK },
4247+ { id: 'model_aimlapi_select', api: 'openai', type: chat_completion_sources.AIMLAPI },
42474248 { id: 'model_xai_select', api: 'openai', type: chat_completion_sources.XAI },
42484249 { id: 'model_pollinations_select', api: 'openai', type: chat_completion_sources.POLLINATIONS },
42494250 { id: 'model_novel_select', api: 'novel', type: null },
public/scripts/tool-calling.js+1 -0
@@ -611,6 +611,7 @@ export class ToolManager {
611611 chat_completion_sources.MISTRALAI,
612612 chat_completion_sources.CLAUDE,
613613 chat_completion_sources.OPENROUTER,
614+ chat_completion_sources.AIMLAPI,
614615 chat_completion_sources.GROQ,
615616 chat_completion_sources.COHERE,
616617 chat_completion_sources.DEEPSEEK,
src/constants.js+6 -0
@@ -177,6 +177,7 @@ export const CHAT_COMPLETION_SOURCES = {
177177 ZEROONEAI: '01ai',
178178 NANOGPT: 'nanogpt',
179179 DEEPSEEK: 'deepseek',
180+ AIMLAPI: 'aimlapi',
180181 XAI: 'xai',
181182 POLLINATIONS: 'pollinations',
182183};
@@ -335,6 +336,11 @@ export const OPENROUTER_HEADERS = {
335336 'X-Title': 'SillyTavern',
336337};
337338
339+export const AIMLAPI_HEADERS = {
340+ 'HTTP-Referer': 'https://sillytavern.app',
341+ 'X-Title': 'SillyTavern',
342+};
343+
338344export const FEATHERLESS_HEADERS = {
339345 'HTTP-Referer': 'https://sillytavern.app',
340346 'X-Title': 'SillyTavern',
src/endpoints/backends/chat-completions.js+100 -0
@@ -4,6 +4,7 @@ import express from 'express';
44import fetch from 'node-fetch';
55
66import {
7+ AIMLAPI_HEADERS,
78 CHAT_COMPLETION_SOURCES,
89 GEMINI_SAFETY,
910 OPENROUTER_HEADERS,
@@ -61,6 +62,7 @@ const API_AI21 = 'https://api.ai21.com/studio/v1';
6162const API_NANOGPT = 'https://nano-gpt.com/api/v1';
6263const API_DEEPSEEK = 'https://api.deepseek.com/beta';
6364const API_XAI = 'https://api.x.ai/v1';
65+const API_AIMLAPI = 'https://api.aimlapi.com/v1';
6466const API_POLLINATIONS = 'https://text.pollinations.ai/openai';
6567
6668/**
@@ -1017,6 +1019,99 @@ async function sendXaiRequest(request, response) {
10171019 }
10181020}
10191021
1022+/**
1023+ * Sends a request to AI/ML API.
1024+ * @param {express.Request} request Express request
1025+ * @param {express.Response} response Express response
1026+ */
1027+async function sendAimlapiRequest(request, response) {
1028+ const apiUrl = API_AIMLAPI;
1029+ const apiKey = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);
1030+
1031+ if (!apiKey) {
1032+ console.warn('AI/ML API key is missing.');
1033+ return response.status(400).send({ error: true });
1034+ }
1035+
1036+ const controller = new AbortController();
1037+ request.socket.removeAllListeners('close');
1038+ request.socket.on('close', function () {
1039+ controller.abort();
1040+ });
1041+
1042+ try {
1043+ let bodyParams = {};
1044+
1045+ if (request.body.logprobs > 0) {
1046+ bodyParams['top_logprobs'] = request.body.logprobs;
1047+ bodyParams['logprobs'] = true;
1048+ }
1049+
1050+ if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
1051+ bodyParams['tools'] = request.body.tools;
1052+ bodyParams['tool_choice'] = request.body.tool_choice;
1053+ }
1054+
1055+ if (Array.isArray(request.body.stop) && request.body.stop.length > 0) {
1056+ bodyParams['stop'] = request.body.stop;
1057+ }
1058+
1059+ if (request.body.reasoning_effort) {
1060+ bodyParams['reasoning_effort'] = request.body.reasoning_effort;
1061+ }
1062+
1063+ const requestBody = {
1064+ 'messages': request.body.messages,
1065+ 'model': request.body.model,
1066+ 'temperature': request.body.temperature,
1067+ 'max_tokens': request.body.max_tokens,
1068+ 'stream': request.body.stream,
1069+ 'presence_penalty': request.body.presence_penalty,
1070+ 'frequency_penalty': request.body.frequency_penalty,
1071+ 'top_p': request.body.top_p,
1072+ 'seed': request.body.seed,
1073+ 'n': request.body.n,
1074+ ...bodyParams,
1075+ };
1076+
1077+ const config = {
1078+ method: 'POST',
1079+ headers: {
1080+ 'Content-Type': 'application/json',
1081+ 'Authorization': 'Bearer ' + apiKey,
1082+ ...AIMLAPI_HEADERS,
1083+ },
1084+ body: JSON.stringify(requestBody),
1085+ signal: controller.signal,
1086+ };
1087+
1088+ console.debug('AI/ML API request:', requestBody);
1089+
1090+ const generateResponse = await fetch(apiUrl + '/chat/completions', config);
1091+
1092+ if (request.body.stream) {
1093+ forwardFetchResponse(generateResponse, response);
1094+ } else {
1095+ if (!generateResponse.ok) {
1096+ const errorText = await generateResponse.text();
1097+ console.warn(`AI/ML API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
1098+ const errorJson = tryParse(errorText) ?? { error: true };
1099+ return response.status(500).send(errorJson);
1100+ }
1101+ const generateResponseJson = await generateResponse.json();
1102+ console.debug('AI/ML API response:', generateResponseJson);
1103+ return response.send(generateResponseJson);
1104+ }
1105+ } catch (error) {
1106+ console.error('Error communicating with AI/ML API: ', error);
1107+ if (!response.headersSent) {
1108+ response.send({ error: true });
1109+ } else {
1110+ response.end();
1111+ }
1112+ }
1113+}
1114+
10201115export const router = express.Router();
10211116
10221117router.post('/status', async function (request, statusResponse) {
@@ -1064,6 +1159,10 @@ router.post('/status', async function (request, statusResponse) {
10641159 apiUrl = new URL(request.body.reverse_proxy || API_XAI);
10651160 apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.XAI);
10661161 headers = {};
1162+ } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.AIMLAPI) {
1163+ apiUrl = API_AIMLAPI;
1164+ apiKey = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);
1165+ headers = { ...AIMLAPI_HEADERS };
10671166 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {
10681167 apiUrl = 'https://text.pollinations.ai';
10691168 apiKey = 'NONE';
@@ -1286,6 +1385,7 @@ router.post('/generate', function (request, response) {
12861385 case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);
12871386 case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);
12881387 case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);
1388+ case CHAT_COMPLETION_SOURCES.AIMLAPI: return sendAimlapiRequest(request, response);
12891389 case CHAT_COMPLETION_SOURCES.XAI: return sendXaiRequest(request, response);
12901390 }
12911391
src/endpoints/openai.js+10 -1
@@ -8,7 +8,7 @@ import express from 'express';
88import { getConfigValue, mergeObjectWithYaml, excludeKeysByYaml, trimV1 } from '../util.js';
99import { setAdditionalHeaders } from '../additional-headers.js';
1010import { readSecret, SECRET_KEYS } from './secrets.js';
1111import { AIMLAPI_HEADERS, OPENROUTER_HEADERS } from '../constants.js';
1212
1313export const router = express.Router();
1414
@@ -65,6 +65,10 @@ router.post('/caption-image', async (request, response) => {
6565 key = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);
6666 }
6767
68+ if (request.body.api === 'aimlapi') {
69+ key = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);
70+ }
71+
6872 if (request.body.api === 'groq') {
6973 key = readSecret(request.user.directories, SECRET_KEYS.GROQ);
7074 }
@@ -128,6 +132,11 @@ router.post('/caption-image', async (request, response) => {
128132 apiUrl = 'https://api.lingyiwanwu.com/v1/chat/completions';
129133 }
130134
135+ if (request.body.api === 'aimlapi') {
136+ apiUrl = 'https://api.aimlapi.com/v1/chat/completions';
137+ Object.assign(headers, AIMLAPI_HEADERS);
138+ }
139+
131140 if (request.body.api === 'groq') {
132141 apiUrl = 'https://api.groq.com/openai/v1/chat/completions';
133142 if (body.messages?.[0]?.role === 'system') {
src/endpoints/secrets.js+1 -0
@@ -54,6 +54,7 @@ export const SECRET_KEYS = {
5454 GENERIC: 'api_key_generic',
5555 DEEPSEEK: 'api_key_deepseek',
5656 SERPER: 'api_key_serper',
57+ AIMLAPI: 'api_key_aimlapi',
5758 XAI: 'api_key_xai',
5859 VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',
5960};
src/endpoints/stable-diffusion.js+87 -0
@@ -11,6 +11,7 @@ import _ from 'lodash';
1111
1212import { delay, getBasicAuthHeader, tryParse } from '../util.js';
1313import { readSecret, SECRET_KEYS } from './secrets.js';
14+import { AIMLAPI_HEADERS } from '../constants.js';
1415
1516/**
1617 * Gets the comfy workflows.
@@ -1336,3 +1337,89 @@ router.use('/nanogpt', nanogpt);
13361337router.use('/bfl', bfl);
13371338router.use('/falai', falai);
13381339router.use('/xai', xai);
1340+
1341+const aimlapi = express.Router();
1342+
1343+aimlapi.post('/models', async (request, response) => {
1344+ try {
1345+ const key = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);
1346+
1347+ if (!key) {
1348+ console.warn('AI/ML API key not found.');
1349+ return response.sendStatus(400);
1350+ }
1351+
1352+ const modelsResponse = await fetch('https://api.aimlapi.com/v1/models', {
1353+ method: 'GET',
1354+ headers: {
1355+ Authorization: `Bearer ${key}`,
1356+ },
1357+ });
1358+
1359+ if (!modelsResponse.ok) {
1360+ console.warn('AI/ML API returned an error.');
1361+ return response.sendStatus(500);
1362+ }
1363+
1364+ /** @type {any} */
1365+ const data = await modelsResponse.json();
1366+ const models = (data.data || [])
1367+ .filter(model =>
1368+ model.type === 'image' &&
1369+ model.id !== 'triposr' &&
1370+ model.id !== 'flux/dev/image-to-image',
1371+ )
1372+ .map(model => ({
1373+ value: model.id,
1374+ text: model.info?.name || model.id,
1375+ }));
1376+
1377+ return response.send({ data: models });
1378+ } catch (error) {
1379+ console.error(error);
1380+ return response.sendStatus(500);
1381+ }
1382+});
1383+
1384+aimlapi.post('/generate-image', async (req, res) => {
1385+ try {
1386+ const key = readSecret(req.user.directories, SECRET_KEYS.AIMLAPI);
1387+ if (!key) return res.sendStatus(400);
1388+
1389+ console.debug('AI/ML API image request:', req.body);
1390+
1391+ const apiRes = await fetch('https://api.aimlapi.com/v1/images/generations', {
1392+ method: 'POST',
1393+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}`, ...AIMLAPI_HEADERS },
1394+ body: JSON.stringify(req.body),
1395+ });
1396+ if (!apiRes.ok) {
1397+ const err = await apiRes.text();
1398+ return res.status(500).send(err);
1399+ }
1400+ /** @type {any} */
1401+ const data = await apiRes.json();
1402+
1403+ const imgObj = Array.isArray(data.images) ? data.images[0] : data.data?.[0];
1404+ if (!imgObj) return res.status(500).send('No image returned');
1405+
1406+ let base64;
1407+ if (imgObj.b64_json || imgObj.base64) {
1408+ base64 = imgObj.b64_json || imgObj.base64;
1409+ } else if (imgObj.url) {
1410+ const blobRes = await fetch(imgObj.url);
1411+ if (!blobRes.ok) throw new Error('Failed to fetch image URL');
1412+ const buffer = await blobRes.arrayBuffer();
1413+ base64 = Buffer.from(buffer).toString('base64');
1414+ } else {
1415+ throw new Error('Unsupported image format');
1416+ }
1417+
1418+ return res.json({ format: 'png', data: base64 });
1419+ } catch (e) {
1420+ console.error(e);
1421+ res.status(500).send('Internal error');
1422+ }
1423+});
1424+
1425+router.use('/aimlapi', aimlapi);