OpenRouter interleaved reasoning forwarding for tool-call continuations (#5160) * fix(openrouter): forward reasoning across active tool-call chains * feat(reasoning): add tool-chain forwarding toggle and honor edited reasoning * feat(reasoning): add OpenRouter interleaved forwarding modes * moved the reasoning forwarding dropdown into a separate line * feat(reasoning): default tool reasoning forwarding to disabled * refactor(openrouter): move tool reasoning mode to CC settings Move OpenRouter tool reasoning forwarding control to response configuration and scope it to OpenRouter. Store mode in chat completion settings (presettable), remove legacy power_user boolean/fallback, and use constants for mode values. Preserve OpenRouter Gemini signature forwarding independently from plaintext tool reasoning mode. * fix(openrouter): tighten active-chain reasoning forwarding Use trailing contiguous tool-chain boundary for active-chain eligibility. Also rename the UI control to Interleaved Thinking Forwarding and place selector on its own line. * fix(openrouter): use adjacent assistant reasoning for tool calls For interleaved thinking forwarding, source reasoning only from the immediately preceding assistant non-tool message. Keep mode gating behavior unchanged and avoid history-window reasoning carryover. * fix(openrouter): skip tool messages for reasoning source When forwarding interleaved reasoning, ignore intervening tool result messages when resolving the preceding assistant reasoning source. This keeps only the first tool call in a chain tied to a prior assistant reasoning block unless a later invocation carries its own reasoning. * fix(openrouter): keep plaintext reasoning with signatures Do not suppress forwarded tool-call reasoning when thought signatures are present. * fix(openrouter): split interleaved thinking mode behavior Restore distinct mode semantics: active_chain uses nearest assistant-text boundary after skipping tool/tool-call messages, while since_last_user scans for latest assistant reasoning since user. Update UI label to Interleaved Thinking with right-aligned dropdown and explanatory tooltip. * style(openrouter): align interleaved thinking dropdown row Match OpenRouter interleaved thinking control layout with existing oneline-dropdown patterns. Also update reasoning-forwarding inline comment wording for current mode behavior. * docs(ui): clarify interleaved thinking tooltip Use explicit API-request wording for OpenRouter interleaved thinking tooltip text. * i18n(openrouter): localize interleaved thinking UI Add locale keys for OpenRouter interleaved thinking label, mode options, and inline helper description. Wire dropdown option text to data-i18n in index.html. * fixed helper text wrapping * fix(ui): make interleaved thinking helper text wrap * i18n(openrouter): translate interleaved thinking labels Replace placeholder English values for interleaved thinking keys in non-English locale files. * fix(ui): restore interleaved thinking dropdown alignment * Remove changes from en.json * Type fixes * Reworked the interleaved reasoning provider logic * Renamed the variables in preparation for potential implementation for other providers * Gate interleaved tool reasoning on reasoning request setting --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

0ba0418facda00b98ac0a124dfdb7eead917a9d5

ZhenyaPav <61346893+ZhenyaPav@users.noreply.github.com>

Signed
21 files changed, +264 -31Ignore whitespace
public/index.html+24 -0
@@ -2011,6 +2011,30 @@
20112011 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>
20122012 </div>
20132013 </div>
2014+ <div class="range-block" data-source="openrouter">
2015+ <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10">
2016+ <div class="flex-container oneline-dropdown">
2017+ <label for="tool_reasoning_mode" data-i18n="Interleaved Thinking">
2018+ Interleaved Thinking
2019+ </label>
2020+ <select id="tool_reasoning_mode">
2021+ <option data-i18n="Disabled" value="disabled">Disabled</option>
2022+ <option data-i18n="Since Last User Message" value="since_last_user">Since Last User Message</option>
2023+ <option data-i18n="Active Tool Chain" value="active_chain">Active Tool Chain</option>
2024+ </select>
2025+ </div>
2026+ </div>
2027+ <div class="toggle-description justifyLeft">
2028+ <span data-i18n="openrouter_interleaved_thinking_hint">
2029+ Sends reasoning from preceding assistant turns with tool-call requests to maintain interleaved thinking context.
2030+ </span>
2031+ </div>
2032+ <div id="openrouter_interleaved_thinking_disabled_hint" class="toggle-description justifyLeft" style="display:none;">
2033+ <em data-i18n="Enable &quot;Request model reasoning&quot; to use Interleaved Thinking.">
2034+ Enable "Request model reasoning" to use Interleaved Thinking.
2035+ </em>
2036+ </div>
2037+ </div>
20142038 <div class="range-block" data-source="openai,aimlapi,openrouter,mistralai,makersuite,vertexai,claude,custom,xai,pollinations,moonshot,cohere,cometapi,nanogpt,electronhub,azure_openai,zai,siliconflow,chutes">
20152039 <label for="openai_media_inlining" class="checkbox_label flexWrap widthFreeExpand">
20162040 <input id="openai_media_inlining" type="checkbox" />
public/locales/ar-sa.json+5 -1
@@ -1438,5 +1438,9 @@
14381438 "Still have questions?": "هل ما زالت لديك اسئلة؟",
14391439 "Join the SillyTavern Discord": "انضم إلى ديسكورد SillyTavern",
14401440 "Post a GitHub issue": "نشر مشكلة على GitHub",
14411441 "Contact the developers": "الاتصال بالمطورين",
1442+ "Interleaved Thinking": "التفكير المتداخل",
1443+ "Since Last User Message": "منذ آخر رسالة من المستخدم",
1444+ "Active Tool Chain": "سلسلة الأدوات النشطة",
1445+ "openrouter_interleaved_thinking_hint": "يرسل الاستدلال من رسائل المساعد السابقة مع طلبات استدعاء الأدوات للحفاظ على سياق التفكير المتداخل."
14421446}
public/locales/de-de.json+5 -1
@@ -1438,5 +1438,9 @@
14381438 "Still have questions?": "Hast du immer noch Fragen?",
14391439 "Join the SillyTavern Discord": "Trete dem SillyTavern Discord bei",
14401440 "Post a GitHub issue": "Veröffentliche ein GitHub-Problem",
14411441 "Contact the developers": "Kontaktiere die Entwickler",
1442+ "Interleaved Thinking": "Verschränktes Denken",
1443+ "Since Last User Message": "Seit letzter Nutzernachricht",
1444+ "Active Tool Chain": "Aktive Tool-Kette",
1445+ "openrouter_interleaved_thinking_hint": "Sendet Begründungen aus vorherigen Assistentennachrichten mit Tool-Aufrufen, um den Kontext für verschränktes Denken zu erhalten."
14421446}
public/locales/es-es.json+5 -1
@@ -1545,5 +1545,9 @@
15451545 "Toggles sharing world tint with character sprites (This will override Character Tint)": "Alterna el tinte mundial compartido con los sprites de personajes (esto anulará el tinte de personajes)",
15461546 "Sets the expression of the user sprite": "Establece la expresión del sprite de usuario",
15471547 "Sets the user sprite set to use for the user sprite": "Establece el conjunto de sprites de usuario para usar en el sprite de usuario",
15481548 "Toggles the user sprite on the VN UI": "Alterna el sprite de usuario en la interfaz VN",
1549+ "Interleaved Thinking": "Razonamiento intercalado",
1550+ "Since Last User Message": "Desde el último mensaje del usuario",
1551+ "Active Tool Chain": "Cadena de herramientas activa",
1552+ "openrouter_interleaved_thinking_hint": "Envía razonamiento de turnos anteriores del asistente junto con solicitudes de herramientas para mantener el contexto del razonamiento intercalado."
15491553}
public/locales/fr-fr.json+5 -1
@@ -2046,5 +2046,9 @@
20462046 "System Prompt Name": "Nom du prompt système:",
20472047 "Instruct Mode": "Mode Instruction:",
20482048 "Save and Update": "Sauvegarder et mettre à jour",
20492049 "Don't ask again for this URL": "Ne plus demander pour cette URL",
2050+ "Interleaved Thinking": "Raisonnement intercalé",
2051+ "Since Last User Message": "Depuis le dernier message utilisateur",
2052+ "Active Tool Chain": "Chaîne d'outils active",
2053+ "openrouter_interleaved_thinking_hint": "Envoie le raisonnement des tours précédents de l'assistant avec les requêtes d'outils pour conserver le contexte du raisonnement intercalé."
20502054}
public/locales/is-is.json+5 -1
@@ -1436,5 +1436,9 @@
14361436 "Still have questions?": "Ertu enn með spurningar?",
14371437 "Join the SillyTavern Discord": "Skráðu þig í SillyTavern Discord",
14381438 "Post a GitHub issue": "Senda inn GitHub málefni",
14391439 "Contact the developers": "Hafa samband við þróunaraðila",
1440+ "Interleaved Thinking": "Fléttuð hugsun",
1441+ "Since Last User Message": "Frá síðustu notandaskilaboðum",
1442+ "Active Tool Chain": "Virk verkfærakeðja",
1443+ "openrouter_interleaved_thinking_hint": "Sendir röksemdir úr fyrri aðstoðarskilaboðum með verkfæraköllum til að viðhalda samhengi fléttaðrar hugsunar."
14401444}
public/locales/it-it.json+5 -1
@@ -1438,5 +1438,9 @@
14381438 "Still have questions?": "Hai ancora domande?",
14391439 "Join the SillyTavern Discord": "Unisciti al SillyTavern Discord",
14401440 "Post a GitHub issue": "Pubblica un problema su GitHub",
14411441 "Contact the developers": "Contatta gli sviluppatori",
1442+ "Interleaved Thinking": "Ragionamento intercalato",
1443+ "Since Last User Message": "Dall'ultimo messaggio utente",
1444+ "Active Tool Chain": "Catena di strumenti attiva",
1445+ "openrouter_interleaved_thinking_hint": "Invia il ragionamento dai turni precedenti dell'assistente con le richieste di strumenti per mantenere il contesto del ragionamento intercalato."
14421446}
public/locales/ja-jp.json+5 -1
@@ -1443,5 +1443,9 @@
14431443 "Contact the developers": "開発者に連絡",
14441444 "Stop Inspecting": "検査を停止",
14451445 "Inspect Prompts": "プロンプトを検査",
14461446 "Toggle prompt inspection": "プロンプト検査の切り替え",
1447+ "Interleaved Thinking": "インターリーブ思考",
1448+ "Since Last User Message": "直前のユーザーメッセージ以降",
1449+ "Active Tool Chain": "アクティブなツールチェーン",
1450+ "openrouter_interleaved_thinking_hint": "インターリーブ思考の文脈を維持するため、直前のアシスタントターンの推論をツール呼び出しリクエストとともに送信します。"
14471451}
public/locales/ko-kr.json+5 -1
@@ -1617,5 +1617,9 @@
16171617 "to install additional features.": "으로 가세요.",
16181618 "Master Import": "마스터 불러오기",
16191619 "Master Export": "마스터 내보내기",
16201620 "Chat Quick Reply Sets": "채팅 빠른 답장 세트들",
1621+ "Interleaved Thinking": "인터리브드 사고",
1622+ "Since Last User Message": "마지막 사용자 메시지 이후",
1623+ "Active Tool Chain": "활성 도구 체인",
1624+ "openrouter_interleaved_thinking_hint": "인터리브드 사고 컨텍스트를 유지하기 위해 이전 어시스턴트 턴의 추론을 도구 호출 요청과 함께 전송합니다."
16211625}
public/locales/nl-nl.json+5 -1
@@ -1434,5 +1434,9 @@
14341434 "Still have questions?": "Heb je nog vragen?",
14351435 "Join the SillyTavern Discord": "Word lid van de SillyTavern Discord",
14361436 "Post a GitHub issue": "Plaats een GitHub-probleem",
14371437 "Contact the developers": "Neem contact op met de ontwikkelaars",
1438+ "Interleaved Thinking": "Verweven redenering",
1439+ "Since Last User Message": "Sinds laatste gebruikersbericht",
1440+ "Active Tool Chain": "Actieve toolketen",
1441+ "openrouter_interleaved_thinking_hint": "Verstuurt redenering uit eerdere assistentbeurten met tool-aanvragen om de context van verweven redenering te behouden."
14381442}
public/locales/pt-pt.json+5 -1
@@ -1436,5 +1436,9 @@
14361436 "Still have questions?": "Ainda tem perguntas?",
14371437 "Join the SillyTavern Discord": "Junte-se ao Discord do SillyTavern",
14381438 "Post a GitHub issue": "Publicar um problema no GitHub",
14391439 "Contact the developers": "Contatar os desenvolvedores",
1440+ "Interleaved Thinking": "Raciocínio intercalado",
1441+ "Since Last User Message": "Desde a última mensagem do utilizador",
1442+ "Active Tool Chain": "Cadeia de ferramentas ativa",
1443+ "openrouter_interleaved_thinking_hint": "Envia o raciocínio de turnos anteriores do assistente com pedidos de ferramentas para manter o contexto de raciocínio intercalado."
14401444}
public/locales/ru-ru.json+5 -1
@@ -2738,5 +2738,9 @@
27382738 "Uses the voices provided by your operating system": "Используем стандартные голоса вашей ОС",
27392739 "Your browser or operating system doesn't support speech synthesis": "Ваш браузер либо ОС не поддерживают синтез речи",
27402740 "Works best when: Pass Asterisks to TTS Engine is enabled, and both Only narrate quotes and Ignore *text, even 'quotes', inside asterisks* are disabled.": "Работает лучше всего, когда: одновременно включена опция \"Передавать звёздочки TTS-движку\", а также отключены обе опции: \"Не озвучивать *текст, даже \"в кавычках\", если он внутри звёздочек*\", и \"Озвучивать только текст \"в кавычках\"\"",
27412741 "Available voices": "Доступные голоса",
2742+ "Interleaved Thinking": "Чередующееся рассуждение",
2743+ "Since Last User Message": "С момента последнего сообщения пользователя",
2744+ "Active Tool Chain": "Активная цепочка инструментов",
2745+ "openrouter_interleaved_thinking_hint": "Отправляет рассуждение из предыдущих ходов ассистента вместе с запросами вызова инструментов, чтобы сохранять контекст чередующегося рассуждения."
27422746}
public/locales/th-th.json+5 -1
@@ -1455,5 +1455,9 @@
14551455 "Contact the developers": "ติดต่อผู้พัฒนา",
14561456 "Stop Inspecting": "Stop Inspecting",
14571457 "Inspect Prompts": "ตรวจสอบ Prompt",
14581458 "Toggle prompt inspection": "สลับการตรวจสอบ Prompt",
1459+ "Interleaved Thinking": "การคิดแบบสลับขั้น",
1460+ "Since Last User Message": "ตั้งแต่ข้อความล่าสุดของผู้ใช้",
1461+ "Active Tool Chain": "สายโซ่เครื่องมือที่ใช้งานอยู่",
1462+ "openrouter_interleaved_thinking_hint": "ส่งเหตุผลจากเทิร์นก่อนหน้าของผู้ช่วยไปพร้อมกับคำขอเรียกใช้เครื่องมือ เพื่อคงบริบทการคิดแบบสลับขั้นไว้"
14591463}
public/locales/uk-ua.json+5 -1
@@ -1436,5 +1436,9 @@
14361436 "Still have questions?": "Все ще є питання?",
14371437 "Join the SillyTavern Discord": "Приєднуйтесь до SillyTavern Discord",
14381438 "Post a GitHub issue": "Опублікуйте проблему на GitHub",
14391439 "Contact the developers": "Зв'яжіться з розробниками",
1440+ "Interleaved Thinking": "Черговане мислення",
1441+ "Since Last User Message": "Від останнього повідомлення користувача",
1442+ "Active Tool Chain": "Активний ланцюжок інструментів",
1443+ "openrouter_interleaved_thinking_hint": "Надсилає міркування з попередніх ходів асистента разом із запитами виклику інструментів, щоб зберегти контекст чергованого мислення."
14401444}
public/locales/vi-vn.json+5 -1
@@ -1436,5 +1436,9 @@
14361436 "Still have questions?": "Bạn còn thắc mắc?",
14371437 "Join the SillyTavern Discord": "Tham gia Kênh Discord của SillyTavern",
14381438 "Post a GitHub issue": "Đăng một vấn đề trên GitHub",
14391439 "Contact the developers": "Liên hệ với Devs",
1440+ "Interleaved Thinking": "Lập luận xen kẽ",
1441+ "Since Last User Message": "Kể từ tin nhắn người dùng gần nhất",
1442+ "Active Tool Chain": "Chuỗi công cụ đang hoạt động",
1443+ "openrouter_interleaved_thinking_hint": "Gửi lập luận từ các lượt trợ lý trước đó cùng với yêu cầu gọi công cụ để duy trì ngữ cảnh lập luận xen kẽ."
14401444}
public/locales/zh-cn.json+4 -0
@@ -3517,6 +3517,10 @@
35173517 "Title/Memo": "标题(备忘)",
35183518 "Strategy": "触发策略",
35193519 "Trigger %": "触发概率 %",
3520+ "Interleaved Thinking": "交错思维",
3521+ "Since Last User Message": "自上一条用户消息起",
3522+ "Active Tool Chain": "活动工具链",
3523+ "openrouter_interleaved_thinking_hint": "发送前面助手轮次的推理并附在工具调用请求中,以维持交错思维上下文。",
35203524 "Class": "类型",
35213525 "Context Length": "上下文长度",
35223526 "Added On": "添加于",
public/locales/zh-tw.json+5 -1
@@ -2758,5 +2758,9 @@
27582758 "[title]Create a new": "新增",
27592759 "[title]Delete a ": "刪除 ",
27602760 "[title]Rename a ": "重新命名 ",
27612761 "completions note prefix": "完成提示前綴",
2762+ "Interleaved Thinking": "交錯思維",
2763+ "Since Last User Message": "自上一則使用者訊息起",
2764+ "Active Tool Chain": "使用中的工具鏈",
2765+ "openrouter_interleaved_thinking_hint": "會在工具呼叫請求中附帶先前助理回合的推理,以維持交錯思維的上下文。"
27622766}
public/script.js+4 -2
@@ -5216,7 +5216,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
52165216 const hasToolCalls = ToolManager.hasToolCalls(streamingProcessor.toolCalls);
52175217 const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(lastMessage?.mes) && !lastMessage?.extra?.reasoning && ['', '...'].includes(streamingProcessor?.result);
52185218 hasToolCalls && shouldDeleteMessage && await deleteLastMessage();
52195219 const invocationResult = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls);, {
5220+ reasoningText: streamingProcessor.reasoningHandler.reasoning,
5221+ });
52205222 const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length;
52215223 if (hasToolCalls) {
52225224 if (shouldStopGeneration) {
@@ -5341,7 +5343,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
53415343 const hasToolCalls = ToolManager.hasToolCalls(data);
53425344 const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(getMessage) && !reasoning;
53435345 hasToolCalls && shouldDeleteMessage && await deleteLastMessage();
53445346 const invocationResult = await ToolManager.invokeFunctionTools(data, { reasoningText: reasoning });
53455347 const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length;
53465348 if (hasToolCalls) {
53475349 if (shouldStopGeneration) {
public/scripts/openai.js+151 -12
@@ -246,6 +246,17 @@ export const verbosity_levels = {
246246 high: 'high',
247247};
248248
249+export const tool_reasoning_modes = {
250+ DISABLED: 'disabled',
251+ SINCE_LAST_USER: 'since_last_user',
252+ ACTIVE_CHAIN: 'active_chain',
253+};
254+
255+// Providers that support interleaved reasoning forwarding in tool-call chains.
256+const interleaved_reasoning_providers = [
257+ chat_completion_sources.OPENROUTER,
258+];
259+
249260export const ZAI_ENDPOINT = {
250261 COMMON: 'common',
251262 CODING: 'coding',
@@ -289,6 +300,7 @@ export const settingsToUpdate = {
289300 openrouter_quantizations: ['#openrouter_quantizations_chat', 'openrouter_quantizations', false, true],
290301 openrouter_allow_fallbacks: ['#openrouter_allow_fallbacks', 'openrouter_allow_fallbacks', true, true],
291302 openrouter_middleout: ['#openrouter_middleout', 'openrouter_middleout', false, true],
303+ tool_reasoning_mode: ['#tool_reasoning_mode', 'tool_reasoning_mode', false, false],
292304 ai21_model: ['#model_ai21_select', 'ai21_model', false, true],
293305 mistralai_model: ['#model_mistralai_select', 'mistralai_model', false, true],
294306 cohere_model: ['#model_cohere_select', 'cohere_model', false, true],
@@ -436,6 +448,7 @@ const default_settings = {
436448 openrouter_quantizations: [],
437449 openrouter_allow_fallbacks: true,
438450 openrouter_middleout: openrouter_middleout_types.ON,
451+ tool_reasoning_mode: tool_reasoning_modes.DISABLED,
439452 reverse_proxy: '',
440453 chat_completion_source: chat_completion_sources.OPENAI,
441454 max_context_unlocked: false,
@@ -577,19 +590,21 @@ function setOpenAIMessages(chat) {
577590 const originModel = chat[j]?.extra?.model;
578591 const isSameModel = originApi === currentApi && originModel === currentModel;
579592 const signature = isSameModel ? chat[j]?.extra?.reasoning_signature : null;
593+ const reasoning = isSameModel ? String(chat[j]?.extra?.reasoning ?? '') : '';
580594
581595 // Remove signaturesreasoning metadata from invocations if the API/model don't match
582596 if (Array.isArray(invocations) && invocations.length > 0) {
583597 invocations.forEach((invocation, index) => {
584598 if (!isSameModel && (invocation.signature &&|| !isSameModelinvocation.reasoning)) {
585599 const cloneInvocation = structuredClone(invocation);
586600 delete cloneInvocation.signature;
601+ delete cloneInvocation.reasoning;
587602 invocations[index] = cloneInvocation;
588603 }
589604 });
590605 }
591606
592607 messages[i] = { 'role': role, 'content': content, name: name, 'media': media, 'mediaDisplay': mediaDisplay, 'mediaIndex': mediaIndex, 'invocations': invocations, 'signature': signature, 'reasoning': reasoning };
593608 j++;
594609 }
595610
@@ -885,6 +900,12 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
885900 const audioInlining = isAudioInliningSupported();
886901 const canUseTools = ToolManager.isToolCallingSupported();
887902 const includeSignature = isReasoningSignatureSupported();
903+ const isToolReasoningProvider = interleaved_reasoning_providers.includes(oai_settings.chat_completion_source);
904+ const toolReasoningMode = isToolReasoningProvider
905+ ? getEffectiveToolReasoningMode()
906+ : tool_reasoning_modes.DISABLED;
907+ const includeToolReasoning = toolReasoningMode !== tool_reasoning_modes.DISABLED;
908+ const lastUserIdx = messages.findLastIndex(x => x.role === 'user');
888909
889910 // Insert chat messages as long as there is budget available
890911 const chatPool = [...messages].reverse();
@@ -936,11 +957,63 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
936957 }
937958
938959 if (canUseTools && Array.isArray(chatPrompt.invocations)) {
960+ const promptIdx = messages.indexOf(chatPrompt);
961+ const reasoningIsEligible = toolReasoningMode !== tool_reasoning_modes.DISABLED
962+ && promptIdx > lastUserIdx;
963+ let previousAssistantReasoning = '';
964+ if (reasoningIsEligible) {
965+ if (toolReasoningMode === tool_reasoning_modes.ACTIVE_CHAIN) {
966+ // Strict chain mode: skip tool/tool-call messages, then use only the first assistant text boundary.
967+ for (let idx = promptIdx - 1; idx > lastUserIdx; idx--) {
968+ const candidate = messages[idx];
969+ if (candidate?.role === 'tool') {
970+ continue;
971+ }
972+ if (candidate?.role === 'assistant' && Array.isArray(candidate.invocations)) {
973+ continue;
974+ }
975+ const hasAssistantText = candidate?.role === 'assistant'
976+ && !Array.isArray(candidate.invocations)
977+ && typeof candidate.content === 'string'
978+ && candidate.content.trim().length > 0;
979+ if (hasAssistantText) {
980+ previousAssistantReasoning = String(candidate.reasoning ?? '');
981+ }
982+ break;
983+ }
984+ } else if (toolReasoningMode === tool_reasoning_modes.SINCE_LAST_USER) {
985+ // Broad mode: use the latest assistant text reasoning anywhere since the last user.
986+ for (let idx = promptIdx - 1; idx > lastUserIdx; idx--) {
987+ const candidate = messages[idx];
988+ const hasAssistantText = candidate?.role === 'assistant'
989+ && !Array.isArray(candidate.invocations)
990+ && typeof candidate.content === 'string'
991+ && candidate.content.trim().length > 0;
992+ if (!hasAssistantText) {
993+ continue;
994+ }
995+ const candidateReasoning = String(candidate.reasoning ?? '');
996+ if (candidateReasoning) {
997+ previousAssistantReasoning = candidateReasoning;
998+ break;
999+ }
1000+ }
1001+ }
1002+ }
9391003 /** @type {import('./tool-calling.js').ToolInvocation[]} */
9401004 const invocations = chatPrompt.invocations;.map(invocation => {
1005+ const clone = structuredClone(invocation);
1006+ if (!reasoningIsEligible) {
1007+ delete clone.reasoning;
1008+ } else if (previousAssistantReasoning) {
1009+ // Prefer currently editable assistant-text reasoning based on forwarding mode over invocation snapshot.
1010+ clone.reasoning = previousAssistantReasoning;
1011+ }
1012+ return clone;
1013+ });
9411014 const toolCallMessage = await Message.createAsync(chatMessage.role, undefined, 'toolCall-' + chatMessage.identifier);
9421015 const toolResultMessages = await Promise.all(invocations.slice().reverse().map((invocation) => Message.createAsync('tool', invocation.result || '[No content]', invocation.id)));
9431016 await toolCallMessage.setToolCalls(invocations, includeSignature, includeToolReasoning);
9441017 if (chatCompletion.canAffordAll([toolCallMessage, ...toolResultMessages])) {
9451018 for (const resultMessage of toolResultMessages) {
9461019 chatCompletion.insertAtStart(resultMessage, 'chatHistory');
@@ -2939,15 +3012,25 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov
29393012 state.images.push(...imageUrls.filter(isDataURL));
29403013 }
29413014 if (show_thoughts) {
2942- state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
3015+ state.reasoning +=
3016+ data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning ??
3017+ data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ??
3018+ data.choices?.filter(x => x?.message?.reasoning)?.[0]?.message?.reasoning ??
3019+ data.choices?.filter(x => x?.message?.reasoning_content)?.[0]?.message?.reasoning_content ??
3020+ '';
29433021 }
29443022 // Extract thought signatures from OpenRouter streaming (reasoning_details in delta).
2945- const reasoningDetails = data?.choices?.[0]?.delta?.reasoning_details || [];
3023+ const reasoningDetails = [
3024+ ...(data?.choices?.[0]?.delta?.reasoning_details || []),
3025+ ...(data?.choices?.[0]?.message?.reasoning_details || []),
3026+ ];
29463027 reasoningDetails.forEach((detail) => {
29473028 if (detail.type === 'reasoning.encrypted' && detail.data) {
2948- if (/^tool_/.test(detail.id)) {
3029+ const isToolLikeId = typeof detail.id === 'string' && /^(tool_|call_)/.test(detail.id);
3030+ if (typeof detail.id === 'string' && detail.id.length > 0) {
29493031 state.toolSignatures[detail.id] = detail.data;
29503032 } else {
3033+ if (!isToolLikeId) {
29513034 state.signature = detail.data;
29523035 }
29533036 }
@@ -3191,6 +3274,8 @@ class Message {
31913274 tool_call = null;
31923275 /** @type {string?} */
31933276 signature = null;
3277+ /** @type {string?} */
3278+ reasoning = null;
31943279
31953280 /**
31963281 * @constructor
@@ -3233,9 +3318,10 @@ class Message {
32333318 * Reconstruct the message from a tool invocation.
32343319 * @param {import('./tool-calling.js').ToolInvocation[]} invocations - The tool invocations to reconstruct the message from.
32353320 * @param {boolean} includeSignature Whether to include the signature in the tool calls.
3321+ * @param {boolean} includeReasoning Whether to include plaintext reasoning fallback.
32363322 * @returns {Promise<void>}
32373323 */
32383324 async setToolCalls(invocations, includeSignature, includeReasoning = false) {
32393325 this.tool_calls = invocations.map(i => ({
32403326 id: i.id,
32413327 type: 'function',
@@ -3245,7 +3331,13 @@ class Message {
32453331 },
32463332 ...(includeSignature && i.signature ? { signature: i.signature } : {}),
32473333 }));
3248- this.tokens = await tokenHandler.countAsync({ role: this.role, tool_calls: JSON.stringify(this.tool_calls) });
3334+ const fallbackReasoning = invocations.find(i => typeof i.reasoning === 'string' && i.reasoning.length > 0)?.reasoning || null;
3335+ this.reasoning = includeReasoning ? fallbackReasoning : null;
3336+ this.tokens = await tokenHandler.countAsync({
3337+ role: this.role,
3338+ tool_calls: JSON.stringify(this.tool_calls),
3339+ ...(this.reasoning ? { reasoning: this.reasoning } : {}),
3340+ });
32493341 }
32503342
32513343 /**
@@ -3496,6 +3588,7 @@ class MessageCollection {
34963588 ...(message.tool_calls && { tool_calls: message.tool_calls }),
34973589 ...(message.role === 'tool' && { tool_call_id: message.identifier }),
34983590 ...(message.signature && { signature: message.signature }),
3591+ ...(message.reasoning && { reasoning: message.reasoning }),
34993592 });
35003593 }
35013594 return acc;
@@ -3786,6 +3879,7 @@ export class ChatCompletion {
37863879 ...(item.tool_calls ? { tool_calls: item.tool_calls } : {}),
37873880 ...(item.role === 'tool' ? { tool_call_id: item.identifier } : {}),
37883881 ...(item.signature ? { signature: item.signature } : {}),
3882+ ...(item.reasoning ? { reasoning: item.reasoning } : {}),
37893883 };
37903884 chat.push(message);
37913885 } else {
@@ -4039,6 +4133,7 @@ function loadOpenAISettings(data, settings) {
40394133
40404134 setNamesBehaviorControls();
40414135 setContinuePostfixControls();
4136+ setToolReasoningControls();
40424137
40434138 $('#openrouter_providers_chat').trigger('change');
40444139 $('#openrouter_quantizations_chat').trigger('change');
@@ -4091,6 +4186,12 @@ function setContinuePostfixControls() {
40914186 $('#continue_postfix_display').text(checkedItemText);
40924187}
40934188
4189+function setToolReasoningControls() {
4190+ const isEnabled = oai_settings.show_thoughts;
4191+ $('#tool_reasoning_mode').prop('disabled', !isEnabled);
4192+ $('#openrouter_interleaved_thinking_disabled_hint').toggle(!isEnabled);
4193+}
4194+
40944195async function getStatusOpen() {
40954196 const noValidateSources = [
40964197 chat_completion_sources.CLAUDE,
@@ -5680,6 +5781,8 @@ function toggleChatCompletionForms() {
56805781 const matchesSource = validSources.includes(oai_settings.chat_completion_source);
56815782 $(this).toggle(mode !== 'except' ? matchesSource : !matchesSource);
56825783 });
5784+
5785+ setToolReasoningControls();
56835786}
56845787
56855788async function testApiConnection() {
@@ -5936,6 +6039,33 @@ export function isAudioInliningSupported() {
59366039}
59376040
59386041/**
6042+ * Gets the tool-call reasoning forwarding mode.
6043+ * @param {ChatCompletionSettings} settings Settings object to use
6044+ * @returns {string} Reasoning forwarding mode
6045+ */
6046+function getToolReasoningMode(settings = oai_settings) {
6047+ const mode = String(settings.tool_reasoning_mode ?? '');
6048+ if (Object.values(tool_reasoning_modes).includes(mode)) {
6049+ return mode;
6050+ }
6051+ return tool_reasoning_modes.DISABLED;
6052+}
6053+
6054+/**
6055+ * Gets the effective tool-call reasoning forwarding mode.
6056+ * Interleaved thinking requires explicit reasoning requests.
6057+ * @param {ChatCompletionSettings} settings Settings object to use
6058+ * @returns {string} Effective reasoning forwarding mode
6059+ */
6060+function getEffectiveToolReasoningMode(settings = oai_settings) {
6061+ if (!settings.show_thoughts) {
6062+ return tool_reasoning_modes.DISABLED;
6063+ }
6064+
6065+ return getToolReasoningMode(settings);
6066+}
6067+
6068+/**
59396069 * Check if the model supports encrypted reasoning signatures.
59406070 * @param {ChatCompletionSettings} settings Settings object to use
59416071 * @returns {boolean} True if reasoning signatures should be included in the request
@@ -6539,6 +6669,14 @@ export function initOpenAI() {
65396669 saveSettingsDebounced();
65406670 });
65416671
6672+ $('#tool_reasoning_mode').on('input', function () {
6673+ oai_settings.tool_reasoning_mode = getToolReasoningMode({
6674+ ...oai_settings,
6675+ tool_reasoning_mode: String($(this).val()),
6676+ });
6677+ saveSettingsDebounced();
6678+ });
6679+
65426680 $('#seed_openai').on('input', function () {
65436681 oai_settings.seed = Number($(this).val());
65446682 saveSettingsDebounced();
@@ -6642,6 +6780,7 @@ export function initOpenAI() {
66426780
66436781 $('#openai_show_thoughts').on('input', function () {
66446782 oai_settings.show_thoughts = !!$(this).prop('checked');
6783+ setToolReasoningControls();
66456784 saveSettingsDebounced();
66466785 });
66476786
public/scripts/reasoning.js+3 -1
@@ -115,7 +115,9 @@ export function extractReasoningFromData(data, {
115115 case chat_completion_sources.XAI:
116116 return data?.choices?.[0]?.message?.reasoning_content ?? '';
117117 case chat_completion_sources.OPENROUTER:
118118 return data?.choices?.[0]?.message?.reasoning ?? '';
119+ ?? data?.choices?.[0]?.message?.reasoning_content
120+ ?? '';
119121 case chat_completion_sources.MAKERSUITE:
120122 case chat_completion_sources.VERTEXAI:
121123 return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
public/scripts/tool-calling.js+3 -1
@@ -20,6 +20,7 @@ import { isTrueBoolean } from './utils.js';
2020 * @property {string} parameters - The parameters for the tool invocation.
2121 * @property {string} result - The result of the tool invocation.
2222 * @property {string?} signature - The thought signature associated with the tool invocation.
23+ * @property {string?} reasoning - The plaintext reasoning associated with this tool call turn.
2324 */
2425
2526/**
@@ -765,7 +766,7 @@ export class ToolManager {
765766 * @param {any} data Reply data
766767 * @returns {Promise<ToolInvocationResult>} Successful tool invocations
767768 */
768769 static async invokeFunctionTools(data, { reasoningText = null } = {}) {
769770 /** @type {ToolInvocationResult} */
770771 const result = {
771772 invocations: [],
@@ -814,6 +815,7 @@ export class ToolManager {
814815 parameters: stringify(parameters),
815816 result: toolResult,
816817 signature: toolCall.signature || null,
818+ reasoning: reasoningText || null,
817819 };
818820 result.invocations.push(invocation);
819821 }