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 -31Showing whitespace changes
public/index.html+24 -0
@@ -2011,6 +2011,30 @@
2011 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>2011 <strong data-i18n="enable_functions_desc_4">Not supported when Prompt Post-Processing with "no tools" is used!</strong>
2012 </div>2012 </div>
2013 </div>2013 </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>
2014 <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">2038 <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">
2015 <label for="openai_media_inlining" class="checkbox_label flexWrap widthFreeExpand">2039 <label for="openai_media_inlining" class="checkbox_label flexWrap widthFreeExpand">
2016 <input id="openai_media_inlining" type="checkbox" />2040 <input id="openai_media_inlining" type="checkbox" />
public/locales/ar-sa.json+5 -1
@@ -1438,5 +1438,9 @@
1438 "Still have questions?": "هل ما زالت لديك اسئلة؟",1438 "Still have questions?": "هل ما زالت لديك اسئلة؟",
1439 "Join the SillyTavern Discord": "انضم إلى ديسكورد SillyTavern",1439 "Join the SillyTavern Discord": "انضم إلى ديسكورد SillyTavern",
1440 "Post a GitHub issue": "نشر مشكلة على GitHub",1440 "Post a GitHub issue": "نشر مشكلة على GitHub",
1441 "Contact the developers": "الاتصال بالمطورين"1441 "Contact the developers": "الاتصال بالمطورين",
1442 "Interleaved Thinking": "التفكير المتداخل",
1443 "Since Last User Message": "منذ آخر رسالة من المستخدم",
1444 "Active Tool Chain": "سلسلة الأدوات النشطة",
1445 "openrouter_interleaved_thinking_hint": "يرسل الاستدلال من رسائل المساعد السابقة مع طلبات استدعاء الأدوات للحفاظ على سياق التفكير المتداخل."
1442}1446}
public/locales/de-de.json+5 -1
@@ -1438,5 +1438,9 @@
1438 "Still have questions?": "Hast du immer noch Fragen?",1438 "Still have questions?": "Hast du immer noch Fragen?",
1439 "Join the SillyTavern Discord": "Trete dem SillyTavern Discord bei",1439 "Join the SillyTavern Discord": "Trete dem SillyTavern Discord bei",
1440 "Post a GitHub issue": "Veröffentliche ein GitHub-Problem",1440 "Post a GitHub issue": "Veröffentliche ein GitHub-Problem",
1441 "Contact the developers": "Kontaktiere die Entwickler"1441 "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."
1442}1446}
public/locales/es-es.json+5 -1
@@ -1545,5 +1545,9 @@
1545 "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)",1545 "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)",
1546 "Sets the expression of the user sprite": "Establece la expresión del sprite de usuario",1546 "Sets the expression of the user sprite": "Establece la expresión del sprite de usuario",
1547 "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",1547 "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",
1548 "Toggles the user sprite on the VN UI": "Alterna el sprite de usuario en la interfaz VN"1548 "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."
1549}1553}
public/locales/fr-fr.json+5 -1
@@ -2046,5 +2046,9 @@
2046 "System Prompt Name": "Nom du prompt système:",2046 "System Prompt Name": "Nom du prompt système:",
2047 "Instruct Mode": "Mode Instruction:",2047 "Instruct Mode": "Mode Instruction:",
2048 "Save and Update": "Sauvegarder et mettre à jour",2048 "Save and Update": "Sauvegarder et mettre à jour",
2049 "Don't ask again for this URL": "Ne plus demander pour cette URL"2049 "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é."
2050}2054}
public/locales/is-is.json+5 -1
@@ -1436,5 +1436,9 @@
1436 "Still have questions?": "Ertu enn með spurningar?",1436 "Still have questions?": "Ertu enn með spurningar?",
1437 "Join the SillyTavern Discord": "Skráðu þig í SillyTavern Discord",1437 "Join the SillyTavern Discord": "Skráðu þig í SillyTavern Discord",
1438 "Post a GitHub issue": "Senda inn GitHub málefni",1438 "Post a GitHub issue": "Senda inn GitHub málefni",
1439 "Contact the developers": "Hafa samband við þróunaraðila"1439 "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."
1440}1444}
public/locales/it-it.json+5 -1
@@ -1438,5 +1438,9 @@
1438 "Still have questions?": "Hai ancora domande?",1438 "Still have questions?": "Hai ancora domande?",
1439 "Join the SillyTavern Discord": "Unisciti al SillyTavern Discord",1439 "Join the SillyTavern Discord": "Unisciti al SillyTavern Discord",
1440 "Post a GitHub issue": "Pubblica un problema su GitHub",1440 "Post a GitHub issue": "Pubblica un problema su GitHub",
1441 "Contact the developers": "Contatta gli sviluppatori"1441 "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."
1442}1446}
public/locales/ja-jp.json+5 -1
@@ -1443,5 +1443,9 @@
1443 "Contact the developers": "開発者に連絡",1443 "Contact the developers": "開発者に連絡",
1444 "Stop Inspecting": "検査を停止",1444 "Stop Inspecting": "検査を停止",
1445 "Inspect Prompts": "プロンプトを検査",1445 "Inspect Prompts": "プロンプトを検査",
1446 "Toggle prompt inspection": "プロンプト検査の切り替え"1446 "Toggle prompt inspection": "プロンプト検査の切り替え",
1447 "Interleaved Thinking": "インターリーブ思考",
1448 "Since Last User Message": "直前のユーザーメッセージ以降",
1449 "Active Tool Chain": "アクティブなツールチェーン",
1450 "openrouter_interleaved_thinking_hint": "インターリーブ思考の文脈を維持するため、直前のアシスタントターンの推論をツール呼び出しリクエストとともに送信します。"
1447}1451}
public/locales/ko-kr.json+5 -1
@@ -1617,5 +1617,9 @@
1617 "to install additional features.": "으로 가세요.",1617 "to install additional features.": "으로 가세요.",
1618 "Master Import": "마스터 불러오기",1618 "Master Import": "마스터 불러오기",
1619 "Master Export": "마스터 내보내기",1619 "Master Export": "마스터 내보내기",
1620 "Chat Quick Reply Sets": "채팅 빠른 답장 세트들"1620 "Chat Quick Reply Sets": "채팅 빠른 답장 세트들",
1621 "Interleaved Thinking": "인터리브드 사고",
1622 "Since Last User Message": "마지막 사용자 메시지 이후",
1623 "Active Tool Chain": "활성 도구 체인",
1624 "openrouter_interleaved_thinking_hint": "인터리브드 사고 컨텍스트를 유지하기 위해 이전 어시스턴트 턴의 추론을 도구 호출 요청과 함께 전송합니다."
1621}1625}
public/locales/nl-nl.json+5 -1
@@ -1434,5 +1434,9 @@
1434 "Still have questions?": "Heb je nog vragen?",1434 "Still have questions?": "Heb je nog vragen?",
1435 "Join the SillyTavern Discord": "Word lid van de SillyTavern Discord",1435 "Join the SillyTavern Discord": "Word lid van de SillyTavern Discord",
1436 "Post a GitHub issue": "Plaats een GitHub-probleem",1436 "Post a GitHub issue": "Plaats een GitHub-probleem",
1437 "Contact the developers": "Neem contact op met de ontwikkelaars"1437 "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."
1438}1442}
public/locales/pt-pt.json+5 -1
@@ -1436,5 +1436,9 @@
1436 "Still have questions?": "Ainda tem perguntas?",1436 "Still have questions?": "Ainda tem perguntas?",
1437 "Join the SillyTavern Discord": "Junte-se ao Discord do SillyTavern",1437 "Join the SillyTavern Discord": "Junte-se ao Discord do SillyTavern",
1438 "Post a GitHub issue": "Publicar um problema no GitHub",1438 "Post a GitHub issue": "Publicar um problema no GitHub",
1439 "Contact the developers": "Contatar os desenvolvedores"1439 "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."
1440}1444}
public/locales/ru-ru.json+5 -1
@@ -2738,5 +2738,9 @@
2738 "Uses the voices provided by your operating system": "Используем стандартные голоса вашей ОС",2738 "Uses the voices provided by your operating system": "Используем стандартные голоса вашей ОС",
2739 "Your browser or operating system doesn't support speech synthesis": "Ваш браузер либо ОС не поддерживают синтез речи",2739 "Your browser or operating system doesn't support speech synthesis": "Ваш браузер либо ОС не поддерживают синтез речи",
2740 "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-движку\", а также отключены обе опции: \"Не озвучивать *текст, даже \"в кавычках\", если он внутри звёздочек*\", и \"Озвучивать только текст \"в кавычках\"\"",2740 "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-движку\", а также отключены обе опции: \"Не озвучивать *текст, даже \"в кавычках\", если он внутри звёздочек*\", и \"Озвучивать только текст \"в кавычках\"\"",
2741 "Available voices": "Доступные голоса"2741 "Available voices": "Доступные голоса",
2742 "Interleaved Thinking": "Чередующееся рассуждение",
2743 "Since Last User Message": "С момента последнего сообщения пользователя",
2744 "Active Tool Chain": "Активная цепочка инструментов",
2745 "openrouter_interleaved_thinking_hint": "Отправляет рассуждение из предыдущих ходов ассистента вместе с запросами вызова инструментов, чтобы сохранять контекст чередующегося рассуждения."
2742}2746}
public/locales/th-th.json+5 -1
@@ -1455,5 +1455,9 @@
1455 "Contact the developers": "ติดต่อผู้พัฒนา",1455 "Contact the developers": "ติดต่อผู้พัฒนา",
1456 "Stop Inspecting": "Stop Inspecting",1456 "Stop Inspecting": "Stop Inspecting",
1457 "Inspect Prompts": "ตรวจสอบ Prompt",1457 "Inspect Prompts": "ตรวจสอบ Prompt",
1458 "Toggle prompt inspection": "สลับการตรวจสอบ Prompt"1458 "Toggle prompt inspection": "สลับการตรวจสอบ Prompt",
1459 "Interleaved Thinking": "การคิดแบบสลับขั้น",
1460 "Since Last User Message": "ตั้งแต่ข้อความล่าสุดของผู้ใช้",
1461 "Active Tool Chain": "สายโซ่เครื่องมือที่ใช้งานอยู่",
1462 "openrouter_interleaved_thinking_hint": "ส่งเหตุผลจากเทิร์นก่อนหน้าของผู้ช่วยไปพร้อมกับคำขอเรียกใช้เครื่องมือ เพื่อคงบริบทการคิดแบบสลับขั้นไว้"
1459}1463}
public/locales/uk-ua.json+5 -1
@@ -1436,5 +1436,9 @@
1436 "Still have questions?": "Все ще є питання?",1436 "Still have questions?": "Все ще є питання?",
1437 "Join the SillyTavern Discord": "Приєднуйтесь до SillyTavern Discord",1437 "Join the SillyTavern Discord": "Приєднуйтесь до SillyTavern Discord",
1438 "Post a GitHub issue": "Опублікуйте проблему на GitHub",1438 "Post a GitHub issue": "Опублікуйте проблему на GitHub",
1439 "Contact the developers": "Зв'яжіться з розробниками"1439 "Contact the developers": "Зв'яжіться з розробниками",
1440 "Interleaved Thinking": "Черговане мислення",
1441 "Since Last User Message": "Від останнього повідомлення користувача",
1442 "Active Tool Chain": "Активний ланцюжок інструментів",
1443 "openrouter_interleaved_thinking_hint": "Надсилає міркування з попередніх ходів асистента разом із запитами виклику інструментів, щоб зберегти контекст чергованого мислення."
1440}1444}
public/locales/vi-vn.json+5 -1
@@ -1436,5 +1436,9 @@
1436 "Still have questions?": "Bạn còn thắc mắc?",1436 "Still have questions?": "Bạn còn thắc mắc?",
1437 "Join the SillyTavern Discord": "Tham gia Kênh Discord của SillyTavern",1437 "Join the SillyTavern Discord": "Tham gia Kênh Discord của SillyTavern",
1438 "Post a GitHub issue": "Đăng một vấn đề trên GitHub",1438 "Post a GitHub issue": "Đăng một vấn đề trên GitHub",
1439 "Contact the developers": "Liên hệ với Devs"1439 "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ẽ."
1440}1444}
public/locales/zh-cn.json+4 -0
@@ -3517,6 +3517,10 @@
3517 "Title/Memo": "标题(备忘)",3517 "Title/Memo": "标题(备忘)",
3518 "Strategy": "触发策略",3518 "Strategy": "触发策略",
3519 "Trigger %": "触发概率 %",3519 "Trigger %": "触发概率 %",
3520 "Interleaved Thinking": "交错思维",
3521 "Since Last User Message": "自上一条用户消息起",
3522 "Active Tool Chain": "活动工具链",
3523 "openrouter_interleaved_thinking_hint": "发送前面助手轮次的推理并附在工具调用请求中,以维持交错思维上下文。",
3520 "Class": "类型",3524 "Class": "类型",
3521 "Context Length": "上下文长度",3525 "Context Length": "上下文长度",
3522 "Added On": "添加于",3526 "Added On": "添加于",
public/locales/zh-tw.json+5 -1
@@ -2758,5 +2758,9 @@
2758 "[title]Create a new": "新增",2758 "[title]Create a new": "新增",
2759 "[title]Delete a ": "刪除 ",2759 "[title]Delete a ": "刪除 ",
2760 "[title]Rename a ": "重新命名 ",2760 "[title]Rename a ": "重新命名 ",
2761 "completions note prefix": "完成提示前綴"2761 "completions note prefix": "完成提示前綴",
2762 "Interleaved Thinking": "交錯思維",
2763 "Since Last User Message": "自上一則使用者訊息起",
2764 "Active Tool Chain": "使用中的工具鏈",
2765 "openrouter_interleaved_thinking_hint": "會在工具呼叫請求中附帶先前助理回合的推理,以維持交錯思維的上下文。"
2762}2766}
public/script.js+4 -2
@@ -5216,7 +5216,9 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
5216 const hasToolCalls = ToolManager.hasToolCalls(streamingProcessor.toolCalls);5216 const hasToolCalls = ToolManager.hasToolCalls(streamingProcessor.toolCalls);
5217 const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(lastMessage?.mes) && !lastMessage?.extra?.reasoning && ['', '...'].includes(streamingProcessor?.result);5217 const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(lastMessage?.mes) && !lastMessage?.extra?.reasoning && ['', '...'].includes(streamingProcessor?.result);
5218 hasToolCalls && shouldDeleteMessage && await deleteLastMessage();5218 hasToolCalls && shouldDeleteMessage && await deleteLastMessage();
5219 const invocationResult = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls);5219 const invocationResult = await ToolManager.invokeFunctionTools(streamingProcessor.toolCalls, {
5220 reasoningText: streamingProcessor.reasoningHandler.reasoning,
5221 });
5220 const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length;5222 const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length;
5221 if (hasToolCalls) {5223 if (hasToolCalls) {
5222 if (shouldStopGeneration) {5224 if (shouldStopGeneration) {
@@ -5341,7 +5343,7 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
5341 const hasToolCalls = ToolManager.hasToolCalls(data);5343 const hasToolCalls = ToolManager.hasToolCalls(data);
5342 const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(getMessage) && !reasoning;5344 const shouldDeleteMessage = type !== 'swipe' && ['', '...'].includes(getMessage) && !reasoning;
5343 hasToolCalls && shouldDeleteMessage && await deleteLastMessage();5345 hasToolCalls && shouldDeleteMessage && await deleteLastMessage();
5344 const invocationResult = await ToolManager.invokeFunctionTools(data);5346 const invocationResult = await ToolManager.invokeFunctionTools(data, { reasoningText: reasoning });
5345 const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length;5347 const shouldStopGeneration = (!invocationResult.invocations.length && shouldDeleteMessage) || invocationResult.stealthCalls.length;
5346 if (hasToolCalls) {5348 if (hasToolCalls) {
5347 if (shouldStopGeneration) {5349 if (shouldStopGeneration) {
public/scripts/openai.js+151 -12
@@ -246,6 +246,17 @@ export const verbosity_levels = {
246 high: 'high',246 high: 'high',
247};247};
248248
249export 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.
256const interleaved_reasoning_providers = [
257 chat_completion_sources.OPENROUTER,
258];
259
249export const ZAI_ENDPOINT = {260export const ZAI_ENDPOINT = {
250 COMMON: 'common',261 COMMON: 'common',
251 CODING: 'coding',262 CODING: 'coding',
@@ -289,6 +300,7 @@ export const settingsToUpdate = {
289 openrouter_quantizations: ['#openrouter_quantizations_chat', 'openrouter_quantizations', false, true],300 openrouter_quantizations: ['#openrouter_quantizations_chat', 'openrouter_quantizations', false, true],
290 openrouter_allow_fallbacks: ['#openrouter_allow_fallbacks', 'openrouter_allow_fallbacks', true, true],301 openrouter_allow_fallbacks: ['#openrouter_allow_fallbacks', 'openrouter_allow_fallbacks', true, true],
291 openrouter_middleout: ['#openrouter_middleout', 'openrouter_middleout', false, true],302 openrouter_middleout: ['#openrouter_middleout', 'openrouter_middleout', false, true],
303 tool_reasoning_mode: ['#tool_reasoning_mode', 'tool_reasoning_mode', false, false],
292 ai21_model: ['#model_ai21_select', 'ai21_model', false, true],304 ai21_model: ['#model_ai21_select', 'ai21_model', false, true],
293 mistralai_model: ['#model_mistralai_select', 'mistralai_model', false, true],305 mistralai_model: ['#model_mistralai_select', 'mistralai_model', false, true],
294 cohere_model: ['#model_cohere_select', 'cohere_model', false, true],306 cohere_model: ['#model_cohere_select', 'cohere_model', false, true],
@@ -436,6 +448,7 @@ const default_settings = {
436 openrouter_quantizations: [],448 openrouter_quantizations: [],
437 openrouter_allow_fallbacks: true,449 openrouter_allow_fallbacks: true,
438 openrouter_middleout: openrouter_middleout_types.ON,450 openrouter_middleout: openrouter_middleout_types.ON,
451 tool_reasoning_mode: tool_reasoning_modes.DISABLED,
439 reverse_proxy: '',452 reverse_proxy: '',
440 chat_completion_source: chat_completion_sources.OPENAI,453 chat_completion_source: chat_completion_sources.OPENAI,
441 max_context_unlocked: false,454 max_context_unlocked: false,
@@ -577,19 +590,21 @@ function setOpenAIMessages(chat) {
577 const originModel = chat[j]?.extra?.model;590 const originModel = chat[j]?.extra?.model;
578 const isSameModel = originApi === currentApi && originModel === currentModel;591 const isSameModel = originApi === currentApi && originModel === currentModel;
579 const signature = isSameModel ? chat[j]?.extra?.reasoning_signature : null;592 const signature = isSameModel ? chat[j]?.extra?.reasoning_signature : null;
593 const reasoning = isSameModel ? String(chat[j]?.extra?.reasoning ?? '') : '';
580594
581 // Remove signatures from invocations if the API/model don't match595 // Remove reasoning metadata from invocations if the API/model don't match
582 if (Array.isArray(invocations) && invocations.length > 0) {596 if (Array.isArray(invocations) && invocations.length > 0) {
583 invocations.forEach((invocation, index) => {597 invocations.forEach((invocation, index) => {
584 if (invocation.signature && !isSameModel) {598 if (!isSameModel && (invocation.signature || invocation.reasoning)) {
585 const cloneInvocation = structuredClone(invocation);599 const cloneInvocation = structuredClone(invocation);
586 delete cloneInvocation.signature;600 delete cloneInvocation.signature;
601 delete cloneInvocation.reasoning;
587 invocations[index] = cloneInvocation;602 invocations[index] = cloneInvocation;
588 }603 }
589 });604 });
590 }605 }
591606
592 messages[i] = { 'role': role, 'content': content, name: name, 'media': media, 'mediaDisplay': mediaDisplay, 'mediaIndex': mediaIndex, 'invocations': invocations, 'signature': signature };607 messages[i] = { 'role': role, 'content': content, name: name, 'media': media, 'mediaDisplay': mediaDisplay, 'mediaIndex': mediaIndex, 'invocations': invocations, 'signature': signature, 'reasoning': reasoning };
593 j++;608 j++;
594 }609 }
595610
@@ -885,6 +900,12 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
885 const audioInlining = isAudioInliningSupported();900 const audioInlining = isAudioInliningSupported();
886 const canUseTools = ToolManager.isToolCallingSupported();901 const canUseTools = ToolManager.isToolCallingSupported();
887 const includeSignature = isReasoningSignatureSupported();902 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
889 // Insert chat messages as long as there is budget available910 // Insert chat messages as long as there is budget available
890 const chatPool = [...messages].reverse();911 const chatPool = [...messages].reverse();
@@ -936,11 +957,63 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
936 }957 }
937958
938 if (canUseTools && Array.isArray(chatPrompt.invocations)) {959 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 }
939 /** @type {import('./tool-calling.js').ToolInvocation[]} */1003 /** @type {import('./tool-calling.js').ToolInvocation[]} */
940 const invocations = chatPrompt.invocations;1004 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 });
941 const toolCallMessage = await Message.createAsync(chatMessage.role, undefined, 'toolCall-' + chatMessage.identifier);1014 const toolCallMessage = await Message.createAsync(chatMessage.role, undefined, 'toolCall-' + chatMessage.identifier);
942 const toolResultMessages = await Promise.all(invocations.slice().reverse().map((invocation) => Message.createAsync('tool', invocation.result || '[No content]', invocation.id)));1015 const toolResultMessages = await Promise.all(invocations.slice().reverse().map((invocation) => Message.createAsync('tool', invocation.result || '[No content]', invocation.id)));
943 await toolCallMessage.setToolCalls(invocations, includeSignature);1016 await toolCallMessage.setToolCalls(invocations, includeSignature, includeToolReasoning);
944 if (chatCompletion.canAffordAll([toolCallMessage, ...toolResultMessages])) {1017 if (chatCompletion.canAffordAll([toolCallMessage, ...toolResultMessages])) {
945 for (const resultMessage of toolResultMessages) {1018 for (const resultMessage of toolResultMessages) {
946 chatCompletion.insertAtStart(resultMessage, 'chatHistory');1019 chatCompletion.insertAtStart(resultMessage, 'chatHistory');
@@ -2939,15 +3012,25 @@ export function getStreamingReply(data, state, { chatCompletionSource = null, ov
2939 state.images.push(...imageUrls.filter(isDataURL));3012 state.images.push(...imageUrls.filter(isDataURL));
2940 }3013 }
2941 if (show_thoughts) {3014 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 '';
2943 }3021 }
2944 // Extract thought signatures from OpenRouter streaming (reasoning_details in delta)3022 // Extract thought signatures from OpenRouter streaming.
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 ];
2946 reasoningDetails.forEach((detail) => {3027 reasoningDetails.forEach((detail) => {
2947 if (detail.type === 'reasoning.encrypted' && detail.data) {3028 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) {
2949 state.toolSignatures[detail.id] = detail.data;3031 state.toolSignatures[detail.id] = detail.data;
2950 } else {3032 }
3033 if (!isToolLikeId) {
2951 state.signature = detail.data;3034 state.signature = detail.data;
2952 }3035 }
2953 }3036 }
@@ -3191,6 +3274,8 @@ class Message {
3191 tool_call = null;3274 tool_call = null;
3192 /** @type {string?} */3275 /** @type {string?} */
3193 signature = null;3276 signature = null;
3277 /** @type {string?} */
3278 reasoning = null;
31943279
3195 /**3280 /**
3196 * @constructor3281 * @constructor
@@ -3233,9 +3318,10 @@ class Message {
3233 * Reconstruct the message from a tool invocation.3318 * Reconstruct the message from a tool invocation.
3234 * @param {import('./tool-calling.js').ToolInvocation[]} invocations - The tool invocations to reconstruct the message from.3319 * @param {import('./tool-calling.js').ToolInvocation[]} invocations - The tool invocations to reconstruct the message from.
3235 * @param {boolean} includeSignature Whether to include the signature in the tool calls.3320 * @param {boolean} includeSignature Whether to include the signature in the tool calls.
3321 * @param {boolean} includeReasoning Whether to include plaintext reasoning fallback.
3236 * @returns {Promise<void>}3322 * @returns {Promise<void>}
3237 */3323 */
3238 async setToolCalls(invocations, includeSignature) {3324 async setToolCalls(invocations, includeSignature, includeReasoning = false) {
3239 this.tool_calls = invocations.map(i => ({3325 this.tool_calls = invocations.map(i => ({
3240 id: i.id,3326 id: i.id,
3241 type: 'function',3327 type: 'function',
@@ -3245,7 +3331,13 @@ class Message {
3245 },3331 },
3246 ...(includeSignature && i.signature ? { signature: i.signature } : {}),3332 ...(includeSignature && i.signature ? { signature: i.signature } : {}),
3247 }));3333 }));
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 });
3249 }3341 }
32503342
3251 /**3343 /**
@@ -3496,6 +3588,7 @@ class MessageCollection {
3496 ...(message.tool_calls && { tool_calls: message.tool_calls }),3588 ...(message.tool_calls && { tool_calls: message.tool_calls }),
3497 ...(message.role === 'tool' && { tool_call_id: message.identifier }),3589 ...(message.role === 'tool' && { tool_call_id: message.identifier }),
3498 ...(message.signature && { signature: message.signature }),3590 ...(message.signature && { signature: message.signature }),
3591 ...(message.reasoning && { reasoning: message.reasoning }),
3499 });3592 });
3500 }3593 }
3501 return acc;3594 return acc;
@@ -3786,6 +3879,7 @@ export class ChatCompletion {
3786 ...(item.tool_calls ? { tool_calls: item.tool_calls } : {}),3879 ...(item.tool_calls ? { tool_calls: item.tool_calls } : {}),
3787 ...(item.role === 'tool' ? { tool_call_id: item.identifier } : {}),3880 ...(item.role === 'tool' ? { tool_call_id: item.identifier } : {}),
3788 ...(item.signature ? { signature: item.signature } : {}),3881 ...(item.signature ? { signature: item.signature } : {}),
3882 ...(item.reasoning ? { reasoning: item.reasoning } : {}),
3789 };3883 };
3790 chat.push(message);3884 chat.push(message);
3791 } else {3885 } else {
@@ -4039,6 +4133,7 @@ function loadOpenAISettings(data, settings) {
40394133
4040 setNamesBehaviorControls();4134 setNamesBehaviorControls();
4041 setContinuePostfixControls();4135 setContinuePostfixControls();
4136 setToolReasoningControls();
40424137
4043 $('#openrouter_providers_chat').trigger('change');4138 $('#openrouter_providers_chat').trigger('change');
4044 $('#openrouter_quantizations_chat').trigger('change');4139 $('#openrouter_quantizations_chat').trigger('change');
@@ -4091,6 +4186,12 @@ function setContinuePostfixControls() {
4091 $('#continue_postfix_display').text(checkedItemText);4186 $('#continue_postfix_display').text(checkedItemText);
4092}4187}
40934188
4189function 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
4094async function getStatusOpen() {4195async function getStatusOpen() {
4095 const noValidateSources = [4196 const noValidateSources = [
4096 chat_completion_sources.CLAUDE,4197 chat_completion_sources.CLAUDE,
@@ -5680,6 +5781,8 @@ function toggleChatCompletionForms() {
5680 const matchesSource = validSources.includes(oai_settings.chat_completion_source);5781 const matchesSource = validSources.includes(oai_settings.chat_completion_source);
5681 $(this).toggle(mode !== 'except' ? matchesSource : !matchesSource);5782 $(this).toggle(mode !== 'except' ? matchesSource : !matchesSource);
5682 });5783 });
5784
5785 setToolReasoningControls();
5683}5786}
56845787
5685async function testApiConnection() {5788async function testApiConnection() {
@@ -5936,6 +6039,33 @@ export function isAudioInliningSupported() {
5936}6039}
59376040
5938/**6041/**
6042 * Gets the tool-call reasoning forwarding mode.
6043 * @param {ChatCompletionSettings} settings Settings object to use
6044 * @returns {string} Reasoning forwarding mode
6045 */
6046function 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 */
6060function getEffectiveToolReasoningMode(settings = oai_settings) {
6061 if (!settings.show_thoughts) {
6062 return tool_reasoning_modes.DISABLED;
6063 }
6064
6065 return getToolReasoningMode(settings);
6066}
6067
6068/**
5939 * Check if the model supports encrypted reasoning signatures.6069 * Check if the model supports encrypted reasoning signatures.
5940 * @param {ChatCompletionSettings} settings Settings object to use6070 * @param {ChatCompletionSettings} settings Settings object to use
5941 * @returns {boolean} True if reasoning signatures should be included in the request6071 * @returns {boolean} True if reasoning signatures should be included in the request
@@ -6539,6 +6669,14 @@ export function initOpenAI() {
6539 saveSettingsDebounced();6669 saveSettingsDebounced();
6540 });6670 });
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
6542 $('#seed_openai').on('input', function () {6680 $('#seed_openai').on('input', function () {
6543 oai_settings.seed = Number($(this).val());6681 oai_settings.seed = Number($(this).val());
6544 saveSettingsDebounced();6682 saveSettingsDebounced();
@@ -6642,6 +6780,7 @@ export function initOpenAI() {
66426780
6643 $('#openai_show_thoughts').on('input', function () {6781 $('#openai_show_thoughts').on('input', function () {
6644 oai_settings.show_thoughts = !!$(this).prop('checked');6782 oai_settings.show_thoughts = !!$(this).prop('checked');
6783 setToolReasoningControls();
6645 saveSettingsDebounced();6784 saveSettingsDebounced();
6646 });6785 });
66476786
public/scripts/reasoning.js+3 -1
@@ -115,7 +115,9 @@ export function extractReasoningFromData(data, {
115 case chat_completion_sources.XAI:115 case chat_completion_sources.XAI:
116 return data?.choices?.[0]?.message?.reasoning_content ?? '';116 return data?.choices?.[0]?.message?.reasoning_content ?? '';
117 case chat_completion_sources.OPENROUTER:117 case chat_completion_sources.OPENROUTER:
118 return data?.choices?.[0]?.message?.reasoning ?? '';118 return data?.choices?.[0]?.message?.reasoning
119 ?? data?.choices?.[0]?.message?.reasoning_content
120 ?? '';
119 case chat_completion_sources.MAKERSUITE:121 case chat_completion_sources.MAKERSUITE:
120 case chat_completion_sources.VERTEXAI:122 case chat_completion_sources.VERTEXAI:
121 return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';123 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';
20 * @property {string} parameters - The parameters for the tool invocation.20 * @property {string} parameters - The parameters for the tool invocation.
21 * @property {string} result - The result of the tool invocation.21 * @property {string} result - The result of the tool invocation.
22 * @property {string?} signature - The thought signature associated with the tool invocation.22 * @property {string?} signature - The thought signature associated with the tool invocation.
23 * @property {string?} reasoning - The plaintext reasoning associated with this tool call turn.
23 */24 */
2425
25/**26/**
@@ -765,7 +766,7 @@ export class ToolManager {
765 * @param {any} data Reply data766 * @param {any} data Reply data
766 * @returns {Promise<ToolInvocationResult>} Successful tool invocations767 * @returns {Promise<ToolInvocationResult>} Successful tool invocations
767 */768 */
768 static async invokeFunctionTools(data) {769 static async invokeFunctionTools(data, { reasoningText = null } = {}) {
769 /** @type {ToolInvocationResult} */770 /** @type {ToolInvocationResult} */
770 const result = {771 const result = {
771 invocations: [],772 invocations: [],
@@ -814,6 +815,7 @@ export class ToolManager {
814 parameters: stringify(parameters),815 parameters: stringify(parameters),
815 result: toolResult,816 result: toolResult,
816 signature: toolCall.signature || null,817 signature: toolCall.signature || null,
818 reasoning: reasoningText || null,
817 };819 };
818 result.invocations.push(invocation);820 result.invocations.push(invocation);
819 }821 }