feat: add MiniMax as a chat completion provider (#5452)

* feat: add MiniMax as a chat completion provider

Add MiniMax (https://www.minimax.io) as a first-class chat completion
provider. MiniMax already has TTS integration in SillyTavern; this
extends support to LLM chat completions via their OpenAI-compatible API.

Supported models:
- MiniMax-M2.5 (default) — 204K context
- MiniMax-M2.5-highspeed — same capability, faster inference

Key implementation details:
- Reuses existing SECRET_KEYS.MINIMAX (shared with TTS)
- API endpoint: https://api.minimax.io/v1
- Temperature clamped to (0.0, 1.0] as required by MiniMax API
- Returns hardcoded model list since MiniMax doesn't expose /v1/models
- Full UI integration: model selector, sampler parameters, streaming

Co-Authored-By: octo-patch <octo-patch@users.noreply.github.com>

* feat: upgrade MiniMax default model to M2.7

- Add MiniMax-M2.7 and MiniMax-M2.7-highspeed to model list
- Set MiniMax-M2.7 as default model
- Keep all previous models as alternatives

* feat: independent request function, vision support, temp clamping for MiniMax

- Extract sendMinimaxRequest() following Chutes pattern (PR #4844)
  with function calling and JSON Schema structured output support
- Clamp temperature to (0.01, 1.0] on backend; limit frontend UI max to 1.0
- Enable image inlining for MiniMax M2.7 model
- Add MiniMax to slash-commands model selector and tokenizer mapping
- Add minimax_model to default preset

* feat: add VLM-based vision support for MiniMax M2.7

M2.7 does not natively accept image input. When images are detected
in messages, pre-process them via the MiniMax VLM endpoint
(/v1/coding_plan/vlm) to convert images to text descriptions before
sending to the chat completions API. Uses the same API key.

* feat: add M2-her model to MiniMax provider

M2-her is MiniMax's dialogue/roleplay-optimized model with 64K context
and 2048 max completion tokens. Text-only (no vision).

* feat: add MiniMax China endpoint (minimaxi.com) support

Add endpoint selector (Global/China) for MiniMax, mirroring the
SiliconFlow pattern. Users can now choose between api.minimax.io
(international) and api.minimaxi.com (China domestic).

* fix: merge consecutive same-role messages for MiniMax

MiniMax API rejects consecutive messages with the same role with
error 'invalid chat setting (2013)'. Merge them before sending.

* review: address PR feedback on MiniMax provider

Backend (src/endpoints/backends/chat-completions.js):
- Drop the entire MiniMax VLM image-preprocessing path; vision is no
  longer advertised for this provider, so M2.7 messages now go straight
  to /chat/completions without a separate VLM round-trip.
- Drop the json_schema -> response_format mapping (MiniMax does not
  document structured-output support; relying on it was speculative).
- Drop the backend temperature clamp; the same clamp now lives in the
  frontend so the wire payload matches what the user sees.
- Drop the MINIMAX branch in /status that returned a hard-coded model
  list; the frontend hardcodes the same list and bypasses /status via
  noValidateSources, so the round-trip was wasted.
- Add a streaming Transform + non-streaming helper that move
  <think>...</think> blocks from delta.content / message.content to
  reasoning_content. MiniMax M2.x emit chain-of-thought inline in
  content; without this transform the raw <think> tags leak into the
  rendered chat. Includes a state machine that holds back partial
  marker bytes so a marker split across SSE chunks is still detected.

Frontend:
- public/scripts/openai.js: add MINIMAX to noValidateSources so the key
  is accepted without a /models call; remove the dead saveModelList
  branch; clamp temperature to (0.0, 1.0] in createGenerationParameters.
- public/scripts/reasoning.js: add MINIMAX to the non-streaming
  reasoning_content extraction case (the backend transform now produces
  this field for MiniMax responses).
- public/scripts/slash-commands.js: add MINIMAX to the /api enum and
  add a MiniMax case to /api-url so users can switch endpoint by
  command.
- public/scripts/custom-request.js: pass minimax_endpoint through the
  override-payload merge alongside the other per-source endpoint fields.
- public/scripts/tokenizers.js: stop returning openai_model (which was
  always a MiniMax model id and thus an unknown tokenizer); fall back
  to gpt-3.5-turbo for a coarse but functional estimate.
- public/scripts/tool-calling.js: add MINIMAX to supportedSources so
  function-calling settings are exposed.
- public/index.html: drop the "-- Connect to the API --" placeholder
  option from the model select (the model list is hardcoded and always
  populated); remove minimax from the vision data-source attributes
  on the inline-media controls.
- public/img/minimax.svg: replace the multicolor brand SVG with a
  single-color currentColor version that matches the other provider
  icons in the connect panel.

* review: drop backend <think> parsing, defer to frontend

Per reviewer feedback: SillyTavern's reasoningHandler / reasoning_auto_parse
setting already extracts <think>...</think> blocks on the client side, so the
backend doesn't need to rewrite MiniMax responses. Removes the SSE Transform,
the non-streaming helper, and the corresponding case in reasoning.js.

* fix: remove isImageInliningSupported declaration for MINIMAX

* fix: remove MINIMAX from stream reasoning parsing

* fix: add to autoconnect logic

* fix: add missing MINIMAX models from docs

* fix: freq. and pres. pen aren't supported for MINIMAX

* fix: use clamp function for adjusting temperature

* fix: pass minimax_endpoint from connection profile to ChatCompletionService

* fix: update supported APIs in slash command documentation

* fix: replace bespoke merge with standard MERGE_TOOLS processing

* fix: add data-i18n attributes for headers

---------

Co-authored-by: octo-patch <octo-patch@users.noreply.github.com>
Co-authored-by: octo-patch <octo-patch@github.com>
Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
This commit is contained in:
Octopus
2026-04-24 05:43:05 +08:00
committed by GitHub
parent a028bec87b
commit aecbb9a2ee
13 changed files with 217 additions and 8 deletions
@@ -10,6 +10,8 @@
"mistralai_model": "mistral-large-latest", "mistralai_model": "mistral-large-latest",
"chutes_model": "deepseek-ai/DeepSeek-V3-0324", "chutes_model": "deepseek-ai/DeepSeek-V3-0324",
"chutes_sort_models": "alphabetically", "chutes_sort_models": "alphabetically",
"minimax_model": "MiniMax-M2.7",
"minimax_endpoint": "global",
"electronhub_model": "gpt-4o-mini", "electronhub_model": "gpt-4o-mini",
"electronhub_sort_models": "alphabetically", "electronhub_sort_models": "alphabetically",
"electronhub_group_models": false, "electronhub_group_models": false,
+1
View File
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Minimax</title><path d="M16.278 2c1.156 0 2.093.927 2.093 2.07v12.501a.74.74 0 00.744.709.74.74 0 00.743-.709V9.099a2.06 2.06 0 012.071-2.049A2.06 2.06 0 0124 9.1v6.561a.649.649 0 01-.652.645.649.649 0 01-.653-.645V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v7.472a2.037 2.037 0 01-2.048 2.026 2.037 2.037 0 01-2.048-2.026v-12.5a.785.785 0 00-.788-.753.785.785 0 00-.789.752l-.001 15.904A2.037 2.037 0 0113.441 22a2.037 2.037 0 01-2.048-2.026V18.04c0-.356.292-.645.652-.645.36 0 .652.289.652.645v1.934c0 .263.142.506.372.638.23.131.514.131.744 0a.734.734 0 00.372-.638V4.07c0-1.143.937-2.07 2.093-2.07zm-5.674 0c1.156 0 2.093.927 2.093 2.07v11.523a.648.648 0 01-.652.645.648.648 0 01-.652-.645V4.07a.785.785 0 00-.789-.78.785.785 0 00-.789.78v14.013a2.06 2.06 0 01-2.07 2.048 2.06 2.06 0 01-2.071-2.048V9.1a.762.762 0 00-.766-.758.762.762 0 00-.766.758v3.8a2.06 2.06 0 01-2.071 2.049A2.06 2.06 0 010 12.9v-1.378c0-.357.292-.646.652-.646.36 0 .653.29.653.646V12.9c0 .418.343.757.766.757s.766-.339.766-.757V9.099a2.06 2.06 0 012.07-2.048 2.06 2.06 0 012.071 2.048v8.984c0 .419.343.758.767.758.423 0 .766-.339.766-.758V4.07c0-1.143.937-2.07 2.093-2.07z"></path></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+30 -3
View File
@@ -697,7 +697,7 @@
</span> </span>
</div> </div>
</div> </div>
<div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,electronhub,chutes,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai,workers_ai"> <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,minimax,electronhub,chutes,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai,workers_ai">
<div class="range-block-title" data-i18n="Temperature"> <div class="range-block-title" data-i18n="Temperature">
Temperature Temperature
</div> </div>
@@ -749,7 +749,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,electronhub,chutes,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai,workers_ai"> <div class="range-block" data-source="openai,claude,aimlapi,openrouter,ai21,makersuite,vertexai,mistralai,custom,cohere,perplexity,groq,siliconflow,minimax,electronhub,chutes,nanogpt,deepseek,xai,pollinations,moonshot,fireworks,cometapi,azure_openai,zai,workers_ai">
<div class="range-block-title" data-i18n="Top P"> <div class="range-block-title" data-i18n="Top P">
Top P Top P
</div> </div>
@@ -1997,7 +1997,7 @@
</b> </b>
</div> </div>
</div> </div>
<div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,siliconflow,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi,electronhub,chutes,azure_openai,zai,nanogpt,workers_ai"> <div class="range-block" data-source="openai,cohere,mistralai,custom,claude,aimlapi,openrouter,groq,siliconflow,minimax,deepseek,makersuite,vertexai,ai21,xai,pollinations,moonshot,fireworks,cometapi,electronhub,chutes,azure_openai,zai,nanogpt,workers_ai">
<label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand"> <label for="openai_function_calling" class="checkbox_label flexWrap widthFreeExpand">
<input id="openai_function_calling" type="checkbox" /> <input id="openai_function_calling" type="checkbox" />
<span data-i18n="Enable function calling">Enable function calling</span> <span data-i18n="Enable function calling">Enable function calling</span>
@@ -2907,6 +2907,7 @@
<option value="makersuite">Google AI Studio</option> <option value="makersuite">Google AI Studio</option>
<option value="vertexai">Google Vertex AI</option> <option value="vertexai">Google Vertex AI</option>
<option value="mistralai">MistralAI</option> <option value="mistralai">MistralAI</option>
<option value="minimax">MiniMax</option>
<option value="moonshot">Moonshot AI</option> <option value="moonshot">Moonshot AI</option>
<option value="nanogpt">NanoGPT</option> <option value="nanogpt">NanoGPT</option>
<option value="openrouter">OpenRouter</option> <option value="openrouter">OpenRouter</option>
@@ -3571,6 +3572,32 @@
<option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option> <option value="" data-i18n="-- Connect to the API --">-- Connect to the API --</option>
</select> </select>
</div> </div>
<div id="minimax_form" data-source="minimax">
<h4 data-i18n="MiniMax API Key">MiniMax API Key</h4>
<div class="flex-container">
<input id="api_key_minimax" name="api_key_minimax" 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_minimax"></div>
</div>
<div data-for="api_key_minimax" 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 data-i18n="MiniMax Endpoint">MiniMax Endpoint</h4>
<select id="minimax_endpoint">
<option value="global" data-i18n="Global (minimax.io)">Global (minimax.io)</option>
<option value="cn" data-i18n="China (minimaxi.com)">China (minimaxi.com)</option>
</select>
<h4 data-i18n="MiniMax Model">MiniMax Model</h4>
<select id="model_minimax_select">
<option value="MiniMax-M2.7">MiniMax-M2.7</option>
<option value="MiniMax-M2.7-highspeed">MiniMax-M2.7-highspeed</option>
<option value="MiniMax-M2.5">MiniMax-M2.5</option>
<option value="MiniMax-M2.5-highspeed">MiniMax-M2.5-highspeed</option>
<option value="MiniMax-M2.1">MiniMax-M2.1</option>
<option value="MiniMax-M2.1-highspeed">MiniMax-M2.1-highspeed</option>
<option value="MiniMax-M2">MiniMax-M2</option>
<option value="M2-her">M2-her</option>
</select>
</div>
<div id="electronhub_form" data-source="electronhub"> <div id="electronhub_form" data-source="electronhub">
<h4 data-i18n="Electron Hub API Key">Electron Hub API Key</h4> <h4 data-i18n="Electron Hub API Key">Electron Hub API Key</h4>
<div> <div>
+1
View File
@@ -408,6 +408,7 @@ function RA_autoconnect(PrevApi) {
|| (secret_state[SECRET_KEYS.ZAI] && oai_settings.chat_completion_source == chat_completion_sources.ZAI) || (secret_state[SECRET_KEYS.ZAI] && oai_settings.chat_completion_source == chat_completion_sources.ZAI)
|| (secret_state[SECRET_KEYS.POLLINATIONS] && oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS) || (secret_state[SECRET_KEYS.POLLINATIONS] && oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS)
|| (secret_state[SECRET_KEYS.WORKERS_AI] && oai_settings.chat_completion_source == chat_completion_sources.WORKERS_AI) || (secret_state[SECRET_KEYS.WORKERS_AI] && oai_settings.chat_completion_source == chat_completion_sources.WORKERS_AI)
|| (secret_state[SECRET_KEYS.MINIMAX] && oai_settings.chat_completion_source == chat_completion_sources.MINIMAX)
|| (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
|| (secret_state[SECRET_KEYS.AZURE_OPENAI] && oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI) || (secret_state[SECRET_KEYS.AZURE_OPENAI] && oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI)
) { ) {
+1 -1
View File
@@ -591,7 +591,7 @@ export class ChatCompletionService {
} }
// Ensure api-url is properly applied for all sources that accept it // Ensure api-url is properly applied for all sources that accept it
['custom_url', 'vertexai_region', 'zai_endpoint', 'siliconflow_endpoint'].forEach(field => { ['custom_url', 'vertexai_region', 'zai_endpoint', 'siliconflow_endpoint', 'minimax_endpoint'].forEach(field => {
// The order is: connection profile => CC preset => CC settings // The order is: connection profile => CC preset => CC settings
overridePayload[field] = overridePayload[field] || settings[field] || oai_settings[field]; overridePayload[field] = overridePayload[field] || settings[field] || oai_settings[field];
}); });
+1
View File
@@ -448,6 +448,7 @@ export class ConnectionManagerRequestService {
vertexai_region: profile['api-url'], vertexai_region: profile['api-url'],
zai_endpoint: profile['api-url'], zai_endpoint: profile['api-url'],
siliconflow_endpoint: profile['api-url'], siliconflow_endpoint: profile['api-url'],
minimax_endpoint: profile['api-url'],
reverse_proxy: proxyPreset?.url, reverse_proxy: proxyPreset?.url,
proxy_password: proxyPreset?.password, proxy_password: proxyPreset?.password,
custom_prompt_post_processing: profile['prompt-post-processing'], custom_prompt_post_processing: profile['prompt-post-processing'],
+52
View File
@@ -47,6 +47,7 @@ import { SECRET_KEYS, secret_state, writeSecret } from './secrets.js';
import { getEventSourceStream } from './sse-stream.js'; import { getEventSourceStream } from './sse-stream.js';
import { import {
clamp,
createThumbnail, createThumbnail,
delay, delay,
download, download,
@@ -197,6 +198,7 @@ export const chat_completion_sources = {
ZAI: 'zai', ZAI: 'zai',
SILICONFLOW: 'siliconflow', SILICONFLOW: 'siliconflow',
WORKERS_AI: 'workers_ai', WORKERS_AI: 'workers_ai',
MINIMAX: 'minimax',
}; };
const character_names_behavior = { const character_names_behavior = {
@@ -270,6 +272,11 @@ export const SILICONFLOW_ENDPOINT = {
CN: 'cn', CN: 'cn',
}; };
export const MINIMAX_ENDPOINT = {
GLOBAL: 'global',
CN: 'cn',
};
const sensitiveFields = [ const sensitiveFields = [
'reverse_proxy', 'reverse_proxy',
'proxy_password', 'proxy_password',
@@ -319,6 +326,8 @@ export const settingsToUpdate = {
chutes_sort_models: ['#chutes_sort_models', 'chutes_sort_models', false, true], chutes_sort_models: ['#chutes_sort_models', 'chutes_sort_models', false, true],
siliconflow_model: ['#model_siliconflow_select', 'siliconflow_model', false, true], siliconflow_model: ['#model_siliconflow_select', 'siliconflow_model', false, true],
siliconflow_endpoint: ['#siliconflow_endpoint', 'siliconflow_endpoint', false, true], siliconflow_endpoint: ['#siliconflow_endpoint', 'siliconflow_endpoint', false, true],
minimax_model: ['#model_minimax_select', 'minimax_model', false, true],
minimax_endpoint: ['#minimax_endpoint', 'minimax_endpoint', false, true],
electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true], electronhub_model: ['#model_electronhub_select', 'electronhub_model', false, true],
electronhub_sort_models: ['#electronhub_sort_models', 'electronhub_sort_models', false, true], electronhub_sort_models: ['#electronhub_sort_models', 'electronhub_sort_models', false, true],
electronhub_group_models: ['#electronhub_group_models', 'electronhub_group_models', false, true], electronhub_group_models: ['#electronhub_group_models', 'electronhub_group_models', false, true],
@@ -431,6 +440,8 @@ const default_settings = {
chutes_sort_models: 'alphabetically', chutes_sort_models: 'alphabetically',
siliconflow_model: 'deepseek-ai/DeepSeek-V3', siliconflow_model: 'deepseek-ai/DeepSeek-V3',
siliconflow_endpoint: SILICONFLOW_ENDPOINT.GLOBAL, siliconflow_endpoint: SILICONFLOW_ENDPOINT.GLOBAL,
minimax_model: 'MiniMax-M2.7',
minimax_endpoint: MINIMAX_ENDPOINT.GLOBAL,
electronhub_model: 'gpt-4o-mini', electronhub_model: 'gpt-4o-mini',
electronhub_sort_models: 'alphabetically', electronhub_sort_models: 'alphabetically',
electronhub_group_models: false, electronhub_group_models: false,
@@ -1712,6 +1723,8 @@ export function getChatCompletionModel(settings = null) {
return settings.groq_model; return settings.groq_model;
case chat_completion_sources.SILICONFLOW: case chat_completion_sources.SILICONFLOW:
return settings.siliconflow_model; return settings.siliconflow_model;
case chat_completion_sources.MINIMAX:
return settings.minimax_model;
case chat_completion_sources.ELECTRONHUB: case chat_completion_sources.ELECTRONHUB:
return settings.electronhub_model; return settings.electronhub_model;
case chat_completion_sources.CHUTES: case chat_completion_sources.CHUTES:
@@ -2845,6 +2858,14 @@ export async function createGenerationParameters(settings, model, type, messages
generate_data.siliconflow_endpoint = settings.siliconflow_endpoint || SILICONFLOW_ENDPOINT.GLOBAL; generate_data.siliconflow_endpoint = settings.siliconflow_endpoint || SILICONFLOW_ENDPOINT.GLOBAL;
} }
if (settings.chat_completion_source === chat_completion_sources.MINIMAX) {
generate_data.minimax_endpoint = settings.minimax_endpoint || MINIMAX_ENDPOINT.GLOBAL;
// MiniMax requires temperature in (0.0, 1.0]; zero is rejected.
if (Number.isFinite(generate_data.temperature)) {
generate_data.temperature = clamp(generate_data.temperature, Number.EPSILON, 1.0);
}
}
if (settings.chat_completion_source === chat_completion_sources.WORKERS_AI) { if (settings.chat_completion_source === chat_completion_sources.WORKERS_AI) {
generate_data.workers_ai_account_id = settings.workers_ai_account_id; generate_data.workers_ai_account_id = settings.workers_ai_account_id;
generate_data.top_k = settings.top_k_openai > 0 ? Math.min(Number(settings.top_k_openai), 50) : undefined; generate_data.top_k = settings.top_k_openai > 0 ? Math.min(Number(settings.top_k_openai), 50) : undefined;
@@ -4255,6 +4276,7 @@ async function getStatusOpen() {
chat_completion_sources.VERTEXAI, chat_completion_sources.VERTEXAI,
chat_completion_sources.PERPLEXITY, chat_completion_sources.PERPLEXITY,
chat_completion_sources.ZAI, chat_completion_sources.ZAI,
chat_completion_sources.MINIMAX,
]; ];
if (noValidateSources.includes(oai_settings.chat_completion_source)) { if (noValidateSources.includes(oai_settings.chat_completion_source)) {
let status = t`Key saved; press \"Test Message\" to verify.`; let status = t`Key saved; press \"Test Message\" to verify.`;
@@ -4312,6 +4334,10 @@ async function getStatusOpen() {
data.siliconflow_endpoint = oai_settings.siliconflow_endpoint; data.siliconflow_endpoint = oai_settings.siliconflow_endpoint;
} }
if (oai_settings.chat_completion_source === chat_completion_sources.MINIMAX) {
data.minimax_endpoint = oai_settings.minimax_endpoint;
}
if (oai_settings.chat_completion_source === chat_completion_sources.WORKERS_AI) { if (oai_settings.chat_completion_source === chat_completion_sources.WORKERS_AI) {
data.workers_ai_account_id = oai_settings.workers_ai_account_id; data.workers_ai_account_id = oai_settings.workers_ai_account_id;
} }
@@ -5266,6 +5292,15 @@ async function onModelChange() {
oai_settings.siliconflow_model = value; oai_settings.siliconflow_model = value;
} }
if ($(this).is('#model_minimax_select')) {
if (!value) {
console.debug('Null MiniMax model selected. Ignoring.');
return;
}
console.log('MiniMax model changed to', value);
oai_settings.minimax_model = value;
}
if ($(this).is('#model_electronhub_select')) { if ($(this).is('#model_electronhub_select')) {
if (!value || !hasModelsLoaded) { if (!value || !hasModelsLoaded) {
console.debug('Null ElectronHub model selected. Ignoring.'); console.debug('Null ElectronHub model selected. Ignoring.');
@@ -5707,6 +5742,15 @@ async function onModelChange() {
$('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input'); $('#temp_openai').attr('max', oai_max_temp).val(oai_settings.temp_openai).trigger('input');
} }
if (oai_settings.chat_completion_source === chat_completion_sources.MINIMAX) {
const maxContext = oai_settings.minimax_model === 'M2-her' ? 65536 : 204800;
$('#openai_max_context').attr('max', maxContext);
oai_settings.openai_max_context = Math.min(Number($('#openai_max_context').attr('max')), oai_settings.openai_max_context);
$('#openai_max_context').val(oai_settings.openai_max_context).trigger('input');
oai_settings.temp_openai = Math.min(claude_max_temp, oai_settings.temp_openai);
$('#temp_openai').attr('max', claude_max_temp).val(oai_settings.temp_openai).trigger('input');
}
if (oai_settings.chat_completion_source == chat_completion_sources.ZAI) { if (oai_settings.chat_completion_source == chat_completion_sources.ZAI) {
const maxContext = getZaiMaxContext(oai_settings.zai_model, oai_settings.max_context_unlocked); const maxContext = getZaiMaxContext(oai_settings.zai_model, oai_settings.max_context_unlocked);
$('#openai_max_context').attr('max', maxContext); $('#openai_max_context').attr('max', maxContext);
@@ -5780,6 +5824,7 @@ async function onConnectButtonClick(e) {
[chat_completion_sources.CHUTES]: { key: SECRET_KEYS.CHUTES, selector: '#api_key_chutes', proxy: false }, [chat_completion_sources.CHUTES]: { key: SECRET_KEYS.CHUTES, selector: '#api_key_chutes', proxy: false },
[chat_completion_sources.POLLINATIONS]: { key: SECRET_KEYS.POLLINATIONS, selector: '#api_key_pollinations', proxy: false }, [chat_completion_sources.POLLINATIONS]: { key: SECRET_KEYS.POLLINATIONS, selector: '#api_key_pollinations', proxy: false },
[chat_completion_sources.WORKERS_AI]: { key: SECRET_KEYS.WORKERS_AI, selector: '#api_key_workers_ai', proxy: false }, [chat_completion_sources.WORKERS_AI]: { key: SECRET_KEYS.WORKERS_AI, selector: '#api_key_workers_ai', proxy: false },
[chat_completion_sources.MINIMAX]: { key: SECRET_KEYS.MINIMAX, selector: '#api_key_minimax', proxy: false },
}; };
// Vertex AI Express version - use API key // Vertex AI Express version - use API key
@@ -5845,6 +5890,8 @@ function toggleChatCompletionForms() {
$('#model_chutes_select').trigger('change'); $('#model_chutes_select').trigger('change');
} else if (oai_settings.chat_completion_source == chat_completion_sources.SILICONFLOW) { } else if (oai_settings.chat_completion_source == chat_completion_sources.SILICONFLOW) {
$('#model_siliconflow_select').trigger('change'); $('#model_siliconflow_select').trigger('change');
} else if (oai_settings.chat_completion_source == chat_completion_sources.MINIMAX) {
$('#model_minimax_select').trigger('change');
} else if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) { } else if (oai_settings.chat_completion_source == chat_completion_sources.ELECTRONHUB) {
$('#model_electronhub_select').trigger('change'); $('#model_electronhub_select').trigger('change');
} else if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) { } else if (oai_settings.chat_completion_source == chat_completion_sources.NANOGPT) {
@@ -7028,6 +7075,10 @@ export function initOpenAI() {
oai_settings.siliconflow_endpoint = String($(this).val()); oai_settings.siliconflow_endpoint = String($(this).val());
saveSettingsDebounced(); saveSettingsDebounced();
}); });
$('#minimax_endpoint').on('input', function () {
oai_settings.minimax_endpoint = String($(this).val());
saveSettingsDebounced();
});
$('#workers_ai_account_id').on('input', function () { $('#workers_ai_account_id').on('input', function () {
oai_settings.workers_ai_account_id = String($(this).val()); oai_settings.workers_ai_account_id = String($(this).val());
saveSettingsDebounced(); saveSettingsDebounced();
@@ -7048,6 +7099,7 @@ export function initOpenAI() {
$('#model_groq_select').on('change', onModelChange); $('#model_groq_select').on('change', onModelChange);
$('#model_chutes_select').on('change', onModelChange); $('#model_chutes_select').on('change', onModelChange);
$('#model_siliconflow_select').on('change', onModelChange); $('#model_siliconflow_select').on('change', onModelChange);
$('#model_minimax_select').on('change', onModelChange);
$('#model_electronhub_select').on('change', onModelChange); $('#model_electronhub_select').on('change', onModelChange);
$('#model_nanogpt_select').on('change', onModelChange); $('#model_nanogpt_select').on('change', onModelChange);
$('#model_deepseek_select').on('change', onModelChange); $('#model_deepseek_select').on('change', onModelChange);
+2 -1
View File
@@ -130,7 +130,7 @@ const FRIENDLY_NAMES = {
[SECRET_KEYS.LINGVA_URL]: 'Lingva Endpoint (e.g. https://lingva.ml/api/v1)', [SECRET_KEYS.LINGVA_URL]: 'Lingva Endpoint (e.g. https://lingva.ml/api/v1)',
[SECRET_KEYS.ONERING_URL]: 'OneRingTranslator Endpoint (e.g. http://127.0.0.1:4990/translate)', [SECRET_KEYS.ONERING_URL]: 'OneRingTranslator Endpoint (e.g. http://127.0.0.1:4990/translate)',
[SECRET_KEYS.DEEPLX_URL]: 'DeepLX Endpoint (e.g. http://127.0.0.1:1188/translate)', [SECRET_KEYS.DEEPLX_URL]: 'DeepLX Endpoint (e.g. http://127.0.0.1:1188/translate)',
[SECRET_KEYS.MINIMAX]: 'MiniMax TTS', [SECRET_KEYS.MINIMAX]: 'MiniMax',
[SECRET_KEYS.MINIMAX_GROUP_ID]: 'MiniMax Group ID', [SECRET_KEYS.MINIMAX_GROUP_ID]: 'MiniMax Group ID',
[SECRET_KEYS.MOONSHOT]: 'Moonshot AI', [SECRET_KEYS.MOONSHOT]: 'Moonshot AI',
[SECRET_KEYS.COMETAPI]: 'CometAPI', [SECRET_KEYS.COMETAPI]: 'CometAPI',
@@ -184,6 +184,7 @@ const INPUT_MAP = {
[SECRET_KEYS.AZURE_OPENAI]: '#api_key_azure_openai', [SECRET_KEYS.AZURE_OPENAI]: '#api_key_azure_openai',
[SECRET_KEYS.ZAI]: '#api_key_zai', [SECRET_KEYS.ZAI]: '#api_key_zai',
[SECRET_KEYS.SILICONFLOW]: '#api_key_siliconflow', [SECRET_KEYS.SILICONFLOW]: '#api_key_siliconflow',
[SECRET_KEYS.MINIMAX]: '#api_key_minimax',
[SECRET_KEYS.POLLINATIONS]: '#api_key_pollinations', [SECRET_KEYS.POLLINATIONS]: '#api_key_pollinations',
[SECRET_KEYS.WORKERS_AI]: '#api_key_workers_ai', [SECRET_KEYS.WORKERS_AI]: '#api_key_workers_ai',
}; };
+30 -2
View File
@@ -70,7 +70,7 @@ import { hideChatMessageRange } from './chats.js';
import { getContext, saveMetadataDebounced } from './extensions.js'; import { getContext, saveMetadataDebounced } from './extensions.js';
import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
import { findGroupMemberId, groups, is_group_generating, openGroupById, regenerateGroup, resetSelectedGroup, saveGroupChat, selected_group, getGroupMembers } from './group-chats.js'; import { findGroupMemberId, groups, is_group_generating, openGroupById, regenerateGroup, resetSelectedGroup, saveGroupChat, selected_group, getGroupMembers } from './group-chats.js';
import { chat_completion_sources, oai_settings, promptManager, SILICONFLOW_ENDPOINT, ZAI_ENDPOINT } from './openai.js'; import { chat_completion_sources, MINIMAX_ENDPOINT, oai_settings, promptManager, SILICONFLOW_ENDPOINT, ZAI_ENDPOINT } from './openai.js';
import { user_avatar } from './personas.js'; import { user_avatar } from './personas.js';
import { addEphemeralStoppingString, chat_styles, context_presets, flushEphemeralStoppingStrings, playMessageSound, power_user } from './power-user.js'; import { addEphemeralStoppingString, chat_styles, context_presets, flushEphemeralStoppingStrings, playMessageSound, power_user } from './power-user.js';
import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js';
@@ -3134,6 +3134,7 @@ export function initDefaultSlashCommands() {
new SlashCommandEnumValue('zai', 'Z.AI', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'zai')), 'Z'), new SlashCommandEnumValue('zai', 'Z.AI', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'zai')), 'Z'),
new SlashCommandEnumValue('vertexai', 'Google Vertex AI', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'vertexai')), 'V'), new SlashCommandEnumValue('vertexai', 'Google Vertex AI', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'vertexai')), 'V'),
new SlashCommandEnumValue('siliconflow', 'SiliconFlow', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'siliconflow')), 'S'), new SlashCommandEnumValue('siliconflow', 'SiliconFlow', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'siliconflow')), 'S'),
new SlashCommandEnumValue('minimax', 'MiniMax', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'minimax')), 'M'),
new SlashCommandEnumValue('kobold', 'KoboldAI Classic', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'kobold')), 'K'), new SlashCommandEnumValue('kobold', 'KoboldAI Classic', enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'kobold')), 'K'),
...Object.values(textgen_types).filter(api => Object.keys(SERVER_INPUTS).includes(api)).map(api => new SlashCommandEnumValue(api, null, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'textgenerationwebui')), 'T')), ...Object.values(textgen_types).filter(api => Object.keys(SERVER_INPUTS).includes(api)).map(api => new SlashCommandEnumValue(api, null, enumTypes.getBasedOnIndex(UNIQUE_APIS.findIndex(x => x === 'textgenerationwebui')), 'T')),
], ],
@@ -3167,7 +3168,7 @@ export function initDefaultSlashCommands() {
${t`If a manual API is provided to <b>set</b> the URL, make sure to set <code>connect=false</code>, as auto-connect only works for the currently selected API, or consider switching to it with <code>/api</code> first.`} ${t`If a manual API is provided to <b>set</b> the URL, make sure to set <code>connect=false</code>, as auto-connect only works for the currently selected API, or consider switching to it with <code>/api</code> first.`}
</div> </div>
<div> <div>
${t`This slash command works for most of the Text Completion sources, KoboldAI Classic, and also Custom OpenAI compatible, Z.AI, SiliconFlow, and Google Vertex AI for the Chat Completion sources. If unsure which APIs are supported, check the auto-completion of the optional <code>api</code> argument of this command.`} ${t`This slash command works for most of the Text Completion sources, KoboldAI Classic, and also Custom OpenAI compatible, Z.AI, SiliconFlow, MiniMax, and Google Vertex AI for the Chat Completion sources. If unsure which APIs are supported, check the auto-completion of the optional <code>api</code> argument of this command.`}
</div> </div>
`, `,
})); }));
@@ -6261,6 +6262,7 @@ function getModelOptions(quiet) {
{ id: 'model_groq_select', api: 'openai', type: chat_completion_sources.GROQ }, { id: 'model_groq_select', api: 'openai', type: chat_completion_sources.GROQ },
{ id: 'model_chutes_select', api: 'openai', type: chat_completion_sources.CHUTES }, { id: 'model_chutes_select', api: 'openai', type: chat_completion_sources.CHUTES },
{ id: 'model_siliconflow_select', api: 'openai', type: chat_completion_sources.SILICONFLOW }, { id: 'model_siliconflow_select', api: 'openai', type: chat_completion_sources.SILICONFLOW },
{ id: 'model_minimax_select', api: 'openai', type: chat_completion_sources.MINIMAX },
{ id: 'model_electronhub_select', api: 'openai', type: chat_completion_sources.ELECTRONHUB }, { id: 'model_electronhub_select', api: 'openai', type: chat_completion_sources.ELECTRONHUB },
{ id: 'model_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT }, { id: 'model_nanogpt_select', api: 'openai', type: chat_completion_sources.NANOGPT },
{ id: 'model_deepseek_select', api: 'openai', type: chat_completion_sources.DEEPSEEK }, { id: 'model_deepseek_select', api: 'openai', type: chat_completion_sources.DEEPSEEK },
@@ -6628,6 +6630,32 @@ async function setApiUrlCallback({ api = null, connect = 'true', quiet = 'false'
return oai_settings.siliconflow_endpoint || SILICONFLOW_ENDPOINT.GLOBAL; return oai_settings.siliconflow_endpoint || SILICONFLOW_ENDPOINT.GLOBAL;
} }
const isCurrentlyMinimax = main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.MINIMAX;
if (api === chat_completion_sources.MINIMAX || (!api && isCurrentlyMinimax)) {
if (!url) {
return oai_settings.minimax_endpoint || MINIMAX_ENDPOINT.GLOBAL;
}
const permittedValues = Object.values(MINIMAX_ENDPOINT);
if (!permittedValues.includes(url)) {
!isQuiet && toastr.warning(t`Valid options are: ${permittedValues.join(', ')}`, t`MiniMax endpoint '${url}' is not a valid option.`);
return '';
}
if (!isCurrentlyMinimax && autoConnect) {
toastr.warning(t`MiniMax is not the currently selected API, so we cannot do an auto-connect. Consider switching to it via /api beforehand.`);
return '';
}
$('#minimax_endpoint').val(url).trigger('input');
if (autoConnect) {
$('#api_button_openai').trigger('click');
}
return oai_settings.minimax_endpoint || MINIMAX_ENDPOINT.GLOBAL;
}
const isCurrentlyVertexAI = main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.VERTEXAI; const isCurrentlyVertexAI = main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.VERTEXAI;
if (api === chat_completion_sources.VERTEXAI || (!api && isCurrentlyVertexAI)) { if (api === chat_completion_sources.VERTEXAI || (!api && isCurrentlyVertexAI)) {
const defaultRegion = 'us-central1'; const defaultRegion = 'us-central1';
+5
View File
@@ -694,6 +694,11 @@ export function getTokenizerModel() {
} }
} }
if (oai_settings.chat_completion_source == chat_completion_sources.MINIMAX) {
// MiniMax uses a proprietary tokenizer; fall back to a coarse OpenAI estimation.
return 'gpt-3.5-turbo';
}
if (oai_settings.chat_completion_source == chat_completion_sources.WORKERS_AI && oai_settings.workers_ai_model) { if (oai_settings.chat_completion_source == chat_completion_sources.WORKERS_AI && oai_settings.workers_ai_model) {
const model = oai_settings.workers_ai_model.toLowerCase(); const model = oai_settings.workers_ai_model.toLowerCase();
+1
View File
@@ -667,6 +667,7 @@ export class ToolManager {
chat_completion_sources.SILICONFLOW, chat_completion_sources.SILICONFLOW,
chat_completion_sources.NANOGPT, chat_completion_sources.NANOGPT,
chat_completion_sources.WORKERS_AI, chat_completion_sources.WORKERS_AI,
chat_completion_sources.MINIMAX,
]; ];
return supportedSources.includes(settings.chat_completion_source); return supportedSources.includes(settings.chat_completion_source);
} }
+6
View File
@@ -209,6 +209,7 @@ export const CHAT_COMPLETION_SOURCES = {
AZURE_OPENAI: 'azure_openai', AZURE_OPENAI: 'azure_openai',
ZAI: 'zai', ZAI: 'zai',
SILICONFLOW: 'siliconflow', SILICONFLOW: 'siliconflow',
MINIMAX: 'minimax',
WORKERS_AI: 'workers_ai', WORKERS_AI: 'workers_ai',
}; };
@@ -557,3 +558,8 @@ export const SILICONFLOW_ENDPOINT = {
GLOBAL: 'global', GLOBAL: 'global',
CN: 'cn', CN: 'cn',
}; };
export const MINIMAX_ENDPOINT = {
GLOBAL: 'global',
CN: 'cn',
};
+85 -1
View File
@@ -18,6 +18,7 @@ import {
OPENROUTER_HEADERS, OPENROUTER_HEADERS,
VERTEX_SAFETY, VERTEX_SAFETY,
SILICONFLOW_ENDPOINT, SILICONFLOW_ENDPOINT,
MINIMAX_ENDPOINT,
ZAI_ENDPOINT, ZAI_ENDPOINT,
} from '../../constants.js'; } from '../../constants.js';
import { import {
@@ -89,6 +90,8 @@ const API_ZAI_COMMON = 'https://api.z.ai/api/paas/v4';
const API_ZAI_CODING = 'https://api.z.ai/api/coding/paas/v4'; const API_ZAI_CODING = 'https://api.z.ai/api/coding/paas/v4';
const API_SILICONFLOW = 'https://api.siliconflow.com/v1'; const API_SILICONFLOW = 'https://api.siliconflow.com/v1';
const API_SILICONFLOW_CN = 'https://api.siliconflow.cn/v1'; const API_SILICONFLOW_CN = 'https://api.siliconflow.cn/v1';
const API_MINIMAX = 'https://api.minimax.io/v1';
const API_MINIMAX_CN = 'https://api.minimaxi.com/v1';
const API_OPENROUTER = 'https://openrouter.ai/api/v1'; const API_OPENROUTER = 'https://openrouter.ai/api/v1';
const API_WORKERS_AI = 'https://api.cloudflare.com/client/v4/accounts'; const API_WORKERS_AI = 'https://api.cloudflare.com/client/v4/accounts';
@@ -1552,7 +1555,87 @@ async function sendChutesRequest(request, response) {
} }
/** /**
* Sends a chat completion request to Azure OpenAI. * Sends a request to MiniMax.
* @param {express.Request} request Express request
* @param {express.Response} response Express response
*/
async function sendMinimaxRequest(request, response) {
const apiUrl = request.body.minimax_endpoint === MINIMAX_ENDPOINT.CN
? API_MINIMAX_CN : API_MINIMAX;
const apiKey = readSecret(request.user.directories, SECRET_KEYS.MINIMAX, request.body.secret_id);
if (!apiKey) {
console.warn('MiniMax 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 {
// MiniMax does not allow consecutive messages with the same role.
// Merge them into a single message to avoid "invalid chat setting (2013)".
const messages = postProcessPrompt(request.body.messages, PROMPT_PROCESSING_TYPE.MERGE_TOOLS, getPromptNames(request));
let bodyParams = {};
if (Array.isArray(request.body.tools) && request.body.tools.length > 0) {
bodyParams['tools'] = request.body.tools;
bodyParams['tool_choice'] = request.body.tool_choice;
}
const requestBody = {
'messages': messages,
'model': request.body.model,
'temperature': request.body.temperature,
'max_tokens': request.body.model === 'M2-her' ? Math.min(request.body.max_tokens, 2048) : request.body.max_tokens,
'stream': request.body.stream,
'top_p': request.body.top_p,
'stop': request.body.stop,
...bodyParams,
};
const config = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey,
},
body: JSON.stringify(requestBody),
signal: controller.signal,
};
console.debug('MiniMax request:', requestBody);
const generateResponse = await fetch(apiUrl + '/chat/completions', config);
if (request.body.stream) {
await forwardFetchResponse(generateResponse, response);
} else {
if (!generateResponse.ok) {
const errorText = await generateResponse.text();
console.warn('MiniMax returned error: ', errorText);
const errorJson = tryParse(errorText) ?? { error: true };
return response.status(500).send(errorJson);
}
const generateResponseJson = await generateResponse.json();
console.debug('MiniMax response:', generateResponseJson);
return response.send(generateResponseJson);
}
} catch (error) {
console.error('Error communicating with MiniMax: ', error);
if (!response.headersSent) {
response.send({ error: true });
} else {
response.end();
}
}
}
/**
* @param {express.Request} request Express request object (contains request.body with all generate_data) * @param {express.Request} request Express request object (contains request.body with all generate_data)
* @param {express.Response} response Express response object * @param {express.Response} response Express response object
*/ */
@@ -2096,6 +2179,7 @@ router.post('/generate', async function (request, response) {
case CHAT_COMPLETION_SOURCES.AIMLAPI: return await sendAimlapiRequest(request, response); case CHAT_COMPLETION_SOURCES.AIMLAPI: return await sendAimlapiRequest(request, response);
case CHAT_COMPLETION_SOURCES.XAI: return await sendXaiRequest(request, response); case CHAT_COMPLETION_SOURCES.XAI: return await sendXaiRequest(request, response);
case CHAT_COMPLETION_SOURCES.CHUTES: return await sendChutesRequest(request, response); case CHAT_COMPLETION_SOURCES.CHUTES: return await sendChutesRequest(request, response);
case CHAT_COMPLETION_SOURCES.MINIMAX: return await sendMinimaxRequest(request, response);
case CHAT_COMPLETION_SOURCES.ELECTRONHUB: return await sendElectronHubRequest(request, response); case CHAT_COMPLETION_SOURCES.ELECTRONHUB: return await sendElectronHubRequest(request, response);
case CHAT_COMPLETION_SOURCES.AZURE_OPENAI: return await sendAzureOpenAIRequest(request, response); case CHAT_COMPLETION_SOURCES.AZURE_OPENAI: return await sendAzureOpenAIRequest(request, response);
} }