Merge pull request #3551 from SillyTavern/claude-thonk Claude 3.7 think mode

19fba66d2c704d70fcdcba6834ee22baabfaa333

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
6 files changed, +75 -8Showing whitespace changes
public/index.html+5 -4
@@ -2000,7 +2000,7 @@
2000 </span>2000 </span>
2001 </div>2001 </div>
2002 </div>2002 </div>
2003 <div class="range-block" data-source="deepseek,openrouter,custom">2003 <div class="range-block" data-source="deepseek,openrouter,custom,claude">
2004 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">2004 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
2005 <input id="openai_show_thoughts" type="checkbox" />2005 <input id="openai_show_thoughts" type="checkbox" />
2006 <span>2006 <span>
@@ -2014,10 +2014,11 @@
2014 </span>2014 </span>
2015 </div>2015 </div>
2016 </div>2016 </div>
2017 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom">2017 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude">
2018 <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Currently supported values are low, medium, and high.&#10;Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response." data-i18n="[title]Constrains effort on reasoning for reasoning models.">2018 <div class="flex-container oneline-dropdown" title="Constrains effort on reasoning for reasoning models.&#10;Currently supported values are low, medium, and high.&#10;Reducing reasoning effort can result in faster responses and fewer tokens used on reasoning in a response." data-i18n="[title]Constrains effort on reasoning for reasoning models.">
2019 <label for="openai_reasoning_effort" data-i18n="Reasoning Effort">2019 <label for="openai_reasoning_effort">
2020 Reasoning Effort2020 <span data-i18n="Reasoning Effort">Reasoning Effort</span>
2021 <i data-source="claude" class="opacity50p fa-solid fa-circle-info" title="Allocates a portion of the response length for thinking (low: 10%, medium: 25%, high: 50%), but minimum 1024 tokens."></i>
2021 </label>2022 </label>
2022 <select id="openai_reasoning_effort">2023 <select id="openai_reasoning_effort">
2023 <option data-i18n="openai_reasoning_effort_low" value="low">Low</option>2024 <option data-i18n="openai_reasoning_effort_low" value="low">Low</option>
public/script.js+1 -1
@@ -5725,7 +5725,7 @@ function extractMessageFromData(data) {
5725 case 'novel':5725 case 'novel':
5726 return data.output;5726 return data.output;
5727 case 'openai':5727 case 'openai':
5728 return data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? data?.message?.content?.[0]?.text ?? data?.message?.tool_plan ?? '';5728 return data?.content?.find(p => p.type === 'text')?.text ?? data?.choices?.[0]?.message?.content ?? data?.choices?.[0]?.text ?? data?.text ?? data?.message?.content?.[0]?.text ?? data?.message?.tool_plan ?? '';
5729 default:5729 default:
5730 return '';5730 return '';
5731 }5731 }
public/scripts/openai.js+3 -0
@@ -2149,6 +2149,9 @@ async function sendOpenAIRequest(type, messages, signal) {
2149 */2149 */
2150function getStreamingReply(data, state) {2150function getStreamingReply(data, state) {
2151 if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {2151 if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {
2152 if (oai_settings.show_thoughts) {
2153 state.reasoning += data?.delta?.thinking || '';
2154 }
2152 return data?.delta?.text || '';2155 return data?.delta?.text || '';
2153 } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {2156 } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {
2154 if (oai_settings.show_thoughts) {2157 if (oai_settings.show_thoughts) {
public/scripts/reasoning.js+2 -0
@@ -76,6 +76,8 @@ export function extractReasoningFromData(data) {
76 return data?.choices?.[0]?.message?.reasoning ?? '';76 return data?.choices?.[0]?.message?.reasoning ?? '';
77 case chat_completion_sources.MAKERSUITE:77 case chat_completion_sources.MAKERSUITE:
78 return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';78 return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
79 case chat_completion_sources.CLAUDE:
80 return data?.content?.find(part => part.type === 'thinking')?.thinking ?? '';
79 case chat_completion_sources.CUSTOM: {81 case chat_completion_sources.CUSTOM: {
80 return data?.choices?.[0]?.message?.reasoning_content82 return data?.choices?.[0]?.message?.reasoning_content
81 ?? data?.choices?.[0]?.message?.reasoning83 ?? data?.choices?.[0]?.message?.reasoning
src/endpoints/backends/chat-completions.js+33 -3
@@ -28,6 +28,7 @@ import {
28 cachingAtDepthForOpenRouterClaude,28 cachingAtDepthForOpenRouterClaude,
29 cachingAtDepthForClaude,29 cachingAtDepthForClaude,
30 getPromptNames,30 getPromptNames,
31 calculateBudgetTokens,
31} from '../../prompt-converters.js';32} from '../../prompt-converters.js';
3233
33import { readSecret, SECRET_KEYS } from '../secrets.js';34import { readSecret, SECRET_KEYS } from '../secrets.js';
@@ -129,6 +130,8 @@ async function sendClaudeRequest(request, response) {
129 const useTools = request.body.model.startsWith('claude-3') && Array.isArray(request.body.tools) && request.body.tools.length > 0;130 const useTools = request.body.model.startsWith('claude-3') && Array.isArray(request.body.tools) && request.body.tools.length > 0;
130 const useSystemPrompt = (request.body.model.startsWith('claude-2') || request.body.model.startsWith('claude-3')) && request.body.claude_use_sysprompt;131 const useSystemPrompt = (request.body.model.startsWith('claude-2') || request.body.model.startsWith('claude-3')) && request.body.claude_use_sysprompt;
131 const convertedPrompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, useSystemPrompt, useTools, getPromptNames(request));132 const convertedPrompt = convertClaudeMessages(request.body.messages, request.body.assistant_prefill, useSystemPrompt, useTools, getPromptNames(request));
133 const useThinking = request.body.model.startsWith('claude-3-7') && Boolean(request.body.include_reasoning);
134 let voidPrefill = false;
132 // Add custom stop sequences135 // Add custom stop sequences
133 const stopSequences = [];136 const stopSequences = [];
134 if (Array.isArray(request.body.stop)) {137 if (Array.isArray(request.body.stop)) {
@@ -163,9 +166,9 @@ async function sendClaudeRequest(request, response) {
163 .map(tool => tool.function)166 .map(tool => tool.function)
164 .map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters }));167 .map(fn => ({ name: fn.name, description: fn.description, input_schema: fn.parameters }));
165168
166 // Claude doesn't do prefills on function calls, and doesn't allow empty messages169 if (requestBody.tools.length) {
167 if (requestBody.tools.length && convertedPrompt.messages.length && convertedPrompt.messages[convertedPrompt.messages.length - 1].role === 'assistant') {170 // No prefill when using tools
168 convertedPrompt.messages.push({ role: 'user', content: [{ type: 'text', text: '\u200b' }] });171 voidPrefill = true;
169 }172 }
170 if (enableSystemPromptCache && requestBody.tools.length) {173 if (enableSystemPromptCache && requestBody.tools.length) {
171 requestBody.tools[requestBody.tools.length - 1]['cache_control'] = { type: 'ephemeral' };174 requestBody.tools[requestBody.tools.length - 1]['cache_control'] = { type: 'ephemeral' };
@@ -180,6 +183,33 @@ async function sendClaudeRequest(request, response) {
180 betaHeaders.push('prompt-caching-2024-07-31');183 betaHeaders.push('prompt-caching-2024-07-31');
181 }184 }
182185
186 if (useThinking) {
187 // No prefill when thinking
188 voidPrefill = true;
189 const reasoningEffort = request.body.reasoning_effort;
190 const budgetTokens = calculateBudgetTokens(requestBody.max_tokens, reasoningEffort, requestBody.stream);
191 const minThinkTokens = 1024;
192 if (requestBody.max_tokens <= minThinkTokens) {
193 const newValue = requestBody.max_tokens + minThinkTokens;
194 console.warn(color.yellow(`Claude thinking requires a minimum of ${minThinkTokens} response tokens.`));
195 console.info(color.blue(`Increasing response length to ${newValue}.`));
196 requestBody.max_tokens = newValue;
197 }
198 requestBody.thinking = {
199 type: 'enabled',
200 budget_tokens: budgetTokens,
201 };
202
203 // NO I CAN'T SILENTLY IGNORE THE TEMPERATURE.
204 delete requestBody.temperature;
205 delete requestBody.top_p;
206 delete requestBody.top_k;
207 }
208
209 if (voidPrefill && convertedPrompt.messages.length && convertedPrompt.messages[convertedPrompt.messages.length - 1].role === 'assistant') {
210 convertedPrompt.messages.push({ role: 'user', content: [{ type: 'text', text: '\u200b' }] });
211 }
212
183 if (betaHeaders.length) {213 if (betaHeaders.length) {
184 additionalHeaders['anthropic-beta'] = betaHeaders.join(',');214 additionalHeaders['anthropic-beta'] = betaHeaders.join(',');
185 }215 }
src/prompt-converters.js+31 -0
@@ -862,3 +862,34 @@ export function cachingAtDepthForOpenRouterClaude(messages, cachingAtDepth) {
862 }862 }
863 }863 }
864}864}
865
866/**
867 * Calculate the budget tokens for a given reasoning effort.
868 * @param {number} maxTokens Maximum tokens
869 * @param {string} reasoningEffort Reasoning effort
870 * @param {boolean} stream If streaming is enabled
871 * @returns {number} Budget tokens
872 */
873export function calculateBudgetTokens(maxTokens, reasoningEffort, stream) {
874 let budgetTokens = 0;
875
876 switch (reasoningEffort) {
877 case 'low':
878 budgetTokens = Math.floor(maxTokens * 0.1);
879 break;
880 case 'medium':
881 budgetTokens = Math.floor(maxTokens * 0.25);
882 break;
883 case 'high':
884 budgetTokens = Math.floor(maxTokens * 0.5);
885 break;
886 }
887
888 budgetTokens = Math.max(budgetTokens, 1024);
889
890 if (!stream) {
891 budgetTokens = Math.min(budgetTokens, 21333);
892 }
893
894 return budgetTokens;
895}