Merge pull request #4135 from D1m7asis/release

feat: Added AI/ML API Provider Support
This commit is contained in:
Dmitry
2025-06-13 19:59:18 +02:00
committed by GitHub
parent 33b9370675
commit fece612f09
19 changed files with 542 additions and 69 deletions
+4
View File
@@ -0,0 +1,4 @@
<svg width="24" height="21" viewBox="0 0 24 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<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" />
<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"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+31 -15
View File
@@ -652,7 +652,7 @@
<input type="number" id="openai_max_tokens" name="openai_max_tokens" class="text_pole" min="1" max="65536">
</div>
</div>
<div class="range-block" data-source="openai,custom,xai">
<div class="range-block" data-source="openai,custom,xai,aimlapi">
<div class="range-block-title" data-i18n="Multiple swipes per generation">
Multiple swipes per generation
</div>
@@ -691,7 +691,7 @@
</span>
</div>
</div>
<div class="range-block" data-source="openai,claude,windowai,openrouter,ai21,scale,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai,pollinations">
<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">
<div class="range-block-title" data-i18n="Temperature">
Temperature
</div>
@@ -704,7 +704,7 @@
</div>
</div>
</div>
<div class="range-block" data-source="openai,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai,pollinations">
<div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai,pollinations">
<div class="range-block-title" data-i18n="Frequency Penalty">
Frequency Penalty
</div>
@@ -717,7 +717,7 @@
</div>
</div>
</div>
<div class="range-block" data-source="openai,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai,pollinations">
<div class="range-block" data-source="openai,aimlapi,openrouter,custom,cohere,perplexity,groq,mistralai,nanogpt,deepseek,xai,pollinations">
<div class="range-block-title" data-i18n="Presence Penalty">
Presence Penalty
</div>
@@ -730,7 +730,7 @@
</div>
</div>
</div>
<div class="range-block" data-source="claude,openrouter,makersuite,vertexai,cohere,perplexity">
<div class="range-block" data-source="claude,aimlapi,openrouter,makersuite,vertexai,cohere,perplexity">
<div class="range-block-title" data-i18n="Top K">
Top K
</div>
@@ -743,7 +743,7 @@
</div>
</div>
</div>
<div class="range-block" data-source="openai,claude,openrouter,ai21,scale,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai,pollinations">
<div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,scale,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,01ai,nanogpt,deepseek,xai,pollinations">
<div class="range-block-title" data-i18n="Top P">
Top P
</div>
@@ -980,7 +980,7 @@
</div>
</div>
</div>
<div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,nanogpt,xai,pollinations">
<div class="range-block" data-source="openai,openrouter,mistralai,custom,cohere,groq,nanogpt,xai,pollinations,aimlapi">
<div class="range-block-title justifyLeft" data-i18n="Seed">
Seed
</div>
@@ -1969,7 +1969,7 @@
</span>
</div>
</div>
<div class="range-block" data-source="makersuite,vertexai,openrouter,claude">
<div class="range-block" data-source="makersuite,vertexai,aimlapi,openrouter,claude">
<label for="openai_enable_web_search" class="checkbox_label flexWrap widthFreeExpand">
<input id="openai_enable_web_search" type="checkbox" />
<span data-i18n="Enable web search">Enable web search</span>
@@ -1983,7 +1983,7 @@
</b>
</div>
</div>
<div class="range-block" data-source="openai,cohere,mistralai,custom,claude,openrouter,groq,deepseek,makersuite,vertexai,ai21,xai,pollinations">
<div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,deepseek,makersuite,vertexai,ai21,xai,pollinations">
<label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
<input id="openai_function_calling" type="checkbox" />
<span data-i18n="Enable function calling">Enable function calling</span>
@@ -1994,7 +1994,7 @@
<strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>
</div>
</div>
<div class="range-block" data-source="openai,openrouter,mistralai,makersuite,vertexai,claude,custom,01ai,xai,pollinations">
<div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,01ai,xai,pollinations">
<label for="openai_image_inlining" class="checkbox_label flexWrap widthFreeExpand">
<input id="openai_image_inlining" type="checkbox" />
<span data-i18n="Send inline images">Send inline images</span>
@@ -2064,7 +2064,7 @@
</span>
</div>
</div>
<div class="range-block" data-source="deepseek,openrouter,custom,claude,xai,makersuite,vertexai">
<div class="range-block" data-source="deepseek,aimlapi,openrouter,custom,claude,xai,makersuite,vertexai">
<label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
<input id="openai_show_thoughts" type="checkbox" />
<span data-i18n="Request model reasoning">Request model reasoning</span>
@@ -2078,7 +2078,7 @@
</span>
</div>
</div>
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,openrouter,pollinations">
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations">
<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.">
<label for="openai_reasoning_effort">
<span data-i18n="Reasoning Effort">Reasoning Effort</span>
@@ -2092,7 +2092,7 @@
<option data-i18n="openai_reasoning_effort_high" value="high">High</option>
<option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option>
</select>
<div class="toggle-description justifyLeft marginBot5" data-source="openai,custom,xai,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.">
<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.">
OpenAI-style options: low, medium, high. Minimum and maximum are aliased to low and high. Auto does not send an effort level.
</div>
<div class="toggle-description justifyLeft marginBot5" data-source="claude" data-i18n="Allocates a portion of the response length for thinking (min: 1024 tokens, low: 10%, medium: 25%, high: 50%, max: 95%), but minimum 1024 tokens. Auto does not request thinking.">
@@ -2131,7 +2131,7 @@
</div>
</div>
</div>
<div class="range-block m-t-1" data-source="openai,openrouter,scale,custom">
<div class="range-block m-t-1" data-source="openai,aimlapi,openrouter,scale,custom">
<div id="logit_bias_openai" class="range-block-title openai_restorable" data-i18n="Logit Bias">
Logit Bias
</div>
@@ -2376,7 +2376,7 @@
<input id="api_key_openrouter-tg" name="api_key_openrouter" class="text_pole flex1 api_key_openrouter" value="" type="text" autocomplete="off">
<div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_openrouter"></div>
</div>
<div data-for="api_key_openrouter" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
<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'.">
For privacy reasons, your API key will be hidden after you click 'Connect'.
</div>
<div>
@@ -2789,6 +2789,7 @@
<optgroup>
<option value="01ai">01.AI (Yi)</option>
<option value="ai21">AI21</option>
<option value="aimlapi">AI/ML API</option>
<option value="claude">Claude</option>
<option value="cohere">Cohere</option>
<option value="deepseek">DeepSeek</option>
@@ -3632,6 +3633,21 @@
<option value="grok-beta">grok-beta</option>
</select>
</div>
<div id="aimlapi_form" data-source="aimlapi">
<h4>
<span>AI/ML API Key</span>
</h4>
<div class="flex-container">
<input id="api_key_aimlapi" name="api_key_aimlapi" class="text_pole flex1" value="" type="text" autocomplete="off">
<div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_aimlapi"></div>
</div>
<div data-for="api_key_aimlapi" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
For privacy reasons, your API key will be hidden after you click 'Connect'.
</div>
<h4>AI/ML Model</h4>
<select id="model_aimlapi_select">
</select>
</div>
<div id="pollinations_form" data-source="pollinations">
<h4 data-i18n="Pollinations Model">Pollinations Model</h4>
<select id="model_pollinations_select">
+24 -3
View File
@@ -250,7 +250,20 @@ import {
import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js';
import { hideLoader, showLoader } from './scripts/loader.js';
import { BulkEditOverlay, CharacterContextMenu } from './scripts/BulkEditOverlay.js';
import { loadFeatherlessModels, loadMancerModels, loadOllamaModels, loadTogetherAIModels, loadInfermaticAIModels, loadOpenRouterModels, loadVllmModels, loadAphroditeModels, loadDreamGenModels, initTextGenModels, loadTabbyModels, loadGenericModels } from './scripts/textgen-models.js';
import {
loadFeatherlessModels,
loadMancerModels,
loadOllamaModels,
loadTogetherAIModels,
loadInfermaticAIModels,
loadOpenRouterModels,
loadVllmModels,
loadAphroditeModels,
loadDreamGenModels,
initTextGenModels,
loadTabbyModels,
loadGenericModels,
} from './scripts/textgen-models.js';
import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, getCurrentEntityId, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities } from './scripts/chats.js';
import { getPresetManager, initPresetManager } from './scripts/preset-manager.js';
import { evaluateMacros, getLastMessageId, initMacros } from './scripts/macros.js';
@@ -542,6 +555,10 @@ export const event_types = {
TOOL_CALLS_PERFORMED: 'tool_calls_performed',
TOOL_CALLS_RENDERED: 'tool_calls_rendered',
CHARACTER_MANAGEMENT_DROPDOWN: 'charManagementDropdown',
SECRET_WRITTEN: 'secret_written',
SECRET_DELETED: 'secret_deleted',
SECRET_ROTATED: 'secret_rotated',
SECRET_EDITED: 'secret_edited',
};
export const eventSource = new EventEmitter([event_types.APP_READY]);
@@ -2726,7 +2743,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll
/**
* Returns the URL of the avatar for the given character Id.
* @param {number} characterId Character Id
* @param {number|string} characterId Character Id
* @returns {string} Avatar URL
*/
export function getCharacterAvatar(characterId) {
@@ -6108,7 +6125,11 @@ export function extractMessageFromData(data, activeApi = null) {
case 'koboldhorde':
return data.text;
case 'textgenerationwebui':
return data.choices?.[0]?.text ?? data.content ?? data.response ?? '';
return data.choices?.[0]?.text
?? data.choices?.[0]?.message?.content
?? data.content
?? data.response
?? '';
case 'novel':
return data.output;
case 'openai':
+1
View File
@@ -412,6 +412,7 @@ function RA_autoconnect(PrevApi) {
|| (secret_state[SECRET_KEYS.NANOGPT] && oai_settings.chat_completion_source == chat_completion_sources.NANOGPT)
|| (secret_state[SECRET_KEYS.DEEPSEEK] && oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK)
|| (secret_state[SECRET_KEYS.XAI] && oai_settings.chat_completion_source == chat_completion_sources.XAI)
|| (secret_state[SECRET_KEYS.AIMLAPI] && oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI)
|| (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS)
|| (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
) {
+1 -1
View File
@@ -1535,7 +1535,7 @@ export async function runGenerationInterceptors(chat, contextSize, type) {
/**
* Writes a field to the character's data extensions object.
* @param {number} characterId Index in the character array
* @param {number|string} characterId Index in the character array
* @param {string} key Field name
* @param {any} value Field value
* @returns {Promise<void>} When the field is written
@@ -433,6 +433,7 @@ jQuery(async function () {
'zerooneai': SECRET_KEYS.ZEROONEAI,
'groq': SECRET_KEYS.GROQ,
'cohere': SECRET_KEYS.COHERE,
'aimlapi': SECRET_KEYS.AIMLAPI,
};
if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {
@@ -18,6 +18,7 @@
<label for="caption_multimodal_api" data-i18n="API">API</label>
<select id="caption_multimodal_api" class="flex1 text_pole">
<option value="zerooneai">01.AI (Yi)</option>
<option value="aimlapi">AI/ML API</option>
<option value="anthropic">Anthropic</option>
<option value="cohere">Cohere</option>
<option value="custom" data-i18n="Custom (OpenAI-compatible)">Custom (OpenAI-compatible)</option>
@@ -127,6 +128,24 @@
<option data-type="groq" value="llama-3.2-11b-vision-preview">llama-3.2-11b-vision-preview</option>
<option data-type="groq" value="llama-3.2-90b-vision-preview">llama-3.2-90b-vision-preview</option>
<option data-type="groq" value="llava-v1.5-7b-4096-preview">llava-v1.5-7b-4096-preview</option>
<option data-type="aimlapi" value="gpt-4o-2024-05-13">gpt-4o-2024-05-13</option>
<option data-type="aimlapi" value="gpt-4o-2024-08-06">gpt-4o-2024-08-06</option>
<option data-type="aimlapi" value="gpt-4o">gpt-4o</option>
<option data-type="aimlapi" value="gpt-4o-mini">gpt-4o-mini</option>
<option data-type="aimlapi" value="gpt-4o-mini-2024-07-18">gpt-4o-mini-2024-07-18</option>
<option data-type="aimlapi" value="chatgpt-4o-latest">chatgpt-4o-latest</option>
<option data-type="aimlapi" value="gpt-4-turbo">gpt-4-turbo</option>
<option data-type="aimlapi" value="claude-3-opus-20240229">claude-3-opus-20240229</option>
<option data-type="aimlapi" value="claude-3-sonnet-20240229">claude-3-sonnet-20240229</option>
<option data-type="aimlapi" value="claude-3-haiku-20240307">claude-3-haiku-20240307</option>
<option data-type="aimlapi" value="claude-3-5-sonnet-20240620">claude-3-5-sonnet-20240620</option>
<option data-type="aimlapi" value="google/gemini-1.5-flash">google/gemini-1.5-flash</option>
<option data-type="aimlapi" value="google/gemini-1.5-pro">google/gemini-1.5-pro</option>
<option data-type="aimlapi" value="google/gemini-2.0-flash">google/gemini-2.0-flash</option>
<option data-type="aimlapi" value="google/gemini-2.0-flash-exp">google/gemini-2.0-flash-exp</option>
<option data-type="aimlapi" value="google/gemini-2.5-flash-preview">google/gemini-2.5-flash-preview</option>
<option data-type="aimlapi" value="meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo">meta-llama/Llama-3.2-11B-Vision-Instruct-Turbo</option>
<option data-type="aimlapi" value="meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo">meta-llama/Llama-3.2-90B-Vision-Instruct-Turbo</option>
<option data-type="openrouter" value="openai/gpt-4-vision-preview">openai/gpt-4-vision-preview</option>
<option data-type="openrouter" value="openai/gpt-4o">openai/gpt-4o</option>
<option data-type="openrouter" value="openai/gpt-4o-2024-05-13">openai/gpt-4o-2024-05-13</option>
+4
View File
@@ -233,6 +233,10 @@ function throwIfInvalidModel(useReverseProxy) {
if (multimodalApi === 'custom' && !oai_settings.custom_url) {
throw new Error('Custom API URL is not set.');
}
if (multimodalApi === 'aimlapi' && !secret_state[SECRET_KEYS.AIMLAPI]) {
throw new Error('AI/ML API key is not set.');
}
}
/**
@@ -41,7 +41,7 @@ import {
stringFormat,
} from '../../utils.js';
import { getMessageTimeStamp, humanizedDateTime } from '../../RossAscends-mods.js';
import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
import { SECRET_KEYS, secret_state } from '../../secrets.js';
import { getNovelAnlas, getNovelUnlimitedImageGeneration, loadNovelSubscriptionData } from '../../nai-settings.js';
import { getMultimodalCaption } from '../shared.js';
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
@@ -53,7 +53,7 @@ import {
} from '../../slash-commands/SlashCommandArgument.js';
import { debounce_timeout } from '../../constants.js';
import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';
import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';
import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
import { ToolManager } from '../../tool-calling.js';
import { MacrosParser } from '../../macros.js';
@@ -74,6 +74,7 @@ const sources = {
novel: 'novel',
vlad: 'vlad',
openai: 'openai',
aimlapi: 'aimlapi',
comfy: 'comfy',
togetherai: 'togetherai',
drawthings: 'drawthings',
@@ -1144,42 +1145,6 @@ function onComfyWorkflowChange() {
saveSettingsDebounced();
}
async function onApiKeyClick(popupText, secretKey) {
const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, '', {
customButtons: [{
text: 'Remove Key',
appendAtEnd: true,
result: POPUP_RESULT.NEGATIVE,
action: async () => {
await writeSecret(secretKey, '');
toastr.success('API Key removed');
await loadSettingOptions();
},
}],
});
if (!key) {
return;
}
await writeSecret(secretKey, String(key));
toastr.success('API Key saved');
await loadSettingOptions();
}
async function onStabilityKeyClick() {
return onApiKeyClick('Stability AI API Key:', SECRET_KEYS.STABILITY);
}
async function onBflKeyClick() {
return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL);
}
async function onFalaiKeyClick() {
return onApiKeyClick('FALAI API Key:', SECRET_KEYS.FALAI);
}
function onBflUpsamplingInput() {
extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked');
saveSettingsDebounced();
@@ -1303,6 +1268,7 @@ async function onModelChange() {
sources.horde,
sources.novel,
sources.openai,
sources.aimlapi,
sources.togetherai,
sources.pollinations,
sources.stability,
@@ -1505,6 +1471,9 @@ async function loadSamplers() {
case sources.openai:
samplers = ['N/A'];
break;
case sources.aimlapi:
samplers = ['N/A'];
break;
case sources.comfy:
samplers = await loadComfySamplers();
break;
@@ -1695,6 +1664,9 @@ async function loadModels() {
case sources.openai:
models = await loadOpenAiModels();
break;
case sources.aimlapi:
models = await loadAimlapiModels();
break;
case sources.comfy:
models = await loadComfyModels();
break;
@@ -1959,6 +1931,23 @@ async function loadOpenAiModels() {
];
}
async function loadAimlapiModels() {
$('#sd_aimlapi_key').toggleClass('success', !!secret_state[SECRET_KEYS.AIMLAPI]);
const result = await fetch('/api/sd/aimlapi/models', {
method: 'POST',
headers: getRequestHeaders(),
});
if (!result.ok) {
return [];
}
const json = await result.json();
return (json.data || []);
}
async function loadVladModels() {
if (!extension_settings.sd.vlad_url) {
return [];
@@ -2086,6 +2075,9 @@ async function loadSchedulers() {
case sources.openai:
schedulers = ['N/A'];
break;
case sources.aimlapi:
schedulers = ['N/A'];
break;
case sources.togetherai:
schedulers = ['N/A'];
break;
@@ -2177,6 +2169,9 @@ async function loadVaes() {
case sources.openai:
vaes = ['N/A'];
break;
case sources.aimlapi:
vaes = ['N/A'];
break;
case sources.togetherai:
vaes = ['N/A'];
break;
@@ -2749,6 +2744,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
case sources.openai:
result = await generateOpenAiImage(prefixedPrompt, signal);
break;
case sources.aimlapi:
result = await generateAimlapiImage(prefixedPrompt, signal);
break;
case sources.comfy:
result = await generateComfyImage(prefixedPrompt, negativePrompt, signal);
break;
@@ -3333,6 +3331,47 @@ async function generateOpenAiImage(prompt, signal) {
}
}
/**
* Universal image generation via AIMLAPI:
* - Builds the right request body for any model (OpenAI vs SD/Flux/Recraft).
* - Extracts the URL or base64 response.
* - If its a URL, fetches the image and converts to base64.
* - Returns { format: 'png', data: '<base64 string>' }, ready for saveBase64AsFile().
*/
async function generateAimlapiImage(prompt, signal) {
const model = extension_settings.sd.model.toLowerCase();
const isSdLike =
model.startsWith('flux/') ||
model.startsWith('stable') ||
model === 'recraft-v3' ||
model === 'triposr';
const body = { prompt, model };
if (isSdLike) {
body.steps = clamp(extension_settings.sd.steps, 1, 50);
body.guidance = clamp(extension_settings.sd.scale, 1.5, 5);
body.width = clamp(extension_settings.sd.width, 256, 1440);
body.height = clamp(extension_settings.sd.height, 256, 1440);
if (extension_settings.sd.seed >= 0) body.seed = extension_settings.sd.seed;
} else {
body.n = 1;
body.size = `${extension_settings.sd.width}x${extension_settings.sd.height}`;
body.quality = extension_settings.sd.openai_quality;
body.style = extension_settings.sd.openai_style;
}
const res = await fetch('/api/sd/aimlapi/generate-image', {
method: 'POST',
headers: getRequestHeaders(),
signal,
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(await res.text());
const { format, data } = await res.json();
return { format, data };
}
/**
* Generates an image in ComfyUI using the provided prompt and configuration settings.
*
@@ -3849,6 +3888,8 @@ function isValidState() {
return secret_state[SECRET_KEYS.NOVEL];
case sources.openai:
return secret_state[SECRET_KEYS.OPENAI];
case sources.aimlapi:
return secret_state[SECRET_KEYS.AIMLAPI];
case sources.comfy:
return true;
case sources.togetherai:
@@ -4522,13 +4563,10 @@ jQuery(async () => {
$('#sd_interactive_visible').on('input', onInteractiveVisibleInput);
$('#sd_tool_visible').on('input', onToolVisibleInput);
$('#sd_swap_dimensions').on('click', onSwapDimensionsClick);
$('#sd_stability_key').on('click', onStabilityKeyClick);
$('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);
$('#sd_huggingface_model_id').on('input', onHFModelInput);
$('#sd_function_tool').on('input', onFunctionToolInput);
$('#sd_bfl_key').on('click', onBflKeyClick);
$('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);
$('#sd_falai_key').on('click', onFalaiKeyClick);
if (!CSS.supports('field-sizing', 'content')) {
$('.sd_settings .inline-drawer-toggle').on('click', function () {
@@ -4556,6 +4594,19 @@ jQuery(async () => {
eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
[event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
eventSource.on(event, async (/** @type {string} */ key) => {
switch (key) {
case SECRET_KEYS.BFL:
case SECRET_KEYS.FALAI:
case SECRET_KEYS.STABILITY:
case SECRET_KEYS.AIMLAPI:
await loadSettingOptions();
break;
}
});
});
await loadSettings();
$('body').addClass('sd');
@@ -37,6 +37,7 @@
</label>
<label for="sd_source" data-i18n="Source">Source</label>
<select id="sd_source">
<option value="aimlapi">AI/ML API</option>
<option value="bfl">BFL (Black Forest Labs)</option>
<option value="comfy">ComfyUI</option>
<option value="drawthings">DrawThings HTTP API</option>
@@ -139,7 +140,16 @@
</div>
<i>Hint: Save an API key in the NovelAI API settings to use it here.</i>
</div>
<div data-sd-source="openai">
<div data-sd-source="aimlapi">
<div class="flex-container flexnowrap alignItemsBaseline marginBot5">
<strong class="flex1" data-i18n="API Key">API Key</strong>
<div id="sd_aimlapi_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_aimlapi">
<i class="fa-fw fa-solid fa-key"></i>
<span data-i18n="Click to set">Click to set</span>
</div>
</div>
</div>
<div data-sd-source="openai,aimlapi">
<small data-i18n="These settings only apply to DALL-E 3">These settings only apply to DALL-E 3</small>
<div class="flex-container">
<div class="flex1">
@@ -200,7 +210,7 @@
<div data-sd-source="stability">
<div class="flex-container flexnowrap alignItemsBaseline marginBot5">
<strong class="flex1" data-i18n="API Key">API Key</strong>
<div id="sd_stability_key" class="menu_button menu_button_icon">
<div id="sd_stability_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_stability">
<i class="fa-fw fa-solid fa-key"></i>
<span data-i18n="Click to set">Click to set</span>
</div>
@@ -244,7 +254,7 @@
<i class="fa-solid fa-share-from-square"></i>
</a>
<span class="expander"></span>
<div id="sd_bfl_key" class="menu_button menu_button_icon">
<div id="sd_bfl_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_bfl">
<i class="fa-fw fa-solid fa-key"></i>
<span data-i18n="Click to set">Click to set</span>
</div>
@@ -264,7 +274,7 @@
<i class="fa-solid fa-share-from-square"></i>
</a>
<span class="expander"></span>
<div id="sd_falai_key" class="menu_button menu_button_icon">
<div id="sd_falai_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_falai">
<i class="fa-fw fa-solid fa-key"></i>
<span data-i18n="Click to set">Click to set</span>
</div>
+137 -3
View File
@@ -185,6 +185,7 @@ export const chat_completion_sources = {
ZEROONEAI: '01ai',
NANOGPT: 'nanogpt',
DEEPSEEK: 'deepseek',
AIMLAPI: 'aimlapi',
XAI: 'xai',
POLLINATIONS: 'pollinations',
};
@@ -273,6 +274,7 @@ export const settingsToUpdate = {
groq_model: ['#model_groq_select', 'groq_model', false, true],
nanogpt_model: ['#model_nanogpt_select', 'nanogpt_model', false, true],
deepseek_model: ['#model_deepseek_select', 'deepseek_model', false, true],
aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true],
zerooneai_model: ['#model_01ai_select', 'zerooneai_model', false, true],
xai_model: ['#model_xai_select', 'xai_model', false, true],
pollinations_model: ['#model_pollinations_select', 'pollinations_model', false, true],
@@ -369,6 +371,7 @@ const default_settings = {
nanogpt_model: 'gpt-4o-mini',
zerooneai_model: 'yi-large',
deepseek_model: 'deepseek-chat',
aimlapi_model: 'gpt-4o-mini-2024-07-18',
xai_model: 'grok-3-beta',
pollinations_model: 'openai',
custom_model: '',
@@ -456,6 +459,7 @@ const oai_settings = {
nanogpt_model: 'gpt-4o-mini',
zerooneai_model: 'yi-large',
deepseek_model: 'deepseek-chat',
aimlapi_model: 'gpt-4-turbo',
xai_model: 'grok-3-beta',
pollinations_model: 'openai',
custom_model: '',
@@ -1712,6 +1716,8 @@ export function getChatCompletionModel(source = null) {
return oai_settings.nanogpt_model;
case chat_completion_sources.DEEPSEEK:
return oai_settings.deepseek_model;
case chat_completion_sources.AIMLAPI:
return oai_settings.aimlapi_model;
case chat_completion_sources.XAI:
return oai_settings.xai_model;
case chat_completion_sources.POLLINATIONS:
@@ -1836,6 +1842,19 @@ function saveModelList(data) {
$('#model_01ai_select').val(oai_settings.zerooneai_model).trigger('change');
}
if (oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI) {
$('#model_aimlapi_select').empty();
const chatModels = model_list.filter(m => m.type === 'chat-completion');
appendAimlapiOptions(aimlapiGroupByVendor(chatModels));
if (!oai_settings.aimlapi_model && chatModels.length > 0) {
oai_settings.aimlapi_model = chatModels[0].id;
}
$('#model_aimlapi_select').val(oai_settings.aimlapi_model).trigger('change');
}
if (oai_settings.chat_completion_source == chat_completion_sources.MISTRALAI) {
/** @type {HTMLSelectElement} */
const mistralModelSelect = document.querySelector('#model_mistralai_select');
@@ -2011,6 +2030,56 @@ function openRouterGroupByVendor(array) {
}, new Map());
}
function aimlapiGroupByVendor(array) {
return array.reduce((acc, curr) => {
const vendor = curr.info.developer;
if (!acc.has(vendor)) {
acc.set(vendor, []);
}
acc.get(vendor).push(curr);
return acc;
}, new Map());
}
function appendAimlapiOptions(model_list) {
const appendOption = (model, parent = null) => {
(parent || $('#model_aimlapi_select')).append(
$('<option>', {
value: model.id,
text: model.info?.name || model.name || model.id,
}));
};
model_list.forEach((models, vendor) => {
const optgroup = $(`<optgroup label="${vendor}">`);
models.forEach((model) => {
appendOption(model, optgroup);
});
$('#model_aimlapi_select').append(optgroup);
});
}
function getAimlapiModelTemplate(option) {
const model = model_list.find(x => x.id === option?.element?.value);
if (!option.id || !model) {
return option.text;
}
const vendor = model.id.split('/')[0];
return $((`
<div class="flex-container flexFlowColumn" title="${DOMPurify.sanitize(model.id)}">
<div><strong>${DOMPurify.sanitize(model.info?.name || model.name || model.id)}</strong> | ${vendor}</div>
</div>
`));
}
async function sendAltScaleRequest(messages, logit_bias, signal, type) {
const generate_url = '/api/backends/scale-alt/generate';
@@ -2065,6 +2134,7 @@ function getReasoningEffort() {
chat_completion_sources.OPENAI,
chat_completion_sources.CUSTOM,
chat_completion_sources.XAI,
chat_completion_sources.AIMLAPI,
chat_completion_sources.OPENROUTER,
chat_completion_sources.POLLINATIONS,
];
@@ -2122,6 +2192,7 @@ async function sendOpenAIRequest(type, messages, signal) {
const is01AI = oai_settings.chat_completion_source == chat_completion_sources.ZEROONEAI;
const isNano = oai_settings.chat_completion_source == chat_completion_sources.NANOGPT;
const isDeepSeek = oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK;
const isAimlapi = oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI;
const isXAI = oai_settings.chat_completion_source == chat_completion_sources.XAI;
const isPollinations = oai_settings.chat_completion_source == chat_completion_sources.POLLINATIONS;
const isTextCompletion = isOAI && textCompletionModels.includes(oai_settings.openai_model);
@@ -2130,7 +2201,7 @@ async function sendOpenAIRequest(type, messages, signal) {
const isContinue = type === 'continue';
const stream = oai_settings.stream_openai && !isQuiet && !isScale && !(isOAI && ['o1-2024-12-17', 'o1'].includes(oai_settings.openai_model));
const useLogprobs = !!power_user.request_token_probabilities;
const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom || isXAI);
const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom || isXAI || isAimlapi);
// If we're using the window.ai extension, use that instead
// Doesn't support logit bias yet
@@ -2196,7 +2267,7 @@ async function sendOpenAIRequest(type, messages, signal) {
}
// Add logprobs request (currently OpenAI only, max 5 on their side)
if (useLogprobs && (isOAI || isCustom || isDeepSeek || isXAI)) {
if (useLogprobs && (isOAI || isCustom || isDeepSeek || isXAI || isAimlapi)) {
generate_data['logprobs'] = 5;
}
@@ -2334,7 +2405,7 @@ async function sendOpenAIRequest(type, messages, signal) {
delete generate_data.max_tokens;
}
if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano || isXAI || isPollinations) && oai_settings.seed >= 0) {
if ((isOAI || isOpenRouter || isMistral || isCustom || isCohere || isNano || isXAI || isPollinations || isAimlapi) && oai_settings.seed >= 0) {
generate_data['seed'] = oai_settings.seed;
}
@@ -3461,6 +3532,7 @@ function loadOpenAISettings(data, settings) {
oai_settings.groq_model = settings.groq_model ?? default_settings.groq_model;
oai_settings.nanogpt_model = settings.nanogpt_model ?? default_settings.nanogpt_model;
oai_settings.deepseek_model = settings.deepseek_model ?? default_settings.deepseek_model;
oai_settings.aimlapi_model = settings.aimlapi_model ?? default_settings.aimlapi_model;
oai_settings.zerooneai_model = settings.zerooneai_model ?? default_settings.zerooneai_model;
oai_settings.xai_model = settings.xai_model ?? default_settings.xai_model;
oai_settings.pollinations_model = settings.pollinations_model ?? default_settings.pollinations_model;
@@ -3558,6 +3630,8 @@ function loadOpenAISettings(data, settings) {
$('#model_deepseek_select').val(oai_settings.deepseek_model);
$(`#model_deepseek_select option[value="${oai_settings.deepseek_model}"`).prop('selected', true);
$('#model_01ai_select').val(oai_settings.zerooneai_model);
$('#model_aimlapi_select').val(oai_settings.aimlapi_model);
$(`#model_aimlapi_select option[value="${oai_settings.aimlapi_model}"`).prop('selected', true);
$('#model_xai_select').val(oai_settings.xai_model);
$(`#model_xai_select option[value="${oai_settings.xai_model}"`).prop('selected', true);
$('#model_pollinations_select').val(oai_settings.pollinations_model);
@@ -3861,6 +3935,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) {
zerooneai_model: settings.zerooneai_model,
xai_model: settings.xai_model,
pollinations_model: settings.pollinations_model,
aimlapi_model: settings.aimlapi_model,
custom_model: settings.custom_model,
custom_url: settings.custom_url,
custom_include_body: settings.custom_include_body,
@@ -4709,6 +4784,15 @@ async function onModelChange() {
oai_settings.pollinations_model = value;
}
if ($(this).is('#model_aimlapi_select')) {
if (!value) {
console.debug('Null AI/ML model selected. Ignoring.');
return;
}
console.log('AI/ML model changed to', value);
oai_settings.aimlapi_model = value;
}
if ($(this).is('#model_xai_select')) {
console.log('XAI model changed to', value);
oai_settings.xai_model = value;
@@ -4995,6 +5079,30 @@ async function onModelChange() {
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source === chat_completion_sources.AIMLAPI) {
let maxContext;
if (oai_settings.max_context_unlocked) {
maxContext = unlocked_max;
} else {
const model = model_list.find(m => m.id === oai_settings.aimlapi_model);
maxContext = (model?.info?.contextLength ?? model?.context_length) || max_32k;
console.log('[AI/ML API] Model CTX:', model?.info?.contextLength);
}
$('#openai_max_context')
.prop('max', maxContext)
.val(Math.min(Number(oai_settings.openai_max_context), maxContext))
.trigger('input');
$('#temp_openai')
.prop('max', oai_max_temp)
.val(Number(oai_settings.temp_openai))
.trigger('input');
oai_settings.openai_max_context = Number($('#openai_max_context').val());
oai_settings.temp_openai = Number($('#temp_openai').val());
}
if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) {
oai_settings.pres_pen_openai = Math.min(Math.max(0, oai_settings.pres_pen_openai), 1);
$('#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) {
}
}
if (oai_settings.chat_completion_source === chat_completion_sources.AIMLAPI) {
const api_key_aimlapi = String($('#api_key_aimlapi').val()).trim();
if (api_key_aimlapi.length) {
await writeSecret(SECRET_KEYS.AIMLAPI, api_key_aimlapi);
}
if (!secret_state[SECRET_KEYS.AIMLAPI]) {
console.log('No secret key saved for AI/ML API');
return;
}
}
startStatusLoading();
saveSettingsDebounced();
await getStatusOpen();
@@ -5329,6 +5450,9 @@ function toggleChatCompletionForms() {
else if (oai_settings.chat_completion_source == chat_completion_sources.DEEPSEEK) {
$('#model_deepseek_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.AIMLAPI) {
$('#model_aimlapi_select').trigger('change');
}
else if (oai_settings.chat_completion_source == chat_completion_sources.XAI) {
$('#model_xai_select').trigger('change');
}
@@ -5478,6 +5602,8 @@ export function isImageInliningSupported() {
return visionSupportedModels.some(model => oai_settings.cohere_model.includes(model));
case chat_completion_sources.XAI:
return visionSupportedModels.some(model => oai_settings.xai_model.includes(model));
case chat_completion_sources.AIMLAPI:
return visionSupportedModels.some(model => oai_settings.aimlapi_model.includes(model));
case chat_completion_sources.POLLINATIONS:
return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.pollinations_model)?.vision);
default:
@@ -6214,6 +6340,13 @@ export function initOpenAI() {
width: '100%',
templateResult: getOpenRouterModelTemplate,
});
$('#model_aimlapi_select').select2({
placeholder: t`Select a model`,
searchInputPlaceholder: t`Search models...`,
searchInputCssClass: 'text_pole',
width: '100%',
templateResult: getAimlapiModelTemplate,
});
}
$('#openrouter_providers_chat').on('change', function () {
@@ -6261,6 +6394,7 @@ export function initOpenAI() {
$('#model_nanogpt_select').on('change', onModelChange);
$('#model_deepseek_select').on('change', onModelChange);
$('#model_01ai_select').on('change', onModelChange);
$('#model_aimlapi_select').on('change', onModelChange);
$('#model_custom_select').on('change', onModelChange);
$('#model_xai_select').on('change', onModelChange);
$('#model_pollinations_select').on('change', onModelChange);
+8 -1
View File
@@ -1,5 +1,5 @@
import { DOMPurify, moment } from '../lib.js';
import { getRequestHeaders } from '../script.js';
import { event_types, eventSource, getRequestHeaders } from '../script.js';
import { t } from './i18n.js';
import { chat_completion_sources } from './openai.js';
import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
@@ -54,6 +54,7 @@ export const SECRET_KEYS = {
GENERIC: 'api_key_generic',
DEEPSEEK: 'api_key_deepseek',
SERPER: 'api_key_serper',
AIMLAPI: 'api_key_aimlapi',
FALAI: 'api_key_falai',
XAI: 'api_key_xai',
VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',
@@ -102,6 +103,7 @@ const FRIENDLY_NAMES = {
[SECRET_KEYS.SERPER]: 'Serper',
[SECRET_KEYS.FALAI]: 'FAL.AI',
[SECRET_KEYS.AZURE_TTS]: 'Azure TTS',
[SECRET_KEYS.AIMLAPI]: 'AI/ML API',
};
const INPUT_MAP = {
@@ -137,6 +139,7 @@ const INPUT_MAP = {
[SECRET_KEYS.NANOGPT]: '#api_key_nanogpt',
[SECRET_KEYS.GENERIC]: '#api_key_generic',
[SECRET_KEYS.DEEPSEEK]: '#api_key_deepseek',
[SECRET_KEYS.AIMLAPI]: '#api_key_aimlapi',
[SECRET_KEYS.XAI]: '#api_key_xai',
[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]: '#vertexai_service_account_json',
};
@@ -303,6 +306,7 @@ export async function writeSecret(key, value, label) {
// Clear the input field
$(INPUT_MAP[key]).val('').trigger('input');
await readSecretState();
await eventSource.emit(event_types.SECRET_WRITTEN, key);
return id;
} catch (error) {
console.error(`Could not write secret value: ${key}`, error);
@@ -327,6 +331,7 @@ export async function deleteSecret(key, id) {
await readSecretState();
// Force reconnection to the API with the new key
$('#main_api').trigger('change');
await eventSource.emit(event_types.SECRET_DELETED, key);
}
} catch (error) {
console.error(`Could not delete secret value: ${key}`, error);
@@ -398,6 +403,7 @@ export async function rotateSecret(key, id) {
await readSecretState();
// Force reconnection to the API with the new key
$('#main_api').trigger('change');
await eventSource.emit(event_types.SECRET_ROTATED, key);
}
} catch (error) {
console.error(`Could not rotate secret value: ${key}`, error);
@@ -420,6 +426,7 @@ export async function renameSecret(key, id, label) {
if (response.ok) {
await readSecretState();
await eventSource.emit(event_types.SECRET_EDITED, key);
}
} catch (error) {
console.error(`Could not rename secret value: ${key}`, error);
+1
View File
@@ -4244,6 +4244,7 @@ function getModelOptions(quiet) {
{ id: 'model_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT },
{ id: 'model_01ai_select', api: 'openai', type: chat_completion_sources.ZEROONEAI },
{ id: 'model_deepseek_select', api: 'openai', type: chat_completion_sources.DEEPSEEK },
{ id: 'model_aimlapi_select', api: 'openai', type: chat_completion_sources.AIMLAPI },
{ id: 'model_xai_select', api: 'openai', type: chat_completion_sources.XAI },
{ id: 'model_pollinations_select', api: 'openai', type: chat_completion_sources.POLLINATIONS },
{ id: 'model_novel_select', api: 'novel', type: null },
+1
View File
@@ -611,6 +611,7 @@ export class ToolManager {
chat_completion_sources.MISTRALAI,
chat_completion_sources.CLAUDE,
chat_completion_sources.OPENROUTER,
chat_completion_sources.AIMLAPI,
chat_completion_sources.GROQ,
chat_completion_sources.COHERE,
chat_completion_sources.DEEPSEEK,
+6
View File
@@ -177,6 +177,7 @@ export const CHAT_COMPLETION_SOURCES = {
ZEROONEAI: '01ai',
NANOGPT: 'nanogpt',
DEEPSEEK: 'deepseek',
AIMLAPI: 'aimlapi',
XAI: 'xai',
POLLINATIONS: 'pollinations',
};
@@ -335,6 +336,11 @@ export const OPENROUTER_HEADERS = {
'X-Title': 'SillyTavern',
};
export const AIMLAPI_HEADERS = {
'HTTP-Referer': 'https://sillytavern.app',
'X-Title': 'SillyTavern',
};
export const FEATHERLESS_HEADERS = {
'HTTP-Referer': 'https://sillytavern.app',
'X-Title': 'SillyTavern',
+100
View File
@@ -4,6 +4,7 @@ import express from 'express';
import fetch from 'node-fetch';
import {
AIMLAPI_HEADERS,
CHAT_COMPLETION_SOURCES,
GEMINI_SAFETY,
OPENROUTER_HEADERS,
@@ -61,6 +62,7 @@ const API_AI21 = 'https://api.ai21.com/studio/v1';
const API_NANOGPT = 'https://nano-gpt.com/api/v1';
const API_DEEPSEEK = 'https://api.deepseek.com/beta';
const API_XAI = 'https://api.x.ai/v1';
const API_AIMLAPI = 'https://api.aimlapi.com/v1';
const API_POLLINATIONS = 'https://text.pollinations.ai/openai';
/**
@@ -1017,6 +1019,99 @@ async function sendXaiRequest(request, response) {
}
}
/**
* Sends a request to AI/ML API.
* @param {express.Request} request Express request
* @param {express.Response} response Express response
*/
async function sendAimlapiRequest(request, response) {
const apiUrl = API_AIMLAPI;
const apiKey = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);
if (!apiKey) {
console.warn('AI/ML API key is missing.');
return response.status(400).send({ error: true });
}
const controller = new AbortController();
request.socket.removeAllListeners('close');
request.socket.on('close', function () {
controller.abort();
});
try {
let bodyParams = {};
if (request.body.logprobs > 0) {
bodyParams['top_logprobs'] = request.body.logprobs;
bodyParams['logprobs'] = true;
}
if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
bodyParams['tools'] = request.body.tools;
bodyParams['tool_choice'] = request.body.tool_choice;
}
if (Array.isArray(request.body.stop) && request.body.stop.length > 0) {
bodyParams['stop'] = request.body.stop;
}
if (request.body.reasoning_effort) {
bodyParams['reasoning_effort'] = request.body.reasoning_effort;
}
const requestBody = {
'messages': request.body.messages,
'model': request.body.model,
'temperature': request.body.temperature,
'max_tokens': request.body.max_tokens,
'stream': request.body.stream,
'presence_penalty': request.body.presence_penalty,
'frequency_penalty': request.body.frequency_penalty,
'top_p': request.body.top_p,
'seed': request.body.seed,
'n': request.body.n,
...bodyParams,
};
const config = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey,
...AIMLAPI_HEADERS,
},
body: JSON.stringify(requestBody),
signal: controller.signal,
};
console.debug('AI/ML API request:', requestBody);
const generateResponse = await fetch(apiUrl + '/chat/completions', config);
if (request.body.stream) {
forwardFetchResponse(generateResponse, response);
} else {
if (!generateResponse.ok) {
const errorText = await generateResponse.text();
console.warn(`AI/ML API returned error: ${generateResponse.status} ${generateResponse.statusText} ${errorText}`);
const errorJson = tryParse(errorText) ?? { error: true };
return response.status(500).send(errorJson);
}
const generateResponseJson = await generateResponse.json();
console.debug('AI/ML API response:', generateResponseJson);
return response.send(generateResponseJson);
}
} catch (error) {
console.error('Error communicating with AI/ML API: ', error);
if (!response.headersSent) {
response.send({ error: true });
} else {
response.end();
}
}
}
export const router = express.Router();
router.post('/status', async function (request, statusResponse) {
@@ -1064,6 +1159,10 @@ router.post('/status', async function (request, statusResponse) {
apiUrl = new URL(request.body.reverse_proxy || API_XAI);
apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.XAI);
headers = {};
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.AIMLAPI) {
apiUrl = API_AIMLAPI;
apiKey = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);
headers = { ...AIMLAPI_HEADERS };
} else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {
apiUrl = 'https://text.pollinations.ai';
apiKey = 'NONE';
@@ -1286,6 +1385,7 @@ router.post('/generate', function (request, response) {
case CHAT_COMPLETION_SOURCES.MISTRALAI: return sendMistralAIRequest(request, response);
case CHAT_COMPLETION_SOURCES.COHERE: return sendCohereRequest(request, response);
case CHAT_COMPLETION_SOURCES.DEEPSEEK: return sendDeepSeekRequest(request, response);
case CHAT_COMPLETION_SOURCES.AIMLAPI: return sendAimlapiRequest(request, response);
case CHAT_COMPLETION_SOURCES.XAI: return sendXaiRequest(request, response);
}
+10 -1
View File
@@ -8,7 +8,7 @@ import express from 'express';
import { getConfigValue, mergeObjectWithYaml, excludeKeysByYaml, trimV1 } from '../util.js';
import { setAdditionalHeaders } from '../additional-headers.js';
import { readSecret, SECRET_KEYS } from './secrets.js';
import { OPENROUTER_HEADERS } from '../constants.js';
import { AIMLAPI_HEADERS, OPENROUTER_HEADERS } from '../constants.js';
export const router = express.Router();
@@ -65,6 +65,10 @@ router.post('/caption-image', async (request, response) => {
key = readSecret(request.user.directories, SECRET_KEYS.ZEROONEAI);
}
if (request.body.api === 'aimlapi') {
key = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);
}
if (request.body.api === 'groq') {
key = readSecret(request.user.directories, SECRET_KEYS.GROQ);
}
@@ -128,6 +132,11 @@ router.post('/caption-image', async (request, response) => {
apiUrl = 'https://api.lingyiwanwu.com/v1/chat/completions';
}
if (request.body.api === 'aimlapi') {
apiUrl = 'https://api.aimlapi.com/v1/chat/completions';
Object.assign(headers, AIMLAPI_HEADERS);
}
if (request.body.api === 'groq') {
apiUrl = 'https://api.groq.com/openai/v1/chat/completions';
if (body.messages?.[0]?.role === 'system') {
+1
View File
@@ -54,6 +54,7 @@ export const SECRET_KEYS = {
GENERIC: 'api_key_generic',
DEEPSEEK: 'api_key_deepseek',
SERPER: 'api_key_serper',
AIMLAPI: 'api_key_aimlapi',
XAI: 'api_key_xai',
VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json',
};
+87
View File
@@ -11,6 +11,7 @@ import _ from 'lodash';
import { delay, getBasicAuthHeader, tryParse } from '../util.js';
import { readSecret, SECRET_KEYS } from './secrets.js';
import { AIMLAPI_HEADERS } from '../constants.js';
/**
* Gets the comfy workflows.
@@ -1336,3 +1337,89 @@ router.use('/nanogpt', nanogpt);
router.use('/bfl', bfl);
router.use('/falai', falai);
router.use('/xai', xai);
const aimlapi = express.Router();
aimlapi.post('/models', async (request, response) => {
try {
const key = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);
if (!key) {
console.warn('AI/ML API key not found.');
return response.sendStatus(400);
}
const modelsResponse = await fetch('https://api.aimlapi.com/v1/models', {
method: 'GET',
headers: {
Authorization: `Bearer ${key}`,
},
});
if (!modelsResponse.ok) {
console.warn('AI/ML API returned an error.');
return response.sendStatus(500);
}
/** @type {any} */
const data = await modelsResponse.json();
const models = (data.data || [])
.filter(model =>
model.type === 'image' &&
model.id !== 'triposr' &&
model.id !== 'flux/dev/image-to-image',
)
.map(model => ({
value: model.id,
text: model.info?.name || model.id,
}));
return response.send({ data: models });
} catch (error) {
console.error(error);
return response.sendStatus(500);
}
});
aimlapi.post('/generate-image', async (req, res) => {
try {
const key = readSecret(req.user.directories, SECRET_KEYS.AIMLAPI);
if (!key) return res.sendStatus(400);
console.debug('AI/ML API image request:', req.body);
const apiRes = await fetch('https://api.aimlapi.com/v1/images/generations', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}`, ...AIMLAPI_HEADERS },
body: JSON.stringify(req.body),
});
if (!apiRes.ok) {
const err = await apiRes.text();
return res.status(500).send(err);
}
/** @type {any} */
const data = await apiRes.json();
const imgObj = Array.isArray(data.images) ? data.images[0] : data.data?.[0];
if (!imgObj) return res.status(500).send('No image returned');
let base64;
if (imgObj.b64_json || imgObj.base64) {
base64 = imgObj.b64_json || imgObj.base64;
} else if (imgObj.url) {
const blobRes = await fetch(imgObj.url);
if (!blobRes.ok) throw new Error('Failed to fetch image URL');
const buffer = await blobRes.arrayBuffer();
base64 = Buffer.from(buffer).toString('base64');
} else {
throw new Error('Unsupported image format');
}
return res.json({ format: 'png', data: base64 });
} catch (e) {
console.error(e);
res.status(500).send('Internal error');
}
});
router.use('/aimlapi', aimlapi);