diff --git a/public/img/moonshot.svg b/public/img/moonshot.svg new file mode 100644 index 000000000..f48c68a80 --- /dev/null +++ b/public/img/moonshot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/index.html b/public/index.html index 1c166b677..4a59a8f6b 100644 --- a/public/index.html +++ b/public/index.html @@ -652,7 +652,7 @@ -
+
Multiple swipes per generation
@@ -691,7 +691,7 @@
-
+
Temperature
@@ -704,7 +704,7 @@
-
+
Frequency Penalty
@@ -717,7 +717,7 @@
-
+
Presence Penalty
@@ -743,7 +743,7 @@
-
+
Top P
@@ -1984,7 +1984,7 @@
-
+
-
+
-
+
+
+

+ + Moonshot AI API Key + +

+
+ + +
+
+ For privacy reasons, your API key will be hidden after you click 'Connect'. +
+

Moonshot AI Model

+ +

diff --git a/public/scripts/RossAscends-mods.js b/public/scripts/RossAscends-mods.js index 6359be518..c289a3a06 100644 --- a/public/scripts/RossAscends-mods.js +++ b/public/scripts/RossAscends-mods.js @@ -410,6 +410,7 @@ function RA_autoconnect(PrevApi) { || (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) + || (secret_state[SECRET_KEYS.MOONSHOT] && oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT) || (oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS) || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM) ) { diff --git a/public/scripts/extensions/caption/index.js b/public/scripts/extensions/caption/index.js index 3bb012928..888aa7c8c 100644 --- a/public/scripts/extensions/caption/index.js +++ b/public/scripts/extensions/caption/index.js @@ -438,6 +438,7 @@ jQuery(async function () { 'groq': SECRET_KEYS.GROQ, 'cohere': SECRET_KEYS.COHERE, 'aimlapi': SECRET_KEYS.AIMLAPI, + 'moonshot': SECRET_KEYS.MOONSHOT, }; if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) { diff --git a/public/scripts/extensions/caption/settings.html b/public/scripts/extensions/caption/settings.html index d81bf39dc..4b13906fb 100644 --- a/public/scripts/extensions/caption/settings.html +++ b/public/scripts/extensions/caption/settings.html @@ -27,6 +27,7 @@ + @@ -51,6 +52,9 @@ + + + diff --git a/public/scripts/extensions/shared.js b/public/scripts/extensions/shared.js index aa25ccd11..17e5bc811 100644 --- a/public/scripts/extensions/shared.js +++ b/public/scripts/extensions/shared.js @@ -238,6 +238,10 @@ function throwIfInvalidModel(useReverseProxy) { if (multimodalApi === 'aimlapi' && !secret_state[SECRET_KEYS.AIMLAPI]) { throw new Error('AI/ML API key is not set.'); } + + if (multimodalApi === 'moonshot' && !secret_state[SECRET_KEYS.MOONSHOT]) { + throw new Error('Moonshot AI API key is not set.'); + } } /** diff --git a/public/scripts/openai.js b/public/scripts/openai.js index 1401d2f7b..1096449d0 100644 --- a/public/scripts/openai.js +++ b/public/scripts/openai.js @@ -183,6 +183,7 @@ export const chat_completion_sources = { AIMLAPI: 'aimlapi', XAI: 'xai', POLLINATIONS: 'pollinations', + MOONSHOT: 'moonshot', }; const character_names_behavior = { @@ -272,6 +273,7 @@ export const settingsToUpdate = { aimlapi_model: ['#model_aimlapi_select', 'aimlapi_model', false, true], xai_model: ['#model_xai_select', 'xai_model', false, true], pollinations_model: ['#model_pollinations_select', 'pollinations_model', false, true], + moonshot_model: ['#model_moonshot_select', 'moonshot_model', false, true], custom_model: ['#custom_model_id', 'custom_model', false, true], custom_url: ['#custom_api_url_text', 'custom_url', false, true], custom_include_body: ['#custom_include_body', 'custom_include_body', false, true], @@ -367,6 +369,7 @@ const default_settings = { aimlapi_model: 'gpt-4o-mini-2024-07-18', xai_model: 'grok-3-beta', pollinations_model: 'openai', + moonshot_model: 'kimi-latest', custom_model: '', custom_url: '', custom_include_body: '', @@ -453,6 +456,7 @@ const oai_settings = { aimlapi_model: 'gpt-4-turbo', xai_model: 'grok-3-beta', pollinations_model: 'openai', + moonshot_model: 'kimi-latest', custom_model: '', custom_url: '', custom_include_body: '', @@ -1613,6 +1617,8 @@ export function getChatCompletionModel(source = null) { return oai_settings.xai_model; case chat_completion_sources.POLLINATIONS: return oai_settings.pollinations_model; + case chat_completion_sources.MOONSHOT: + return oai_settings.moonshot_model; default: console.error(`Unknown chat completion source: ${activeSource}`); return ''; @@ -2038,13 +2044,14 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } = 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 isMoonshot = oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT; const isTextCompletion = isOAI && textCompletionModels.includes(oai_settings.openai_model); const isQuiet = type === 'quiet'; const isImpersonate = type === 'impersonate'; const isContinue = type === 'continue'; const stream = oai_settings.stream_openai && !isQuiet && !(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 || isAimlapi); + const canMultiSwipe = oai_settings.n > 1 && !isContinue && !isImpersonate && !isQuiet && (isOAI || isCustom || isXAI || isAimlapi || isMoonshot); const logitBiasSources = [chat_completion_sources.OPENAI, chat_completion_sources.OPENROUTER, chat_completion_sources.CUSTOM]; if (oai_settings.bias_preset_selected @@ -2384,7 +2391,7 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || ''); } return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? ''; - } else if ([chat_completion_sources.CUSTOM, chat_completion_sources.POLLINATIONS, chat_completion_sources.AIMLAPI].includes(chat_completion_source)) { + } else if ([chat_completion_sources.CUSTOM, chat_completion_sources.POLLINATIONS, chat_completion_sources.AIMLAPI, chat_completion_sources.MOONSHOT].includes(chat_completion_source)) { if (show_thoughts) { state.reasoning += data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ?? @@ -3345,6 +3352,7 @@ function loadOpenAISettings(data, settings) { oai_settings.aimlapi_model = settings.aimlapi_model ?? default_settings.aimlapi_model; oai_settings.xai_model = settings.xai_model ?? default_settings.xai_model; oai_settings.pollinations_model = settings.pollinations_model ?? default_settings.pollinations_model; + oai_settings.moonshot_model = settings.moonshot_model ?? default_settings.moonshot_model; oai_settings.custom_model = settings.custom_model ?? default_settings.custom_model; oai_settings.custom_url = settings.custom_url ?? default_settings.custom_url; oai_settings.custom_include_body = settings.custom_include_body ?? default_settings.custom_include_body; @@ -3442,6 +3450,8 @@ function loadOpenAISettings(data, settings) { $(`#model_xai_select option[value="${oai_settings.xai_model}"`).prop('selected', true); $('#model_pollinations_select').val(oai_settings.pollinations_model); $(`#model_pollinations_select option[value="${oai_settings.pollinations_model}"`).prop('selected', true); + $('#model_moonshot_select').val(oai_settings.moonshot_model); + $(`#model_moonshot_select option[value="${oai_settings.moonshot_model}"`).prop('selected', true); $('#custom_model_id').val(oai_settings.custom_model); $('#custom_api_url_text').val(oai_settings.custom_url); $('#openai_max_context').val(oai_settings.openai_max_context); @@ -3714,6 +3724,7 @@ async function saveOpenAIPreset(name, settings, triggerUi = true) { xai_model: settings.xai_model, pollinations_model: settings.pollinations_model, aimlapi_model: settings.aimlapi_model, + moonshot_model: settings.moonshot_model, custom_model: settings.custom_model, custom_url: settings.custom_url, custom_include_body: settings.custom_include_body, @@ -4424,6 +4435,28 @@ function getGroqMaxContext(model, isUnlocked) { return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || max_128k; } +function getMoonshotMaxContext(model, isUnlocked) { + if (isUnlocked) { + return unlocked_max; + } + + const contextMap = { + 'moonshot-v1-8k': max_8k, + 'moonshot-v1-32k': max_32k, + 'moonshot-v1-128k': max_128k, + 'moonshot-v1-auto': max_128k, + 'moonshot-v1-8k-vision-preview': max_8k, + 'moonshot-v1-32k-vision-preview': max_32k, + 'moonshot-v1-128k-vision-preview': max_128k, + 'kimi-k2-0711-preview': max_32k, + 'kimi-latest': max_32k, + 'kimi-thinking-preview': max_32k, + }; + + // Return context size if model found, otherwise default to 32k + return Object.entries(contextMap).find(([key]) => model.includes(key))?.[1] || max_32k; +} + async function onModelChange() { biasCache = undefined; let value = String($(this).val() || ''); @@ -4559,6 +4592,11 @@ async function onModelChange() { oai_settings.xai_model = value; } + if (value && $(this).is('#model_moonshot_select')) { + console.log('Moonshot model changed to', value); + oai_settings.moonshot_model = value; + } + if ([chat_completion_sources.MAKERSUITE, chat_completion_sources.VERTEXAI].includes(oai_settings.chat_completion_source)) { if (oai_settings.max_context_unlocked) { $('#openai_max_context').attr('max', max_2mil); @@ -4822,6 +4860,15 @@ async function onModelChange() { $('#freq_pen_openai').attr('max', 2).attr('min', -2).val(oai_settings.freq_pen_openai).trigger('input'); } + if (oai_settings.chat_completion_source === chat_completion_sources.MOONSHOT) { + const maxContext = getMoonshotMaxContext(oai_settings.moonshot_model, oai_settings.max_context_unlocked); + $('#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'); + } + $('#openai_max_context_counter').attr('max', Number($('#openai_max_context').attr('max'))); saveSettingsDebounced(); @@ -5053,6 +5100,19 @@ async function onConnectButtonClick(e) { } } + if (oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT) { + const api_key_moonshot = String($('#api_key_moonshot').val()).trim(); + + if (api_key_moonshot.length) { + await writeSecret(SECRET_KEYS.MOONSHOT, api_key_moonshot); + } + + if (!secret_state[SECRET_KEYS.MOONSHOT]) { + console.log('No secret key saved for Moonshot'); + return; + } + } + startStatusLoading(); saveSettingsDebounced(); await getStatusOpen(); @@ -5114,6 +5174,9 @@ function toggleChatCompletionForms() { else if (oai_settings.chat_completion_source == chat_completion_sources.POLLINATIONS) { $('#model_pollinations_select').trigger('change'); } + else if (oai_settings.chat_completion_source == chat_completion_sources.MOONSHOT) { + $('#model_moonshot_select').trigger('change'); + } $('[data-source]').each(function () { const validSources = $(this).data('source').split(','); $(this).toggle(validSources.includes(oai_settings.chat_completion_source)); @@ -5221,6 +5284,10 @@ export function isImageInliningSupported() { 'grok-4', 'grok-2-vision', 'grok-vision', + // Moonshot + 'moonshot-v1-8k-vision-preview', + 'moonshot-v1-32k-vision-preview', + 'moonshot-v1-128k-vision-preview', ]; switch (oai_settings.chat_completion_source) { @@ -5249,6 +5316,8 @@ export function isImageInliningSupported() { 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); + case chat_completion_sources.MOONSHOT: + return visionSupportedModels.some(model => oai_settings.moonshot_model.includes(model)); default: return false; } @@ -6037,6 +6106,7 @@ export function initOpenAI() { $('#model_custom_select').on('change', onModelChange); $('#model_xai_select').on('change', onModelChange); $('#model_pollinations_select').on('change', onModelChange); + $('#model_moonshot_select').on('change', onModelChange); $('#settings_preset_openai').on('change', onSettingsPresetChange); $('#new_oai_preset').on('click', onNewPresetClick); $('#delete_oai_preset').on('click', onDeletePresetClick); diff --git a/public/scripts/reasoning.js b/public/scripts/reasoning.js index 4ad35a559..8b2cf6926 100644 --- a/public/scripts/reasoning.js +++ b/public/scripts/reasoning.js @@ -120,6 +120,7 @@ export function extractReasoningFromData(data, { return data?.content?.find(part => part.type === 'thinking')?.thinking ?? ''; case chat_completion_sources.AIMLAPI: case chat_completion_sources.POLLINATIONS: + case chat_completion_sources.MOONSHOT: case chat_completion_sources.CUSTOM: { return data?.choices?.[0]?.message?.reasoning_content ?? data?.choices?.[0]?.message?.reasoning diff --git a/public/scripts/secrets.js b/public/scripts/secrets.js index 46db54c29..469c30792 100644 --- a/public/scripts/secrets.js +++ b/public/scripts/secrets.js @@ -63,6 +63,7 @@ export const SECRET_KEYS = { VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json', MINIMAX: 'api_key_minimax', MINIMAX_GROUP_ID: 'minimax_group_id', + MOONSHOT: 'api_key_moonshot', }; const FRIENDLY_NAMES = { @@ -114,6 +115,7 @@ const FRIENDLY_NAMES = { [SECRET_KEYS.DEEPLX_URL]: 'DeepLX Endpoint (e.g. http://127.0.0.1:1188/translate)', [SECRET_KEYS.MINIMAX]: 'MiniMax TTS', [SECRET_KEYS.MINIMAX_GROUP_ID]: 'MiniMax Group ID', + [SECRET_KEYS.MOONSHOT]: 'Moonshot AI', }; const INPUT_MAP = { @@ -148,6 +150,7 @@ const INPUT_MAP = { [SECRET_KEYS.AIMLAPI]: '#api_key_aimlapi', [SECRET_KEYS.XAI]: '#api_key_xai', [SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]: '#vertexai_service_account_json', + [SECRET_KEYS.MOONSHOT]: '#api_key_moonshot', }; const getLabel = () => moment().format('L LT'); diff --git a/public/scripts/slash-commands.js b/public/scripts/slash-commands.js index 662573f9d..5e533ed25 100644 --- a/public/scripts/slash-commands.js +++ b/public/scripts/slash-commands.js @@ -4805,6 +4805,7 @@ function getModelOptions(quiet) { { 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_moonshot_select', api: 'openai', type: chat_completion_sources.MOONSHOT }, { id: 'model_novel_select', api: 'novel', type: null }, { id: 'horde_model', api: 'koboldhorde', type: null }, ]; diff --git a/public/scripts/tool-calling.js b/public/scripts/tool-calling.js index 0fda8fe5d..e131767f3 100644 --- a/public/scripts/tool-calling.js +++ b/public/scripts/tool-calling.js @@ -620,6 +620,7 @@ export class ToolManager { chat_completion_sources.AI21, chat_completion_sources.XAI, chat_completion_sources.POLLINATIONS, + chat_completion_sources.MOONSHOT, ]; return supportedSources.includes(oai_settings.chat_completion_source); } diff --git a/src/constants.js b/src/constants.js index 008efcafd..aab29c581 100644 --- a/src/constants.js +++ b/src/constants.js @@ -178,6 +178,7 @@ export const CHAT_COMPLETION_SOURCES = { AIMLAPI: 'aimlapi', XAI: 'xai', POLLINATIONS: 'pollinations', + MOONSHOT: 'moonshot', }; /** diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js index 348bc374c..006d4d26c 100644 --- a/src/endpoints/backends/chat-completions.js +++ b/src/endpoints/backends/chat-completions.js @@ -65,6 +65,7 @@ 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'; +const API_MOONSHOT = 'https://api.moonshot.ai/v1'; /** * Gets OpenRouter transforms based on the request. @@ -97,6 +98,23 @@ function getOpenRouterPlugins(request) { return plugins; } +/** + * Hacky way to use JSON schema only if json_object format is supported. + * @param {object} bodyParams Additional body parameters + * @param {object[]} messages Array of messages + * @param {object} jsonSchema JSON schema object + */ +function setJsonObjectFormat(bodyParams, messages, jsonSchema) { + bodyParams['response_format'] = { + type: 'json_object', + }; + const message = { + role: 'user', + content: `JSON schema for the response:\n${JSON.stringify(jsonSchema.value, null, 4)}`, + }; + messages.push(message); +} + /** * Sends a request to Claude API. * @param {express.Request} request Express request @@ -890,7 +908,7 @@ async function sendDeepSeekRequest(request, response) { const postProcessType = String(request.body.model).endsWith('-reasoner') ? PROMPT_PROCESSING_TYPE.STRICT_TOOLS : PROMPT_PROCESSING_TYPE.SEMI_TOOLS; - const processedMessages = addAssistantPrefix(postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request)), bodyParams.tools); + const processedMessages = addAssistantPrefix(postProcessPrompt(request.body.messages, postProcessType, getPromptNames(request)), bodyParams.tools, 'prefix'); const requestBody = { 'messages': processedMessages, @@ -1220,6 +1238,10 @@ router.post('/status', async function (request, statusResponse) { apiUrl = API_GROQ; apiKey = readSecret(request.user.directories, SECRET_KEYS.GROQ); headers = {}; + } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MOONSHOT) { + apiUrl = API_MOONSHOT; + apiKey = readSecret(request.user.directories, SECRET_KEYS.MOONSHOT); + headers = {}; } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MAKERSUITE) { apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MAKERSUITE); apiUrl = trimTrailingSlash(request.body.reverse_proxy || API_MAKERSUITE); @@ -1598,17 +1620,17 @@ router.post('/generate', function (request, response) { referrer: 'sillytavern', seed: request.body.seed ?? Math.floor(Math.random() * 99999999), }; - // Hack to support JSON schema if (request.body.json_schema) { - bodyParams['response_format'] = { - type: 'json_object', - }; - const message = { - role: 'user', - content: `JSON schema for the response:\n${JSON.stringify(request.body.json_schema.value, null, 4)}`, - }; - request.body.messages.push(message); + setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema); } + } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MOONSHOT) { + apiUrl = API_MOONSHOT; + apiKey = readSecret(request.user.directories, SECRET_KEYS.MOONSHOT); + headers = {}; + bodyParams = {}; + request.body.json_schema + ? setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema) + : addAssistantPrefix(request.body.messages, [], 'partial'); } else { console.warn('This chat completion source is not supported yet.'); return response.status(400).send({ error: true }); diff --git a/src/endpoints/openai.js b/src/endpoints/openai.js index 1407767ef..c438cfbd4 100644 --- a/src/endpoints/openai.js +++ b/src/endpoints/openai.js @@ -73,6 +73,10 @@ router.post('/caption-image', async (request, response) => { key = readSecret(request.user.directories, SECRET_KEYS.COHERE); } + if (request.body.api === 'moonshot') { + key = readSecret(request.user.directories, SECRET_KEYS.MOONSHOT); + } + const noKeyTypes = ['custom', 'ooba', 'koboldcpp', 'vllm', 'llamacpp', 'pollinations']; if (!key && !request.body.reverse_proxy && !noKeyTypes.includes(request.body.api)) { console.warn('No key found for API', request.body.api); @@ -153,6 +157,10 @@ router.post('/caption-image', async (request, response) => { apiUrl = 'https://text.pollinations.ai/openai/chat/completions'; } + if (request.body.api === 'moonshot') { + apiUrl = 'https://api.moonshot.ai/v1/chat/completions'; + } + if (['koboldcpp', 'vllm', 'llamacpp', 'ooba'].includes(request.body.api)) { apiUrl = `${trimV1(request.body.server_url)}/v1/chat/completions`; } diff --git a/src/endpoints/secrets.js b/src/endpoints/secrets.js index e423fce16..7b3f75043 100644 --- a/src/endpoints/secrets.js +++ b/src/endpoints/secrets.js @@ -56,6 +56,7 @@ export const SECRET_KEYS = { VERTEXAI_SERVICE_ACCOUNT: 'vertexai_service_account_json', MINIMAX: 'api_key_minimax', MINIMAX_GROUP_ID: 'minimax_group_id', + MOONSHOT: 'api_key_moonshot', }; /** diff --git a/src/prompt-converters.js b/src/prompt-converters.js index 74748e220..14d921953 100644 --- a/src/prompt-converters.js +++ b/src/prompt-converters.js @@ -53,15 +53,16 @@ export function getPromptNames(request) { * Adds an assistant prefix to the last message. * @param {any[]} prompt Prompt messages array * @param {any[]} tools Array of tool definitions + * @param {string} property The property to set the prefix on * @returns {any[]} Transformed messages array */ -export function addAssistantPrefix(prompt, tools) { +export function addAssistantPrefix(prompt, tools, property) { if (!prompt.length) { return prompt; } const hasAnyTools = (Array.isArray(tools) && tools.length > 0) || prompt.some(x => x.role === 'tool'); if (!hasAnyTools && prompt[prompt.length - 1].role === 'assistant') { - prompt[prompt.length - 1].prefix = true; + prompt[prompt.length - 1][property] = true; } return prompt; }