also see https://rentry.org/kimi-k3-jb for a prefill patch (thanks, very cool)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 | diff --git a/public/index.html b/public/index.html
index 383404f7f..554437002 100644
--- a/public/index.html
+++ b/public/index.html
@@ -2029,7 +2029,7 @@
</div>
</div>
</div>
- <div class="range-block" data-source="openrouter,custom">
+ <div class="range-block" data-source="openrouter,custom,moonshot">
<div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10">
<div class="flex-container oneline-dropdown">
<label for="tool_reasoning_mode" data-i18n="Interleaved Thinking">
@@ -2039,6 +2039,7 @@
<option data-i18n="Disabled" value="disabled">Disabled</option>
<option data-i18n="Since Last User Message" value="since_last_user">Since Last User Message</option>
<option data-i18n="Active Tool Chain" value="active_chain">Active Tool Chain</option>
+ <option data-i18n="Entire Conversation" value="full_conversation">Entire Conversation</option>
</select>
</div>
</div>
@@ -2047,6 +2048,11 @@
Sends reasoning from preceding assistant turns with tool-call requests to maintain interleaved thinking context.
</span>
</div>
+ <div class="toggle-description justifyLeft" data-source="moonshot">
+ <span data-i18n="moonshot_interleaved_thinking_hint">
+ Kimi thinking models always send at least the active tool chain.
+ </span>
+ </div>
<div id="openrouter_interleaved_thinking_disabled_hint" class="toggle-description justifyLeft" style="display:none;">
<em data-i18n="Enable "Request model reasoning" to use Interleaved Thinking.">
Enable "Request model reasoning" to use Interleaved Thinking.
@@ -2155,7 +2161,7 @@
</strong>
</div>
</div>
- <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub,azure_openai,chutes,nanogpt,deepseek">
+ <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,vertexai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub,azure_openai,chutes,nanogpt,deepseek,moonshot">
<div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models. 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>
@@ -2172,6 +2178,9 @@
<div class="toggle-description justifyLeft marginBot5" data-source="openai,custom,xai,aimlapi,openrouter,pollinations,perplexity,cometapi,electronhub,azure_openai,chutes" data-i18n="OpenAI-compatible effort levels vary by model. Minimum and maximum may use model-specific aliases. Auto does not send an effort level.">
OpenAI-compatible effort levels vary by model. Minimum and maximum may use model-specific aliases. Auto does not send an effort level.
</div>
+ <div class="toggle-description justifyLeft marginBot5" data-source="moonshot">
+ Kimi K3 currently only supports Maximum. K2 models ignore this setting.
+ </div>
<strong class="toggle-description justifyLeft marginBot5" data-source="openrouter">
Request model reasoning = Off with Reasoning Effort = Minimum disables reasoning entirely on models that support that, but can cause errors with some models.
</strong>
diff --git a/public/script.js b/public/script.js
index 12bfe8a44..3489ba947 100644
--- a/public/script.js
+++ b/public/script.js
@@ -107,6 +107,7 @@ import {
openai_messages_count,
chat_completion_sources,
getChatCompletionModel,
+ isMoonshotKimiAlwaysOnThinkingModel,
proxies,
loadProxyPresets,
selected_proxy,
@@ -3557,6 +3558,8 @@ class StreamingProcessor {
this.images = [];
/** @type {string?} */
this.reasoningSignature = null;
+ /** @type {string} Reasoning text to preserve in tool-call chains. */
+ this.toolReasoning = '';
}
/**
@@ -3864,6 +3867,7 @@ class StreamingProcessor {
}
// Get the updated reasoning string into the handler
this.reasoningHandler.updateReasoning(this.messageId, state?.reasoning);
+ this.toolReasoning = state?.toolReasoning || this.reasoningHandler.reasoning;
this.images = state?.images ?? [];
this.reasoningSignature = state?.signature ?? null;
await eventSource.emit(event_types.STREAM_TOKEN_RECEIVED, text);
@@ -5390,7 +5394,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
await streamingProcessor.finalizeIntermediaryMessage(streamingProcessor.messageId, getMessage, { unlockUI: false });
}
const invocationResult = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls, {
- reasoningText: streamingProcessor.reasoningHandler.reasoning,
+ reasoningText: streamingProcessor.toolReasoning || streamingProcessor.reasoningHandler.reasoning,
});
const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length;
if (hasToolCalls) {
@@ -5460,6 +5464,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
let getMessage = extractMessageFromData(data);
let title = extractTitleFromData(data);
let reasoning = extractReasoningFromData(data);
+ let toolReasoning = reasoning;
let imageUrls = extractImagesFromData(data);
const reasoningSignature = extractReasoningSignatureFromData(data);
kobold_horde_model = title;
@@ -5480,6 +5485,15 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
reasoning = reasoning.trim();
}
+ if (main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.MOONSHOT && isMoonshotKimiAlwaysOnThinkingModel(getChatCompletionModel())) {
+ toolReasoning = getRegexedString(extractReasoningFromData(data, { ignoreShowThoughts: true }), regex_placement.REASONING);
+ if (power_user.trim_spaces) {
+ toolReasoning = toolReasoning.trim();
+ }
+ } else {
+ toolReasoning = reasoning;
+ }
+
if (isContinue) {
continue_mag = promptReasoning.removePrefix(continue_mag);
getMessage = continue_mag + getMessage;
@@ -5516,7 +5530,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
const hasToolCalls = ToolManager.hasToolCalls(data);
const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(getMessage) && !reasoning;
hasToolCalls && shouldDeleteMessage && await deleteLastMessage();
- const invocationResult = await ToolManager.invokeFunctionTools(data, { reasoningText: reasoning });
+ const invocationResult = await ToolManager.invokeFunctionTools(data, { reasoningText: toolReasoning });
const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length;
if (hasToolCalls) {
if (shouldStopGeneration) {
diff --git a/public/scripts/openai.js b/public/scripts/openai.js
index ad92a37d9..26c7cfeaf 100644
--- a/public/scripts/openai.js
+++ b/public/scripts/openai.js
@@ -255,8 +255,49 @@ export const tool_reasoning_modes = {
DISABLED: 'disabled',
SINCE_LAST_USER: 'since_last_user',
ACTIVE_CHAIN: 'active_chain',
+ FULL_CONVERSATION: 'full_conversation',
};
+const moonshotKimiFixedParameterModelRegex = /^kimi-k2(?:\.5|\.6|\.7-code|-0905-preview|-turbo-preview|-thinking|-thinking-turbo)$/;
+const moonshotKimiK3ModelRegex = /^kimi-k3(?:$|[-.])/;
+
+/**
+ * Checks if a Moonshot Kimi model only accepts fixed sampler values.
+ * @param {string} model Model identifier
+ * @returns {boolean} True if the model rejects modified sampler values
+ */
+export function isMoonshotKimiFixedParameterModel(model) {
+ return moonshotKimiFixedParameterModelRegex.test(String(model || ''));
+}
+
+/**
+ * Checks if a Moonshot model belongs to the Kimi K3 family.
+ * @param {string} model Model identifier
+ * @returns {boolean} True if the model uses K3 reasoning effort controls
+ */
+function isMoonshotKimiK3Model(model) {
+ return moonshotKimiK3ModelRegex.test(String(model || ''));
+}
+
+/**
+ * Checks if a Moonshot Kimi model always has thinking enabled.
+ * @param {string} model Model identifier
+ * @returns {boolean} True if thinking cannot be disabled
+ */
+export function isMoonshotKimiAlwaysOnThinkingModel(model) {
+ return /^kimi-k2\.7-code$/.test(String(model || ''));
+}
+
+/**
+ * Checks if a Moonshot Kimi request will produce thinking output.
+ * @param {string} model Model identifier
+ * @param {boolean} includeReasoning Whether reasoning was requested
+ * @returns {boolean} True if thinking output should be preserved
+ */
+export function isMoonshotKimiThinkingEnabledModel(model, includeReasoning) {
+ return isMoonshotKimiAlwaysOnThinkingModel(model) || ((isMoonshotKimiFixedParameterModel(model) || isMoonshotKimiK3Model(model)) && includeReasoning);
+}
+
// Providers that support interleaved reasoning forwarding in tool-call chains.
const interleaved_reasoning_providers = [
chat_completion_sources.OPENROUTER,
@@ -936,11 +977,20 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
const audioInlining = isAudioInliningSupported();
const canUseTools = ToolManager.isToolCallingSupported();
const includeSignature = isReasoningSignatureSupported();
- const isToolReasoningProvider = interleaved_reasoning_providers.includes(oai_settings.chat_completion_source);
- const toolReasoningMode = isToolReasoningProvider
- ? getEffectiveToolReasoningMode()
- : tool_reasoning_modes.DISABLED;
+ const isMoonshotKimiThinkingEnabled = oai_settings.chat_completion_source === chat_completion_sources.MOONSHOT
+ && isMoonshotKimiThinkingEnabledModel(oai_settings.moonshot_model, oai_settings.show_thoughts);
+ const isToolReasoningProvider = isMoonshotKimiThinkingEnabled || interleaved_reasoning_providers.includes(oai_settings.chat_completion_source);
+ let toolReasoningMode = tool_reasoning_modes.DISABLED;
+ if (isMoonshotKimiThinkingEnabled) {
+ // Kimi thinking models always need at least the active chain; users can widen it to the full conversation.
+ toolReasoningMode = getEffectiveToolReasoningMode() === tool_reasoning_modes.FULL_CONVERSATION
+ ? tool_reasoning_modes.FULL_CONVERSATION
+ : tool_reasoning_modes.ACTIVE_CHAIN;
+ } else if (isToolReasoningProvider) {
+ toolReasoningMode = getEffectiveToolReasoningMode();
+ }
const includeToolReasoning = toolReasoningMode !== tool_reasoning_modes.DISABLED;
+ const isFullConversationReasoning = toolReasoningMode === tool_reasoning_modes.FULL_CONVERSATION;
const lastUserIdx = messages.findLastIndex(x => x.role === 'user');
// Insert chat messages as long as there is budget available
@@ -994,13 +1044,15 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
if (canUseTools && Array.isArray(chatPrompt.invocations)) {
const promptIdx = messages.indexOf(chatPrompt);
- const reasoningIsEligible = toolReasoningMode !== tool_reasoning_modes.DISABLED
- && promptIdx > lastUserIdx;
+ const reasoningIsEligible = includeToolReasoning
+ && (isFullConversationReasoning || promptIdx > lastUserIdx);
let previousAssistantReasoning = '';
if (reasoningIsEligible) {
+ // Full conversation mode scans the entire history; other modes stop at the last user message.
+ const scanFloor = isFullConversationReasoning ? -1 : lastUserIdx;
if (toolReasoningMode === tool_reasoning_modes.ACTIVE_CHAIN) {
// Strict chain mode: skip tool/tool-call messages, then use only the first assistant text boundary.
- for (let idx = promptIdx - 1; idx > lastUserIdx; idx--) {
+ for (let idx = promptIdx - 1; idx > scanFloor; idx--) {
const candidate = messages[idx];
if (candidate?.role === 'tool') {
continue;
@@ -1017,9 +1069,9 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
}
break;
}
- } else if (toolReasoningMode === tool_reasoning_modes.SINCE_LAST_USER) {
- // Broad mode: use the latest assistant text reasoning anywhere since the last user.
- for (let idx = promptIdx - 1; idx > lastUserIdx; idx--) {
+ } else {
+ // Broad modes: use the latest assistant text reasoning anywhere in scope.
+ for (let idx = promptIdx - 1; idx > scanFloor; idx--) {
const candidate = messages[idx];
const hasAssistantText = candidate?.role === 'assistant'
&& !Array.isArray(candidate.invocations)
@@ -1066,6 +1118,13 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
chatMessage.signature = chatPrompt.signature;
}
+ // Full conversation mode: send stored reasoning with every assistant turn,
+ // as recommended for models like Kimi K2 Thinking that expect reasoning
+ // passed back across the whole multi-turn conversation.
+ if (isFullConversationReasoning && chatMessage.role === 'assistant' && chatPrompt.reasoning) {
+ await chatMessage.setReasoning(chatPrompt.reasoning);
+ }
+
if (chatCompletion.canAfford(chatMessage)) {
chatCompletion.insertAtStart(chatMessage, 'chatHistory');
} else {
@@ -2546,6 +2605,7 @@ function getReasoningEffort(settings = null, model = null) {
chat_completion_sources.ELECTRONHUB,
chat_completion_sources.CHUTES,
chat_completion_sources.DEEPSEEK,
+ chat_completion_sources.MOONSHOT,
];
if (!reasoningEffortSources.includes(settings.chat_completion_source)) {
@@ -2553,6 +2613,15 @@ function getReasoningEffort(settings = null, model = null) {
}
function resolveReasoningEffort() {
+ if (settings.chat_completion_source === chat_completion_sources.MOONSHOT) {
+ if (!settings.show_thoughts || !isMoonshotKimiK3Model(model) || settings.reasoning_effort === reasoning_effort_types.auto) {
+ return undefined;
+ }
+
+ // K3 currently accepts max; pass other known/future values through as Moonshot enables them.
+ return settings.reasoning_effort;
+ }
+
if (settings.chat_completion_source === chat_completion_sources.DEEPSEEK) {
switch (settings.reasoning_effort) {
case reasoning_effort_types.auto:
@@ -2983,12 +3052,13 @@ export async function createGenerationParameters(settings, model, type, messages
// https://platform.moonshot.ai/docs/api/chat#public-service-address
if (settings.chat_completion_source === chat_completion_sources.MOONSHOT) {
- // >Kimi API is fully compatible with OpenAI's API format
- if (/kimi-k2.5/.test(model)) {
+ // Kimi K2.5+ models use fixed sampler values; sending modified values is rejected.
+ if (isMoonshotKimiFixedParameterModel(model)) {
delete generate_data.temperature;
delete generate_data.top_p;
delete generate_data.frequency_penalty;
delete generate_data.presence_penalty;
+ delete generate_data.n;
}
}
@@ -3105,7 +3175,7 @@ async function sendOpenAIRequest(type, messages, signal, { jsonSchema = null } =
let text = '';
const swipes = [];
const toolCalls = [];
- const state = { reasoning: '', images: [], signature: '', toolSignatures: {} };
+ const state = { reasoning: '', toolReasoning: '', images: [], signature: '', toolSignatures: {} };
while (true) {
const { done, value } = await reader.read();
if (done) return;
@@ -3227,11 +3297,14 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov
});
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, chat_completion_sources.MOONSHOT, chat_completion_sources.COMETAPI, chat_completion_sources.ELECTRONHUB, chat_completion_sources.NANOGPT, chat_completion_sources.ZAI, chat_completion_sources.SILICONFLOW, chat_completion_sources.CHUTES, chat_completion_sources.WORKERS_AI].includes(chat_completion_source)) {
+ const reasoningDelta = data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content
+ ?? data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning
+ ?? '';
if (show_thoughts) {
- state.reasoning +=
- data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??
- data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning ??
- '';
+ state.reasoning += reasoningDelta;
+ }
+ if (chat_completion_source === chat_completion_sources.MOONSHOT && isMoonshotKimiAlwaysOnThinkingModel(getChatCompletionModel())) {
+ state.toolReasoning += reasoningDelta;
}
return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
} else if (chat_completion_source === chat_completion_sources.MISTRALAI) {
@@ -3531,6 +3604,20 @@ class Message {
});
}
+ /**
+ * Attaches reasoning text to the message and recounts tokens.
+ * @param {string} reasoning Reasoning text to send with the message.
+ * @returns {Promise<void>}
+ */
+ async setReasoning(reasoning) {
+ this.reasoning = reasoning;
+ this.tokens = await tokenHandler.countAsync({
+ role: this.role,
+ content: this.content,
+ ...(this.reasoning ? { reasoning: this.reasoning } : {}),
+ });
+ }
+
/**
* Add a name to the message.
* @param {string} name Name to set for the message.
@@ -5295,6 +5382,8 @@ function getMoonshotMaxContext(model, isUnlocked) {
'kimi-latest': max_256k,
'kimi-thinking-preview': max_32k,
'kimi-k2.5': max_256k,
+ 'kimi-k2.6': max_256k,
+ 'kimi-k2.7-code': max_256k,
'kimi-k2-0905-preview': max_256k,
'kimi-k2-turbo-preview': max_256k,
'kimi-k2-thinking': max_256k,
@@ -6205,6 +6294,8 @@ export function isImageInliningSupported() {
'moonshot-v1-32k-vision-preview',
'moonshot-v1-128k-vision-preview',
'kimi-k2.5',
+ 'kimi-k2.6',
+ 'kimi-k2.7-code',
'kimi-latest',
// Z.AI (GLM)
'glm-4.5v',
@@ -6258,7 +6349,8 @@ export function isImageInliningSupported() {
case chat_completion_sources.COMETAPI:
return true;
case chat_completion_sources.MOONSHOT:
- return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.moonshot_model)?.supports_image_in);
+ return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.moonshot_model)?.supports_image_in)
+ || visionSupportedModels.some(model => String(oai_settings.moonshot_model || '').includes(model));
case chat_completion_sources.NANOGPT:
return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.nanogpt_model)?.capabilities?.vision);
case chat_completion_sources.ZAI:
@@ -6294,6 +6386,11 @@ export function isVideoInliningSupported() {
'gemini-exp-1206',
'gemini-3',
'gemma-4',
+ // Moonshot
+ 'kimi-k2.5',
+ 'kimi-k2.6',
+ 'kimi-k2.7-code',
+ 'kimi-latest',
// Z.AI (GLM)
'glm-4.5v',
'glm-4.6v',
@@ -6307,6 +6404,8 @@ export function isVideoInliningSupported() {
return videoSupportedModels.some(model => oai_settings.vertexai_model.includes(model));
case chat_completion_sources.OPENROUTER:
return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.openrouter_model)?.architecture?.input_modalities?.includes('video'));
+ case chat_completion_sources.MOONSHOT:
+ return videoSupportedModels.some(model => String(oai_settings.moonshot_model || '').includes(model));
case chat_completion_sources.ZAI:
return videoSupportedModels.some(model => oai_settings.zai_model.includes(model));
default:
diff --git a/src/endpoints/backends/chat-completions.js b/src/endpoints/backends/chat-completions.js
index 666c6c858..e63251f48 100644
--- a/src/endpoints/backends/chat-completions.js
+++ b/src/endpoints/backends/chat-completions.js
@@ -97,6 +97,69 @@ const API_MINIMAX_CN = 'https://api.minimaxi.com/v1';
const API_OPENROUTER = 'https://openrouter.ai/api/v1';
const API_WORKERS_AI = 'https://api.cloudflare.com/client/v4/accounts';
+const MOONSHOT_KIMI_FIXED_PARAMETER_MODEL_REGEX = /^kimi-k2(?:\.5|\.6|\.7-code|-0905-preview|-turbo-preview|-thinking|-thinking-turbo)$/;
+const MOONSHOT_KIMI_K3_MODEL_REGEX = /^kimi-k3(?:$|[-.])/;
+
+/**
+ * Checks if a Moonshot Kimi model only accepts fixed sampler values.
+ * @param {string} model Model identifier
+ * @returns {boolean} True if the model rejects modified sampler values
+ */
+function isMoonshotKimiFixedParameterModel(model) {
+ return MOONSHOT_KIMI_FIXED_PARAMETER_MODEL_REGEX.test(String(model || ''));
+}
+
+/**
+ * Checks if a Moonshot model belongs to the Kimi K3 family.
+ * @param {string} model Model identifier
+ * @returns {boolean} True if the model uses K3 reasoning effort controls
+ */
+function isMoonshotKimiK3Model(model) {
+ return MOONSHOT_KIMI_K3_MODEL_REGEX.test(String(model || ''));
+}
+
+/**
+ * Checks if a Moonshot Kimi model always has thinking enabled.
+ * @param {string} model Model identifier
+ * @returns {boolean} True if thinking cannot be disabled
+ */
+function isMoonshotKimiAlwaysOnThinkingModel(model) {
+ return /^kimi-k2\.7-code$/.test(String(model || ''));
+}
+
+/**
+ * Moves SillyTavern's internal reasoning field to Kimi's reasoning_content field.
+ * @param {object[]} messages Prompt messages
+ * @param {boolean} thinkingEnabled Whether the request uses Kimi thinking mode
+ * @returns {void}
+ */
+function normalizeMoonshotReasoningContent(messages, thinkingEnabled) {
+ if (!Array.isArray(messages)) {
+ return;
+ }
+
+ for (const message of messages) {
+ const hasToolCalls = Array.isArray(message.tool_calls);
+ const isAssistant = message.role === 'assistant';
+
+ // Kimi recommends passing thinking content back on assistant turns across
+ // the whole multi-turn conversation via the reasoning_content field.
+ if (thinkingEnabled && (hasToolCalls || isAssistant) && typeof message.reasoning === 'string' && message.reasoning.length > 0) {
+ message.reasoning_content = message.reasoning;
+ }
+ delete message.reasoning;
+
+ if (!thinkingEnabled) {
+ delete message.reasoning_content;
+ continue;
+ }
+
+ if (hasToolCalls && !('reasoning_content' in message)) {
+ message.reasoning_content = '';
+ }
+ }
+}
+
/**
* Module-scoped Claude caching configuration values.
*/
@@ -2450,11 +2513,24 @@ router.post('/generate', async function (request, response) {
apiUrl = new URL(request.body.reverse_proxy || API_MOONSHOT).toString();
apiKey = request.body.reverse_proxy ? request.body.proxy_password : readSecret(request.user.directories, SECRET_KEYS.MOONSHOT, request.body.secret_id);
headers = {};
- bodyParams = {
- thinking: {
- type: request.body.include_reasoning ? 'enabled' : 'disabled',
- },
- };
+ const isKimiK3 = isMoonshotKimiK3Model(request.body.model);
+ const thinkingEnabled = isMoonshotKimiAlwaysOnThinkingModel(request.body.model) || Boolean(request.body.include_reasoning);
+ bodyParams = isKimiK3 && thinkingEnabled
+ ? {}
+ : {
+ thinking: {
+ type: thinkingEnabled ? 'enabled' : 'disabled',
+ },
+ };
+ // kimi-k2.6 ignores historical reasoning_content unless Preserved Thinking
+ // is requested. K3/k2.7-code have it always on; k2.5 rejects the parameter.
+ if (thinkingEnabled && /^kimi-k2\.6/.test(request.body.model)) {
+ bodyParams.thinking.keep = 'all';
+ }
+ if (isKimiK3 && thinkingEnabled && request.body.reasoning_effort && request.body.reasoning_effort !== 'auto') {
+ bodyParams.reasoning_effort = request.body.reasoning_effort;
+ }
+ normalizeMoonshotReasoningContent(request.body.messages, thinkingEnabled);
request.body.json_schema
? setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema)
: addAssistantPrefix(request.body.messages, [], 'partial');
@@ -2554,6 +2630,11 @@ router.post('/generate', async function (request, response) {
if (!isTextCompletion && Array.isArray(request.body.tools) && request.body.tools.length > 0) {
bodyParams['tools'] = request.body.tools;
bodyParams['tool_choice'] = request.body.tool_choice;
+ if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MOONSHOT
+ && bodyParams.thinking?.type === 'enabled'
+ && !['auto', 'none', undefined].includes(bodyParams['tool_choice'])) {
+ bodyParams['tool_choice'] = 'auto';
+ }
}
if (request.body.json_schema && !bodyParams['response_format']) {
@@ -2586,6 +2667,14 @@ router.post('/generate', async function (request, response) {
...bodyParams,
};
+ if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MOONSHOT && isMoonshotKimiFixedParameterModel(request.body.model)) {
+ delete requestBody.temperature;
+ delete requestBody.presence_penalty;
+ delete requestBody.frequency_penalty;
+ delete requestBody.top_p;
+ delete requestBody.n;
+ }
+
if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.CUSTOM) {
excludeKeysByYaml(requestBody, request.body.custom_exclude_body);
}
|