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>
Signed| @@ -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 "Request model reasoning" 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" /> |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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 | } |
| @@ -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": "添加于", |
| @@ -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 | } |
| @@ -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) { |
| @@ -246,6 +246,17 @@ export const verbosity_levels = { | |||
| 246 | high: 'high', | 246 | high: 'high', |
| 247 | }; | 247 | }; |
| 248 | 248 | ||
| 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 | |||
| 249 | export const ZAI_ENDPOINT = { | 260 | export 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 ?? '') : ''; | ||
| 580 | 594 | ||
| 581 | // Remove signatures from invocations if the API/model don't match | 595 | // 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 | } |
| 591 | 606 | ||
| 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 | } |
| 595 | 610 | ||
| @@ -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'); | ||
| 888 | 909 | ||
| 889 | // Insert chat messages as long as there is budget available | 910 | // 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 | } |
| 937 | 958 | ||
| 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; | ||
| 3194 | 3279 | ||
| 3195 | /** | 3280 | /** |
| 3196 | * @constructor | 3281 | * @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 | } |
| 3250 | 3342 | ||
| 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) { | |||
| 4039 | 4133 | ||
| 4040 | setNamesBehaviorControls(); | 4134 | setNamesBehaviorControls(); |
| 4041 | setContinuePostfixControls(); | 4135 | setContinuePostfixControls(); |
| 4136 | setToolReasoningControls(); | ||
| 4042 | 4137 | ||
| 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 | } |
| 4093 | 4188 | ||
| 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 | |||
| 4094 | async function getStatusOpen() { | 4195 | async 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 | } |
| 5684 | 5787 | ||
| 5685 | async function testApiConnection() { | 5788 | async function testApiConnection() { |
| @@ -5936,6 +6039,33 @@ export function isAudioInliningSupported() { | |||
| 5936 | } | 6039 | } |
| 5937 | 6040 | ||
| 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 | */ | ||
| 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 | /** | ||
| 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 use | 6070 | * @param {ChatCompletionSettings} settings Settings object to use |
| 5941 | * @returns {boolean} True if reasoning signatures should be included in the request | 6071 | * @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 | }); |
| 6541 | 6671 | ||
| 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() { | |||
| 6642 | 6780 | ||
| 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 | }); |
| 6647 | 6786 | ||
| @@ -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') ?? ''; |
| @@ -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 | */ |
| 24 | 25 | ||
| 25 | /** | 26 | /** |
| @@ -765,7 +766,7 @@ export class ToolManager { | |||
| 765 | * @param {any} data Reply data | 766 | * @param {any} data Reply data |
| 766 | * @returns {Promise<ToolInvocationResult>} Successful tool invocations | 767 | * @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 | } |