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

fece612f09ef66fef15aa7eeed889aef70c20e11

Dmitry <tumaskow@yandex.ru>

Signed
19 files changed, +542 -69Showing whitespace changes
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 @@
652 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="65536">652 <input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="65536">
653 </div>653 </div>
654 </div>654 </div>
655 <div class="range-block" data-source="openai,custom,xai">655 <div class="range-block" data-source="openai,custom,xai,aimlapi">
656 <div class="range-block-title" data-i18n="Multiple swipes per generation">656 <div class="range-block-title" data-i18n="Multiple swipes per generation">
657 Multiple swipes per generation657 Multiple swipes per generation
658 </div>658 </div>
@@ -691,7 +691,7 @@
691 </span>691 </span>
692 </div>692 </div>
693 </div>693 </div>
694 <div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai,pollinations">694 <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">
695 <div class="range-block-title" data-i18n="Temperature">695 <div class="range-block-title" data-i18n="Temperature">
696 Temperature696 Temperature
697 </div>697 </div>
@@ -704,7 +704,7 @@
704 </div>704 </div>
705 </div>705 </div>
706 </div>706 </div>
707 <div class="range-block" data-source="openai,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai,pollinations">707 <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai,pollinations">
708 <div class="range-block-title" data-i18n="Frequency Penalty">708 <div class="range-block-title" data-i18n="Frequency Penalty">
709 Frequency Penalty709 Frequency Penalty
710 </div>710 </div>
@@ -717,7 +717,7 @@
717 </div>717 </div>
718 </div>718 </div>
719 </div>719 </div>
720 <div class="range-block" data-source="openai,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai,pollinations">720 <div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai,pollinations">
721 <div class="range-block-title" data-i18n="Presence Penalty">721 <div class="range-block-title" data-i18n="Presence Penalty">
722 Presence Penalty722 Presence Penalty
723 </div>723 </div>
@@ -730,7 +730,7 @@
730 </div>730 </div>
731 </div>731 </div>
732 </div>732 </div>
733 <div class="range-block" data-source="claude,openrouter,makersuite,vertexai,cohere,perplexity">733 <div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity">
734 <div class="range-block-title" data-i18n="Top K">734 <div class="range-block-title" data-i18n="Top K">
735 Top K735 Top K
736 </div>736 </div>
@@ -743,7 +743,7 @@
743 </div>743 </div>
744 </div>744 </div>
745 </div>745 </div>
746 <div class="range-block" data-source="openai,claude,openrouter,ai21,scale,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai,pollinations">746 <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,scale,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai,pollinations">
747 <div class="range-block-title" data-i18n="Top P">747 <div class="range-block-title" data-i18n="Top P">
748 Top P748 Top P
749 </div>749 </div>
@@ -980,7 +980,7 @@
980 </div>980 </div>
981 </div>981 </div>
982 </div>982 </div>
983 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,nanogpt,xai,pollinations">983 <div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,nanogpt,xai,pollinations,aimlapi">
984 <div class="range-block-title justifyLeft" data-i18n="Seed">984 <div class="range-block-title justifyLeft" data-i18n="Seed">
985 Seed985 Seed
986 </div>986 </div>
@@ -1969,7 +1969,7 @@
1969 </span>1969 </span>
1970 </div>1970 </div>
1971 </div>1971 </div>
1972 <div class="range-block" data-source="makersuite,vertexai,openrouter,claude">1972 <div class="range-block" data-source="makersuite,vertexai,aimlapi,openrouter,claude">
1973 <label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">1973 <label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">
1974 <input id="openai_enable_web_search" type="checkbox" />1974 <input id="openai_enable_web_search" type="checkbox" />
1975 <span data-i18n="Enable web search">Enable web search</span>1975 <span data-i18n="Enable web search">Enable web search</span>
@@ -1983,7 +1983,7 @@
1983 </b>1983 </b>
1984 </div>1984 </div>
1985 </div>1985 </div>
1986 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek,makersuite,vertexai,ai21,xai,pollinations">1986 <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,deepseek,makersuite,vertexai,ai21,xai,pollinations">
1987 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">1987 <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
1988 <input id="openai_function_calling" type="checkbox" />1988 <input id="openai_function_calling" type="checkbox" />
1989 <span data-i18n="Enable function calling">Enable function calling</span>1989 <span data-i18n="Enable function calling">Enable function calling</span>
@@ -1994,7 +1994,7 @@
1994 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>1994 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>
1995 </div>1995 </div>
1996 </div>1996 </div>
1997 <div class="range-block" data-source="openai,openrouter,mistralai,makersuite,vertexai,claude,custom,01ai,xai,pollinations">1997 <div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,01ai,xai,pollinations">
1998 <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">1998 <label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
1999 <input id="openai_image_inlining" type="checkbox" />1999 <input id="openai_image_inlining" type="checkbox" />
2000 <span data-i18n="Send inline images">Send inline images</span>2000 <span data-i18n="Send inline images">Send inline images</span>
@@ -2064,7 +2064,7 @@
2064 </span>2064 </span>
2065 </div>2065 </div>
2066 </div>2066 </div>
2067 <div class="range-block" data-source="deepseek,openrouter,custom,claude,xai,makersuite,vertexai">2067 <div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai">
2068 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">2068 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
2069 <input id="openai_show_thoughts" type="checkbox" />2069 <input id="openai_show_thoughts" type="checkbox" />
2070 <span data-i18n="Request model reasoning">Request model reasoning</span>2070 <span data-i18n="Request model reasoning">Request model reasoning</span>
@@ -2078,7 +2078,7 @@
2078 </span>2078 </span>
2079 </div>2079 </div>
2080 </div>2080 </div>
2081 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,openrouter,pollinations">2081 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations">
2082 <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.">2082 <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.">
2083 <label for="openai_reasoning_effort">2083 <label for="openai_reasoning_effort">
2084 <span data-i18n="Reasoning Effort">Reasoning Effort</span>2084 <span data-i18n="Reasoning Effort">Reasoning Effort</span>
@@ -2092,7 +2092,7 @@
2092 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>2092 <option data-i18n="openai_reasoning_effort_high" value="high">High</option>
2093 <option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>2093 <option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>
2094 </select>2094 </select>
2095 <div class="toggle-description justifyLeft marginBot5" data-source="openai,custom,xai,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.">2095 <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.">
2096 OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.2096 OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.
2097 </div>2097 </div>
2098 <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.">2098 <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 @@
2131 </div>2131 </div>
2132 </div>2132 </div>
2133 </div>2133 </div>
2134 <div class="range-block m-t-1" data-source="openai,openrouter,scale,custom">2134 <div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,scale,custom">
2135 <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias">2135 <div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias">
2136 Logit Bias2136 Logit Bias
2137 </div>2137 </div>
@@ -2376,7 +2376,7 @@
2376 <input id="api_key_openrouter-tg" name="api_key_openrouter" class="text_pole flex1 api_key_openrouter" value="" type="text" autocomplete="off">2376 <input id="api_key_openrouter-tg" name="api_key_openrouter" class="text_pole flex1 api_key_openrouter" value="" type="text" autocomplete="off">
2377 <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>2377 <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>
2378 </div>2378 </div>
2379 <div data-for="api_key_openrouter" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">2379 <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'.">
2380 For privacy reasons, your API key will be hidden after you click 'Connect'.2380 For privacy reasons, your API key will be hidden after you click 'Connect'.
2381 </div>2381 </div>
2382 <div>2382 <div>
@@ -2789,6 +2789,7 @@
2789 <optgroup>2789 <optgroup>
2790 <option value="01ai">01.AI (Yi)</option>2790 <option value="01ai">01.AI (Yi)</option>
2791 <option value="ai21">AI21</option>2791 <option value="ai21">AI21</option>
2792 <option value="aimlapi">AI/ML API</option>
2792 <option value="claude">Claude</option>2793 <option value="claude">Claude</option>
2793 <option value="cohere">Cohere</option>2794 <option value="cohere">Cohere</option>
2794 <option value="deepseek">DeepSeek</option>2795 <option value="deepseek">DeepSeek</option>
@@ -3632,6 +3633,21 @@
3632 <option value="grok-beta">grok-beta</option>3633 <option value="grok-beta">grok-beta</option>
3633 </select>3634 </select>
3634 </div>3635 </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>
3635 <div id="pollinations_form" data-source="pollinations">3651 <div id="pollinations_form" data-source="pollinations">
3636 <h4 data-i18n="Pollinations Model">Pollinations Model</h4>3652 <h4 data-i18n="Pollinations Model">Pollinations Model</h4>
3637 <select id="model_pollinations_select">3653 <select id="model_pollinations_select">
public/script.js+24 -3
@@ -250,7 +250,20 @@ import {
250import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';250import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
251import { hideLoader, showLoader } from './scripts/loader.js';251import { hideLoader, showLoader } from './scripts/loader.js';
252import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';252import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';
253import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels, loadTabbyModels, loadGenericModels } from './scripts/textgen-models.js';253import {
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';
254import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities } from './scripts/chats.js';267import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities } from './scripts/chats.js';
255import { getPresetManager, initPresetManager } from './scripts/preset-manager.js';268import { getPresetManager, initPresetManager } from './scripts/preset-manager.js';
256import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js';269import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js';
@@ -542,6 +555,10 @@ export const event_types = {
542 TOOL_CALLS_PERFORMED: 'tool_calls_performed',555 TOOL_CALLS_PERFORMED: 'tool_calls_performed',
543 TOOL_CALLS_RENDERED: 'tool_calls_rendered',556 TOOL_CALLS_RENDERED: 'tool_calls_rendered',
544 CHARACTER_MANAGEMENT_DROPDOWN: 'charManagementDropdown',557 CHARACTER_MANAGEMENT_DROPDOWN: 'charManagementDropdown',
558 SECRET_WRITTEN: 'secret_written',
559 SECRET_DELETED: 'secret_deleted',
560 SECRET_ROTATED: 'secret_rotated',
561 SECRET_EDITED: 'secret_edited',
545};562};
546563
547export const eventSource = new EventEmitter([event_types.APP_READY]);564export const eventSource = new EventEmitter([event_types.APP_READY]);
@@ -2726,7 +2743,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
27262743
2727/**2744/**
2728 * Returns the URL of the avatar for the given character Id.2745 * Returns the URL of the avatar for the given character Id.
2729 * @param {number} characterId Character Id2746 * @param {number|string} characterId Character Id
2730 * @returns {string} Avatar URL2747 * @returns {string} Avatar URL
2731 */2748 */
2732export function getCharacterAvatar(characterId) {2749export function getCharacterAvatar(characterId) {
@@ -6108,7 +6125,11 @@ export function extractMessageFromData(data, activeApi = null) {
6108 case 'koboldhorde':6125 case 'koboldhorde':
6109 return data.text;6126 return data.text;
6110 case 'textgenerationwebui':6127 case 'textgenerationwebui':
6111 return data.choices?.[0]?.text ?? data.content ?? data.response ?? '';6128 return data.choices?.[0]?.text
6129 ?? data.choices?.[0]?.message?.content
6130 ?? data.content
6131 ?? data.response
6132 ?? '';
6112 case 'novel':6133 case 'novel':
6113 return data.output;6134 return data.output;
6114 case 'openai':6135 case 'openai':
public/scripts/RossAscends-mods.js+1 -0
@@ -412,6 +412,7 @@ function RA_autoconnect(PrevApi) {
412 || (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT)412 || (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT)
413 || (secret_state[SECRET_KEYS.DEEPSEEK] && oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK)413 || (secret_state[SECRET_KEYS.DEEPSEEK] && oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK)
414 || (secret_state[SECRET_KEYS.XAI] && oai_settings.chat_completion_source == chat_completion_sources.XAI)414 || (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)
415 || (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS)416 || (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS)
416 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)417 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
417 ) {418 ) {
public/scripts/extensions.js+1 -1
@@ -1535,7 +1535,7 @@ export async function runGenerationInterceptors(chat, contextSize, type) {
15351535
1536/**1536/**
1537 * Writes a field to the character's data extensions object.1537 * Writes a field to the character's data extensions object.
1538 * @param {number} characterId Index in the character array1538 * @param {number|string} characterId Index in the character array
1539 * @param {string} key Field name1539 * @param {string} key Field name
1540 * @param {any} value Field value1540 * @param {any} value Field value
1541 * @returns {Promise<void>} When the field is written1541 * @returns {Promise<void>} When the field is written
public/scripts/extensions/caption/index.js+1 -0
@@ -433,6 +433,7 @@ jQuery(async function () {
433 'zerooneai': SECRET_KEYS.ZEROONEAI,433 'zerooneai': SECRET_KEYS.ZEROONEAI,
434 'groq': SECRET_KEYS.GROQ,434 'groq': SECRET_KEYS.GROQ,
435 'cohere': SECRET_KEYS.COHERE,435 'cohere': SECRET_KEYS.COHERE,
436 'aimlapi': SECRET_KEYS.AIMLAPI,
436 };437 };
437438
438 if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {439 if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {
public/scripts/extensions/caption/settings.html+19 -0
@@ -18,6 +18,7 @@
18 <label for="caption_multimodal_api" data-i18n="API">API</label>18 <label for="caption_multimodal_api" data-i18n="API">API</label>
19 <select id="caption_multimodal_api" class="flex1 text_pole">19 <select id="caption_multimodal_api" class="flex1 text_pole">
20 <option value="zerooneai">01.AI (Yi)</option>20 <option value="zerooneai">01.AI (Yi)</option>
21 <option value="aimlapi">AI/ML API</option>
21 <option value="anthropic">Anthropic</option>22 <option value="anthropic">Anthropic</option>
22 <option value="cohere">Cohere</option>23 <option value="cohere">Cohere</option>
23 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>24 <option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
@@ -127,6 +128,24 @@
127 <option data-type="groq" value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>128 <option data-type="groq" value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
128 <option data-type="groq" value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>129 <option data-type="groq" value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
129 <option data-type="groq" value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>130 <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>
130 <option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>149 <option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
131 <option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>150 <option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
132 <option data-type="openrouter" value="openai/gpt-4o-2024-05-13">openai/gpt-4o-2024-05-13</option>151 <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) {
233 if (multimodalApi === 'custom' && !oai_settings.custom_url) {233 if (multimodalApi === 'custom' && !oai_settings.custom_url) {
234 throw new Error('Custom API URL is not set.');234 throw new Error('Custom API URL is not set.');
235 }235 }
236
237 if (multimodalApi === 'aimlapi' && !secret_state[SECRET_KEYS.AIMLAPI]) {
238 throw new Error('AI/ML API key is not set.');
239 }
236}240}
237241
238/**242/**
public/scripts/extensions/stable-diffusion/index.js+92 -41
@@ -41,7 +41,7 @@ import {
41 stringFormat,41 stringFormat,
42} from '../../utils.js';42} from '../../utils.js';
43import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js';43import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js';
44import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';44import { SECRET_KEYS, secret_state } from '../../secrets.js';
45import { getNovelAnlas, getNovelUnlimitedImageGeneration, loadNovelSubscriptionData } from '../../nai-settings.js';45import { getNovelAnlas, getNovelUnlimitedImageGeneration, loadNovelSubscriptionData } from '../../nai-settings.js';
46import { getMultimodalCaption } from '../shared.js';46import { getMultimodalCaption } from '../shared.js';
47import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';47import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
@@ -53,7 +53,7 @@ import {
53} from '../../slash-commands/SlashCommandArgument.js';53} from '../../slash-commands/SlashCommandArgument.js';
54import { debounce_timeout } from '../../constants.js';54import { debounce_timeout } from '../../constants.js';
55import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';55import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
56import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';56import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';
57import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';57import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
58import { ToolManager } from '../../tool-calling.js';58import { ToolManager } from '../../tool-calling.js';
59import { MacrosParser } from '../../macros.js';59import { MacrosParser } from '../../macros.js';
@@ -74,6 +74,7 @@ const sources = {
74 novel: 'novel',74 novel: 'novel',
75 vlad: 'vlad',75 vlad: 'vlad',
76 openai: 'openai',76 openai: 'openai',
77 aimlapi: 'aimlapi',
77 comfy: 'comfy',78 comfy: 'comfy',
78 togetherai: 'togetherai',79 togetherai: 'togetherai',
79 drawthings: 'drawthings',80 drawthings: 'drawthings',
@@ -1144,42 +1145,6 @@ function onComfyWorkflowChange() {
1144 saveSettingsDebounced();1145 saveSettingsDebounced();
1145}1146}
11461147
1147async 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
1171async function onStabilityKeyClick() {
1172 return onApiKeyClick('Stability AI API Key:', SECRET_KEYS.STABILITY);
1173}
1174
1175async function onBflKeyClick() {
1176 return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL);
1177}
1178
1179async function onFalaiKeyClick() {
1180 return onApiKeyClick('FALAI API Key:', SECRET_KEYS.FALAI);
1181}
1182
1183function onBflUpsamplingInput() {1148function onBflUpsamplingInput() {
1184 extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked');1149 extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked');
1185 saveSettingsDebounced();1150 saveSettingsDebounced();
@@ -1303,6 +1268,7 @@ async function onModelChange() {
1303 sources.horde,1268 sources.horde,
1304 sources.novel,1269 sources.novel,
1305 sources.openai,1270 sources.openai,
1271 sources.aimlapi,
1306 sources.togetherai,1272 sources.togetherai,
1307 sources.pollinations,1273 sources.pollinations,
1308 sources.stability,1274 sources.stability,
@@ -1505,6 +1471,9 @@ async function loadSamplers() {
1505 case sources.openai:1471 case sources.openai:
1506 samplers = ['N/A'];1472 samplers = ['N/A'];
1507 break;1473 break;
1474 case sources.aimlapi:
1475 samplers = ['N/A'];
1476 break;
1508 case sources.comfy:1477 case sources.comfy:
1509 samplers = await loadComfySamplers();1478 samplers = await loadComfySamplers();
1510 break;1479 break;
@@ -1695,6 +1664,9 @@ async function loadModels() {
1695 case sources.openai:1664 case sources.openai:
1696 models = await loadOpenAiModels();1665 models = await loadOpenAiModels();
1697 break;1666 break;
1667 case sources.aimlapi:
1668 models = await loadAimlapiModels();
1669 break;
1698 case sources.comfy:1670 case sources.comfy:
1699 models = await loadComfyModels();1671 models = await loadComfyModels();
1700 break;1672 break;
@@ -1959,6 +1931,23 @@ async function loadOpenAiModels() {
1959 ];1931 ];
1960}1932}
19611933
1934async 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
1962async function loadVladModels() {1951async function loadVladModels() {
1963 if (!extension_settings.sd.vlad_url) {1952 if (!extension_settings.sd.vlad_url) {
1964 return [];1953 return [];
@@ -2086,6 +2075,9 @@ async function loadSchedulers() {
2086 case sources.openai:2075 case sources.openai:
2087 schedulers = ['N/A'];2076 schedulers = ['N/A'];
2088 break;2077 break;
2078 case sources.aimlapi:
2079 schedulers = ['N/A'];
2080 break;
2089 case sources.togetherai:2081 case sources.togetherai:
2090 schedulers = ['N/A'];2082 schedulers = ['N/A'];
2091 break;2083 break;
@@ -2177,6 +2169,9 @@ async function loadVaes() {
2177 case sources.openai:2169 case sources.openai:
2178 vaes = ['N/A'];2170 vaes = ['N/A'];
2179 break;2171 break;
2172 case sources.aimlapi:
2173 vaes = ['N/A'];
2174 break;
2180 case sources.togetherai:2175 case sources.togetherai:
2181 vaes = ['N/A'];2176 vaes = ['N/A'];
2182 break;2177 break;
@@ -2749,6 +2744,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2749 case sources.openai:2744 case sources.openai:
2750 result = await generateOpenAiImage(prefixedPrompt, signal);2745 result = await generateOpenAiImage(prefixedPrompt, signal);
2751 break;2746 break;
2747 case sources.aimlapi:
2748 result = await generateAimlapiImage(prefixedPrompt, signal);
2749 break;
2752 case sources.comfy:2750 case sources.comfy:
2753 result = await generateComfyImage(prefixedPrompt, negativePrompt, signal);2751 result = await generateComfyImage(prefixedPrompt, negativePrompt, signal);
2754 break;2752 break;
@@ -3334,6 +3332,47 @@ async function generateOpenAiImage(prompt, signal) {
3334}3332}
33353333
3336/**3334/**
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 */
3341async 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/**
3337 * Generates an image in ComfyUI using the provided prompt and configuration settings.3376 * Generates an image in ComfyUI using the provided prompt and configuration settings.
3338 *3377 *
3339 * @param {string} prompt - The main instruction used to guide the image generation.3378 * @param {string} prompt - The main instruction used to guide the image generation.
@@ -3849,6 +3888,8 @@ function isValidState() {
3849 return secret_state[SECRET_KEYS.NOVEL];3888 return secret_state[SECRET_KEYS.NOVEL];
3850 case sources.openai:3889 case sources.openai:
3851 return secret_state[SECRET_KEYS.OPENAI];3890 return secret_state[SECRET_KEYS.OPENAI];
3891 case sources.aimlapi:
3892 return secret_state[SECRET_KEYS.AIMLAPI];
3852 case sources.comfy:3893 case sources.comfy:
3853 return true;3894 return true;
3854 case sources.togetherai:3895 case sources.togetherai:
@@ -4522,13 +4563,10 @@ jQuery(async () => {
4522 $('#sd_interactive_visible').on('input', onInteractiveVisibleInput);4563 $('#sd_interactive_visible').on('input', onInteractiveVisibleInput);
4523 $('#sd_tool_visible').on('input', onToolVisibleInput);4564 $('#sd_tool_visible').on('input', onToolVisibleInput);
4524 $('#sd_swap_dimensions').on('click', onSwapDimensionsClick);4565 $('#sd_swap_dimensions').on('click', onSwapDimensionsClick);
4525 $('#sd_stability_key').on('click', onStabilityKeyClick);
4526 $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);4566 $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);
4527 $('#sd_huggingface_model_id').on('input', onHFModelInput);4567 $('#sd_huggingface_model_id').on('input', onHFModelInput);
4528 $('#sd_function_tool').on('input', onFunctionToolInput);4568 $('#sd_function_tool').on('input', onFunctionToolInput);
4529 $('#sd_bfl_key').on('click', onBflKeyClick);
4530 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);4569 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);
4531 $('#sd_falai_key').on('click', onFalaiKeyClick);
45324570
4533 if (!CSS.supports('field-sizing', 'content')) {4571 if (!CSS.supports('field-sizing', 'content')) {
4534 $('.sd_settings .inline-drawer-toggle').on('click', function () {4572 $('.sd_settings .inline-drawer-toggle').on('click', function () {
@@ -4556,6 +4594,19 @@ jQuery(async () => {
45564594
4557 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);4595 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
4559 await loadSettings();4610 await loadSettings();
4560 $('body').addClass('sd');4611 $('body').addClass('sd');
45614612
public/scripts/extensions/stable-diffusion/settings.html+14 -4
@@ -37,6 +37,7 @@
37 </label>37 </label>
38 <label for="sd_source" data-i18n="Source">Source</label>38 <label for="sd_source" data-i18n="Source">Source</label>
39 <select id="sd_source">39 <select id="sd_source">
40 <option value="aimlapi">AI/ML API</option>
40 <option value="bfl">BFL (Black Forest Labs)</option>41 <option value="bfl">BFL (Black Forest Labs)</option>
41 <option value="comfy">ComfyUI</option>42 <option value="comfy">ComfyUI</option>
42 <option value="drawthings">DrawThings HTTP API</option>43 <option value="drawthings">DrawThings HTTP API</option>
@@ -139,7 +140,16 @@
139 </div>140 </div>
140 <i>Hint: Save an API key in the NovelAI API settings to use it here.</i>141 <i>Hint: Save an API key in the NovelAI API settings to use it here.</i>
141 </div>142 </div>
142 <div data-sd-source="openai">143 <div data-sd-source="aimlapi">
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">
143 <small data-i18n="These settings only apply to DALL-E 3">These settings only apply to DALL-E 3</small>153 <small data-i18n="These settings only apply to DALL-E 3">These settings only apply to DALL-E 3</small>
144 <div class="flex-container">154 <div class="flex-container">
145 <div class="flex1">155 <div class="flex1">
@@ -200,7 +210,7 @@
200 <div data-sd-source="stability">210 <div data-sd-source="stability">
201 <div class="flex-container flexnowrap alignItemsBaseline marginBot5">211 <div class="flex-container flexnowrap alignItemsBaseline marginBot5">
202 <strong class="flex1" data-i18n="API Key">API Key</strong>212 <strong class="flex1" data-i18n="API Key">API Key</strong>
203 <div id="sd_stability_key" class="menu_button menu_button_icon">213 <div id="sd_stability_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_stability">
204 <i class="fa-fw fa-solid fa-key"></i>214 <i class="fa-fw fa-solid fa-key"></i>
205 <span data-i18n="Click to set">Click to set</span>215 <span data-i18n="Click to set">Click to set</span>
206 </div>216 </div>
@@ -244,7 +254,7 @@
244 <i class="fa-solid fa-share-from-square"></i>254 <i class="fa-solid fa-share-from-square"></i>
245 </a>255 </a>
246 <span class="expander"></span>256 <span class="expander"></span>
247 <div id="sd_bfl_key" class="menu_button menu_button_icon">257 <div id="sd_bfl_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_bfl">
248 <i class="fa-fw fa-solid fa-key"></i>258 <i class="fa-fw fa-solid fa-key"></i>
249 <span data-i18n="Click to set">Click to set</span>259 <span data-i18n="Click to set">Click to set</span>
250 </div>260 </div>
@@ -264,7 +274,7 @@
264 <i class="fa-solid fa-share-from-square"></i>274 <i class="fa-solid fa-share-from-square"></i>
265 </a>275 </a>
266 <span class="expander"></span>276 <span class="expander"></span>
267 <div id="sd_falai_key" class="menu_button menu_button_icon">277 <div id="sd_falai_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_falai">
268 <i class="fa-fw fa-solid fa-key"></i>278 <i class="fa-fw fa-solid fa-key"></i>
269 <span data-i18n="Click to set">Click to set</span>279 <span data-i18n="Click to set">Click to set</span>
270 </div>280 </div>
public/scripts/openai.js+137 -3
@@ -185,6 +185,7 @@ export const chat_completion_sources = {
185 ZEROONEAI: '01ai',185 ZEROONEAI: '01ai',
186 NANOGPT: 'nanogpt',186 NANOGPT: 'nanogpt',
187 DEEPSEEK: 'deepseek',187 DEEPSEEK: 'deepseek',
188 AIMLAPI: 'aimlapi',
188 XAI: 'xai',189 XAI: 'xai',
189 POLLINATIONS: 'pollinations',190 POLLINATIONS: 'pollinations',
190};191};
@@ -273,6 +274,7 @@ export const settingsToUpdate = {
273 groq_model: ['#model_groq_select', 'groq_model', false, true],274 groq_model: ['#model_groq_select', 'groq_model', false, true],
274 nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true],275 nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true],
275 deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true],276 deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true],
277 aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true],
276 zerooneai_model: ['#model_01ai_select', 'zerooneai_model', false, true],278 zerooneai_model: ['#model_01ai_select', 'zerooneai_model', false, true],
277 xai_model: ['#model_xai_select', 'xai_model', false, true],279 xai_model: ['#model_xai_select', 'xai_model', false, true],
278 pollinations_model: ['#model_pollinations_select', 'pollinations_model', false, true],280 pollinations_model: ['#model_pollinations_select', 'pollinations_model', false, true],
@@ -369,6 +371,7 @@ const default_settings = {
369 nanogpt_model: 'gpt-4o-mini',371 nanogpt_model: 'gpt-4o-mini',
370 zerooneai_model: 'yi-large',372 zerooneai_model: 'yi-large',
371 deepseek_model: 'deepseek-chat',373 deepseek_model: 'deepseek-chat',
374 aimlapi_model: 'gpt-4o-mini-2024-07-18',
372 xai_model: 'grok-3-beta',375 xai_model: 'grok-3-beta',
373 pollinations_model: 'openai',376 pollinations_model: 'openai',
374 custom_model: '',377 custom_model: '',
@@ -456,6 +459,7 @@ const oai_settings = {
456 nanogpt_model: 'gpt-4o-mini',459 nanogpt_model: 'gpt-4o-mini',
457 zerooneai_model: 'yi-large',460 zerooneai_model: 'yi-large',
458 deepseek_model: 'deepseek-chat',461 deepseek_model: 'deepseek-chat',
462 aimlapi_model: 'gpt-4-turbo',
459 xai_model: 'grok-3-beta',463 xai_model: 'grok-3-beta',
460 pollinations_model: 'openai',464 pollinations_model: 'openai',
461 custom_model: '',465 custom_model: '',
@@ -1712,6 +1716,8 @@ export function getChatCompletionModel(source = null) {
1712 return oai_settings.nanogpt_model;1716 return oai_settings.nanogpt_model;
1713 case chat_completion_sources.DEEPSEEK:1717 case chat_completion_sources.DEEPSEEK:
1714 return oai_settings.deepseek_model;1718 return oai_settings.deepseek_model;
1719 case chat_completion_sources.AIMLAPI:
1720 return oai_settings.aimlapi_model;
1715 case chat_completion_sources.XAI:1721 case chat_completion_sources.XAI:
1716 return oai_settings.xai_model;1722 return oai_settings.xai_model;
1717 case chat_completion_sources.POLLINATIONS:1723 case chat_completion_sources.POLLINATIONS:
@@ -1836,6 +1842,19 @@ function saveModelList(data) {
1836 $('#model_01ai_select').val(oai_settings.zerooneai_model).trigger('change');1842 $('#model_01ai_select').val(oai_settings.zerooneai_model).trigger('change');
1837 }1843 }
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
1839 if (oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) {1858 if (oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) {
1840 /** @type {HTMLSelectElement} */1859 /** @type {HTMLSelectElement} */
1841 const mistralModelSelect = document.querySelector('#model_mistralai_select');1860 const mistralModelSelect = document.querySelector('#model_mistralai_select');
@@ -2011,6 +2030,56 @@ function openRouterGroupByVendor(array) {
2011 }, new Map());2030 }, new Map());
2012}2031}
20132032
2033function 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
2047function 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
2067function 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
2014async function sendAltScaleRequest(messages, logit_bias, signal, type) {2083async function sendAltScaleRequest(messages, logit_bias, signal, type) {
2015 const generate_url = '/api/backends/scale-alt/generate';2084 const generate_url = '/api/backends/scale-alt/generate';
20162085
@@ -2065,6 +2134,7 @@ function getReasoningEffort() {
2065 chat_completion_sources.OPENAI,2134 chat_completion_sources.OPENAI,
2066 chat_completion_sources.CUSTOM,2135 chat_completion_sources.CUSTOM,
2067 chat_completion_sources.XAI,2136 chat_completion_sources.XAI,
2137 chat_completion_sources.AIMLAPI,
2068 chat_completion_sources.OPENROUTER,2138 chat_completion_sources.OPENROUTER,
2069 chat_completion_sources.POLLINATIONS,2139 chat_completion_sources.POLLINATIONS,
2070 ];2140 ];
@@ -2122,6 +2192,7 @@ async function sendOpenAIRequest(type, messages, signal) {
2122 const is01AI = oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI;2192 const is01AI = oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI;
2123 const isNano = oai_settings.chat_completion_source == chat_completion_sources.NANOGPT;2193 const isNano = oai_settings.chat_completion_source == chat_completion_sources.NANOGPT;
2124 const isDeepSeek = oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK;2194 const isDeepSeek = oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK;
2195 const isAimlapi = oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI;
2125 const isXAI = oai_settings.chat_completion_source == chat_completion_sources.XAI;2196 const isXAI = oai_settings.chat_completion_source == chat_completion_sources.XAI;
2126 const isPollinations = oai_settings.chat_completion_source == chat_completion_sources.POLLINATIONS;2197 const isPollinations = oai_settings.chat_completion_source == chat_completion_sources.POLLINATIONS;
2127 const isTextCompletion = isOAI && textCompletionModels.includes(oai_settings.openai_model);2198 const isTextCompletion = isOAI && textCompletionModels.includes(oai_settings.openai_model);
@@ -2130,7 +2201,7 @@ async function sendOpenAIRequest(type, messages, signal) {
2130 const isContinue = type === 'continue';2201 const isContinue = type === 'continue';
2131 const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isOAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model));2202 const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isOAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model));
2132 const useLogprobs = !!power_user.request_token_probabilities;2203 const useLogprobs = !!power_user.request_token_probabilities;
2133 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom || isXAI);2204 const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom || isXAI || isAimlapi);
21342205
2135 // If we're using the window.ai extension, use that instead2206 // If we're using the window.ai extension, use that instead
2136 // Doesn't support logit bias yet2207 // Doesn't support logit bias yet
@@ -2196,7 +2267,7 @@ async function sendOpenAIRequest(type, messages, signal) {
2196 }2267 }
21972268
2198 // Add logprobs request (currently OpenAI only, max 5 on their side)2269 // Add logprobs request (currently OpenAI only, max 5 on their side)
2199 if (useLogprobs && (isOAI || isCustom || isDeepSeek || isXAI)) {2270 if (useLogprobs && (isOAI || isCustom || isDeepSeek || isXAI || isAimlapi)) {
2200 generate_data['logprobs'] = 5;2271 generate_data['logprobs'] = 5;
2201 }2272 }
22022273
@@ -2334,7 +2405,7 @@ async function sendOpenAIRequest(type, messages, signal) {
2334 delete generate_data.max_tokens;2405 delete generate_data.max_tokens;
2335 }2406 }
23362407
2337 if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano || isXAI || isPollinations) && oai_settings.seed >= 0) {2408 if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano || isXAI || isPollinations || isAimlapi) && oai_settings.seed >= 0) {
2338 generate_data['seed'] = oai_settings.seed;2409 generate_data['seed'] = oai_settings.seed;
2339 }2410 }
23402411
@@ -3461,6 +3532,7 @@ function loadOpenAISettings(data, settings) {
3461 oai_settings.groq_model = settings.groq_model ?? default_settings.groq_model;3532 oai_settings.groq_model = settings.groq_model ?? default_settings.groq_model;
3462 oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model;3533 oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model;
3463 oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model;3534 oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model;
3535 oai_settings.aimlapi_model = settings.aimlapi_model ?? default_settings.aimlapi_model;
3464 oai_settings.zerooneai_model = settings.zerooneai_model ?? default_settings.zerooneai_model;3536 oai_settings.zerooneai_model = settings.zerooneai_model ?? default_settings.zerooneai_model;
3465 oai_settings.xai_model = settings.xai_model ?? default_settings.xai_model;3537 oai_settings.xai_model = settings.xai_model ?? default_settings.xai_model;
3466 oai_settings.pollinations_model = settings.pollinations_model ?? default_settings.pollinations_model;3538 oai_settings.pollinations_model = settings.pollinations_model ?? default_settings.pollinations_model;
@@ -3558,6 +3630,8 @@ function loadOpenAISettings(data, settings) {
3558 $('#model_deepseek_select').val(oai_settings.deepseek_model);3630 $('#model_deepseek_select').val(oai_settings.deepseek_model);
3559 $(`#model_deepseek_select option[value="${oai_settings.deepseek_model}"`).prop('selected', true);3631 $(`#model_deepseek_select option[value="${oai_settings.deepseek_model}"`).prop('selected', true);
3560 $('#model_01ai_select').val(oai_settings.zerooneai_model);3632 $('#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);
3561 $('#model_xai_select').val(oai_settings.xai_model);3635 $('#model_xai_select').val(oai_settings.xai_model);
3562 $(`#model_xai_select option[value="${oai_settings.xai_model}"`).prop('selected', true);3636 $(`#model_xai_select option[value="${oai_settings.xai_model}"`).prop('selected', true);
3563 $('#model_pollinations_select').val(oai_settings.pollinations_model);3637 $('#model_pollinations_select').val(oai_settings.pollinations_model);
@@ -3861,6 +3935,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
3861 zerooneai_model: settings.zerooneai_model,3935 zerooneai_model: settings.zerooneai_model,
3862 xai_model: settings.xai_model,3936 xai_model: settings.xai_model,
3863 pollinations_model: settings.pollinations_model,3937 pollinations_model: settings.pollinations_model,
3938 aimlapi_model: settings.aimlapi_model,
3864 custom_model: settings.custom_model,3939 custom_model: settings.custom_model,
3865 custom_url: settings.custom_url,3940 custom_url: settings.custom_url,
3866 custom_include_body: settings.custom_include_body,3941 custom_include_body: settings.custom_include_body,
@@ -4709,6 +4784,15 @@ async function onModelChange() {
4709 oai_settings.pollinations_model = value;4784 oai_settings.pollinations_model = value;
4710 }4785 }
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
4712 if ($(this).is('#model_xai_select')) {4796 if ($(this).is('#model_xai_select')) {
4713 console.log('XAI model changed to', value);4797 console.log('XAI model changed to', value);
4714 oai_settings.xai_model = value;4798 oai_settings.xai_model = value;
@@ -4995,6 +5079,30 @@ async function onModelChange() {
4995 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');5079 $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
4996 }5080 }
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
4998 if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {5106 if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
4999 oai_settings.pres_pen_openai = Math.min(Math.max(0, oai_settings.pres_pen_openai), 1);5107 oai_settings.pres_pen_openai = Math.min(Math.max(0, oai_settings.pres_pen_openai), 1);
5000 $('#pres_pen_openai').attr('max', 1).attr('min', 0).val(oai_settings.pres_pen_openai).trigger('input');5108 $('#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) {
5268 }5376 }
5269 }5377 }
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
5271 startStatusLoading();5392 startStatusLoading();
5272 saveSettingsDebounced();5393 saveSettingsDebounced();
5273 await getStatusOpen();5394 await getStatusOpen();
@@ -5329,6 +5450,9 @@ function toggleChatCompletionForms() {
5329 else if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) {5450 else if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) {
5330 $('#model_deepseek_select').trigger('change');5451 $('#model_deepseek_select').trigger('change');
5331 }5452 }
5453 else if (oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI) {
5454 $('#model_aimlapi_select').trigger('change');
5455 }
5332 else if (oai_settings.chat_completion_source == chat_completion_sources.XAI) {5456 else if (oai_settings.chat_completion_source == chat_completion_sources.XAI) {
5333 $('#model_xai_select').trigger('change');5457 $('#model_xai_select').trigger('change');
5334 }5458 }
@@ -5478,6 +5602,8 @@ export function isImageInliningSupported() {
5478 return visionSupportedModels.some(model => oai_settings.cohere_model.includes(model));5602 return visionSupportedModels.some(model => oai_settings.cohere_model.includes(model));
5479 case chat_completion_sources.XAI:5603 case chat_completion_sources.XAI:
5480 return visionSupportedModels.some(model => oai_settings.xai_model.includes(model));5604 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));
5481 case chat_completion_sources.POLLINATIONS:5607 case chat_completion_sources.POLLINATIONS:
5482 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.pollinations_model)?.vision);5608 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.pollinations_model)?.vision);
5483 default:5609 default:
@@ -6214,6 +6340,13 @@ export function initOpenAI() {
6214 width: '100%',6340 width: '100%',
6215 templateResult: getOpenRouterModelTemplate,6341 templateResult: getOpenRouterModelTemplate,
6216 });6342 });
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 });
6217 }6350 }
62186351
6219 $('#openrouter_providers_chat').on('change', function () {6352 $('#openrouter_providers_chat').on('change', function () {
@@ -6261,6 +6394,7 @@ export function initOpenAI() {
6261 $('#model_nanogpt_select').on('change', onModelChange);6394 $('#model_nanogpt_select').on('change', onModelChange);
6262 $('#model_deepseek_select').on('change', onModelChange);6395 $('#model_deepseek_select').on('change', onModelChange);
6263 $('#model_01ai_select').on('change', onModelChange);6396 $('#model_01ai_select').on('change', onModelChange);
6397 $('#model_aimlapi_select').on('change', onModelChange);
6264 $('#model_custom_select').on('change', onModelChange);6398 $('#model_custom_select').on('change', onModelChange);
6265 $('#model_xai_select').on('change', onModelChange);6399 $('#model_xai_select').on('change', onModelChange);
6266 $('#model_pollinations_select').on('change', onModelChange);6400 $('#model_pollinations_select').on('change', onModelChange);
public/scripts/secrets.js+8 -1
@@ -1,5 +1,5 @@
1import { DOMPurify, moment } from '../lib.js';1import { DOMPurify, moment } from '../lib.js';
2import { getRequestHeaders } from '../script.js';2import { event_types, eventSource, getRequestHeaders } from '../script.js';
3import { t } from './i18n.js';3import { t } from './i18n.js';
4import { chat_completion_sources } from './openai.js';4import { chat_completion_sources } from './openai.js';
5import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';5import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
@@ -54,6 +54,7 @@ export const SECRET_KEYS = {
54 GENERIC: 'api_key_generic',54 GENERIC: 'api_key_generic',
55 DEEPSEEK: 'api_key_deepseek',55 DEEPSEEK: 'api_key_deepseek',
56 SERPER: 'api_key_serper',56 SERPER: 'api_key_serper',
57 AIMLAPI: 'api_key_aimlapi',
57 FALAI: 'api_key_falai',58 FALAI: 'api_key_falai',
58 XAI: 'api_key_xai',59 XAI: 'api_key_xai',
59 VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',60 VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',
@@ -102,6 +103,7 @@ const FRIENDLY_NAMES = {
102 [SECRET_KEYS.SERPER]: 'Serper',103 [SECRET_KEYS.SERPER]: 'Serper',
103 [SECRET_KEYS.FALAI]: 'FAL.AI',104 [SECRET_KEYS.FALAI]: 'FAL.AI',
104 [SECRET_KEYS.AZURE_TTS]: 'Azure TTS',105 [SECRET_KEYS.AZURE_TTS]: 'Azure TTS',
106 [SECRET_KEYS.AIMLAPI]: 'AI/ML API',
105};107};
106108
107const INPUT_MAP = {109const INPUT_MAP = {
@@ -137,6 +139,7 @@ const INPUT_MAP = {
137 [SECRET_KEYS.NANOGPT]: '#api_key_nanogpt',139 [SECRET_KEYS.NANOGPT]: '#api_key_nanogpt',
138 [SECRET_KEYS.GENERIC]: '#api_key_generic',140 [SECRET_KEYS.GENERIC]: '#api_key_generic',
139 [SECRET_KEYS.DEEPSEEK]: '#api_key_deepseek',141 [SECRET_KEYS.DEEPSEEK]: '#api_key_deepseek',
142 [SECRET_KEYS.AIMLAPI]: '#api_key_aimlapi',
140 [SECRET_KEYS.XAI]: '#api_key_xai',143 [SECRET_KEYS.XAI]: '#api_key_xai',
141 [SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]: '#vertexai_service_account_json',144 [SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]: '#vertexai_service_account_json',
142};145};
@@ -303,6 +306,7 @@ export async function writeSecret(key, value, label) {
303 // Clear the input field306 // Clear the input field
304 $(INPUT_MAP[key]).val('').trigger('input');307 $(INPUT_MAP[key]).val('').trigger('input');
305 await readSecretState();308 await readSecretState();
309 await eventSource.emit(event_types.SECRET_WRITTEN, key);
306 return id;310 return id;
307 } catch (error) {311 } catch (error) {
308 console.error(`Could not write secret value: ${key}`, error);312 console.error(`Could not write secret value: ${key}`, error);
@@ -327,6 +331,7 @@ export async function deleteSecret(key, id) {
327 await readSecretState();331 await readSecretState();
328 // Force reconnection to the API with the new key332 // Force reconnection to the API with the new key
329 $('#main_api').trigger('change');333 $('#main_api').trigger('change');
334 await eventSource.emit(event_types.SECRET_DELETED, key);
330 }335 }
331 } catch (error) {336 } catch (error) {
332 console.error(`Could not delete secret value: ${key}`, error);337 console.error(`Could not delete secret value: ${key}`, error);
@@ -398,6 +403,7 @@ export async function rotateSecret(key, id) {
398 await readSecretState();403 await readSecretState();
399 // Force reconnection to the API with the new key404 // Force reconnection to the API with the new key
400 $('#main_api').trigger('change');405 $('#main_api').trigger('change');
406 await eventSource.emit(event_types.SECRET_ROTATED, key);
401 }407 }
402 } catch (error) {408 } catch (error) {
403 console.error(`Could not rotate secret value: ${key}`, error);409 console.error(`Could not rotate secret value: ${key}`, error);
@@ -420,6 +426,7 @@ export async function renameSecret(key, id, label) {
420426
421 if (response.ok) {427 if (response.ok) {
422 await readSecretState();428 await readSecretState();
429 await eventSource.emit(event_types.SECRET_EDITED, key);
423 }430 }
424 } catch (error) {431 } catch (error) {
425 console.error(`Could not rename secret value: ${key}`, error);432 console.error(`Could not rename secret value: ${key}`, error);
public/scripts/slash-commands.js+1 -0
@@ -4244,6 +4244,7 @@ function getModelOptions(quiet) {
4244 { id: 'model_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT },4244 { id: 'model_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT },
4245 { id: 'model_01ai_select', api: 'openai', type: chat_completion_sources.ZEROONEAI },4245 { id: 'model_01ai_select', api: 'openai', type: chat_completion_sources.ZEROONEAI },
4246 { id: 'model_deepseek_select', api: 'openai', type: chat_completion_sources.DEEPSEEK },4246 { id: 'model_deepseek_select', api: 'openai', type: chat_completion_sources.DEEPSEEK },
4247 { id: 'model_aimlapi_select', api: 'openai', type: chat_completion_sources.AIMLAPI },
4247 { id: 'model_xai_select', api: 'openai', type: chat_completion_sources.XAI },4248 { id: 'model_xai_select', api: 'openai', type: chat_completion_sources.XAI },
4248 { id: 'model_pollinations_select', api: 'openai', type: chat_completion_sources.POLLINATIONS },4249 { id: 'model_pollinations_select', api: 'openai', type: chat_completion_sources.POLLINATIONS },
4249 { id: 'model_novel_select', api: 'novel', type: null },4250 { id: 'model_novel_select', api: 'novel', type: null },
public/scripts/tool-calling.js+1 -0
@@ -611,6 +611,7 @@ export class ToolManager {
611 chat_completion_sources.MISTRALAI,611 chat_completion_sources.MISTRALAI,
612 chat_completion_sources.CLAUDE,612 chat_completion_sources.CLAUDE,
613 chat_completion_sources.OPENROUTER,613 chat_completion_sources.OPENROUTER,
614 chat_completion_sources.AIMLAPI,
614 chat_completion_sources.GROQ,615 chat_completion_sources.GROQ,
615 chat_completion_sources.COHERE,616 chat_completion_sources.COHERE,
616 chat_completion_sources.DEEPSEEK,617 chat_completion_sources.DEEPSEEK,
src/constants.js+6 -0
@@ -177,6 +177,7 @@ export const CHAT_COMPLETION_SOURCES = {
177 ZEROONEAI: '01ai',177 ZEROONEAI: '01ai',
178 NANOGPT: 'nanogpt',178 NANOGPT: 'nanogpt',
179 DEEPSEEK: 'deepseek',179 DEEPSEEK: 'deepseek',
180 AIMLAPI: 'aimlapi',
180 XAI: 'xai',181 XAI: 'xai',
181 POLLINATIONS: 'pollinations',182 POLLINATIONS: 'pollinations',
182};183};
@@ -335,6 +336,11 @@ export const OPENROUTER_HEADERS = {
335 'X-Title': 'SillyTavern',336 'X-Title': 'SillyTavern',
336};337};
337338
339export const AIMLAPI_HEADERS = {
340 'HTTP-Referer': 'https://sillytavern.app',
341 'X-Title': 'SillyTavern',
342};
343
338export const FEATHERLESS_HEADERS = {344export const FEATHERLESS_HEADERS = {
339 'HTTP-Referer': 'https://sillytavern.app',345 'HTTP-Referer': 'https://sillytavern.app',
340 'X-Title': 'SillyTavern',346 'X-Title': 'SillyTavern',
src/endpoints/backends/chat-completions.js+100 -0
@@ -4,6 +4,7 @@ import express from 'express';
4import fetch from 'node-fetch';4import fetch from 'node-fetch';
55
6import {6import {
7 AIMLAPI_HEADERS,
7 CHAT_COMPLETION_SOURCES,8 CHAT_COMPLETION_SOURCES,
8 GEMINI_SAFETY,9 GEMINI_SAFETY,
9 OPENROUTER_HEADERS,10 OPENROUTER_HEADERS,
@@ -61,6 +62,7 @@ const API_AI21 = 'https://api.ai21.com/studio/v1';
61const API_NANOGPT = 'https://nano-gpt.com/api/v1';62const API_NANOGPT = 'https://nano-gpt.com/api/v1';
62const API_DEEPSEEK = 'https://api.deepseek.com/beta';63const API_DEEPSEEK = 'https://api.deepseek.com/beta';
63const API_XAI = 'https://api.x.ai/v1';64const API_XAI = 'https://api.x.ai/v1';
65const API_AIMLAPI = 'https://api.aimlapi.com/v1';
64const API_POLLINATIONS = 'https://text.pollinations.ai/openai';66const API_POLLINATIONS = 'https://text.pollinations.ai/openai';
6567
66/**68/**
@@ -1017,6 +1019,99 @@ async function sendXaiRequest(request, response) {
1017 }1019 }
1018}1020}
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 */
1027async 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
1020export const router = express.Router();1115export const router = express.Router();
10211116
1022router.post('/status', async function (request, statusResponse) {1117router.post('/status', async function (request, statusResponse) {
@@ -1064,6 +1159,10 @@ router.post('/status', async function (request, statusResponse) {
1064 apiUrl = new URL(request.body.reverse_proxy || API_XAI);1159 apiUrl = new URL(request.body.reverse_proxy || API_XAI);
1065 apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.XAI);1160 apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.XAI);
1066 headers = {};1161 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 };
1067 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {1166 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {
1068 apiUrl = 'https://text.pollinations.ai';1167 apiUrl = 'https://text.pollinations.ai';
1069 apiKey = 'NONE';1168 apiKey = 'NONE';
@@ -1286,6 +1385,7 @@ router.post('/generate', function (request, response) {
1286 case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);1385 case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);
1287 case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);1386 case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);
1288 case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);1387 case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);
1388 case CHAT_COMPLETION_SOURCES.AIMLAPI: return sendAimlapiRequest(request, response);
1289 case CHAT_COMPLETION_SOURCES.XAI: return sendXaiRequest(request, response);1389 case CHAT_COMPLETION_SOURCES.XAI: return sendXaiRequest(request, response);
1290 }1390 }
12911391
src/endpoints/openai.js+10 -1
@@ -8,7 +8,7 @@ import express from 'express';
8import { getConfigValue, mergeObjectWithYaml, excludeKeysByYaml, trimV1 } from '../util.js';8import { getConfigValue, mergeObjectWithYaml, excludeKeysByYaml, trimV1 } from '../util.js';
9import { setAdditionalHeaders } from '../additional-headers.js';9import { setAdditionalHeaders } from '../additional-headers.js';
10import { readSecret, SECRET_KEYS } from './secrets.js';10import { readSecret, SECRET_KEYS } from './secrets.js';
11import { OPENROUTER_HEADERS } from '../constants.js';11import { AIMLAPI_HEADERS, OPENROUTER_HEADERS } from '../constants.js';
1212
13export const router = express.Router();13export const router = express.Router();
1414
@@ -65,6 +65,10 @@ router.post('/caption-image', async (request, response) => {
65 key = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);65 key = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);
66 }66 }
6767
68 if (request.body.api === 'aimlapi') {
69 key = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);
70 }
71
68 if (request.body.api === 'groq') {72 if (request.body.api === 'groq') {
69 key = readSecret(request.user.directories, SECRET_KEYS.GROQ);73 key = readSecret(request.user.directories, SECRET_KEYS.GROQ);
70 }74 }
@@ -128,6 +132,11 @@ router.post('/caption-image', async (request, response) => {
128 apiUrl = 'https://api.lingyiwanwu.com/v1/chat/completions';132 apiUrl = 'https://api.lingyiwanwu.com/v1/chat/completions';
129 }133 }
130134
135 if (request.body.api === 'aimlapi') {
136 apiUrl = 'https://api.aimlapi.com/v1/chat/completions';
137 Object.assign(headers, AIMLAPI_HEADERS);
138 }
139
131 if (request.body.api === 'groq') {140 if (request.body.api === 'groq') {
132 apiUrl = 'https://api.groq.com/openai/v1/chat/completions';141 apiUrl = 'https://api.groq.com/openai/v1/chat/completions';
133 if (body.messages?.[0]?.role === 'system') {142 if (body.messages?.[0]?.role === 'system') {
src/endpoints/secrets.js+1 -0
@@ -54,6 +54,7 @@ export const SECRET_KEYS = {
54 GENERIC: 'api_key_generic',54 GENERIC: 'api_key_generic',
55 DEEPSEEK: 'api_key_deepseek',55 DEEPSEEK: 'api_key_deepseek',
56 SERPER: 'api_key_serper',56 SERPER: 'api_key_serper',
57 AIMLAPI: 'api_key_aimlapi',
57 XAI: 'api_key_xai',58 XAI: 'api_key_xai',
58 VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',59 VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',
59};60};
src/endpoints/stable-diffusion.js+87 -0
@@ -11,6 +11,7 @@ import _ from 'lodash';
1111
12import { delay, getBasicAuthHeader, tryParse } from '../util.js';12import { delay, getBasicAuthHeader, tryParse } from '../util.js';
13import { readSecret, SECRET_KEYS } from './secrets.js';13import { readSecret, SECRET_KEYS } from './secrets.js';
14import { AIMLAPI_HEADERS } from '../constants.js';
1415
15/**16/**
16 * Gets the comfy workflows.17 * Gets the comfy workflows.
@@ -1336,3 +1337,89 @@ router.use('/nanogpt', nanogpt);
1336router.use('/bfl', bfl);1337router.use('/bfl', bfl);
1337router.use('/falai', falai);1338router.use('/falai', falai);
1338router.use('/xai', xai);1339router.use('/xai', xai);
1340
1341const aimlapi = express.Router();
1342
1343aimlapi.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
1384aimlapi.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
1425router.use('/aimlapi', aimlapi);