Claude 3.7 think mode

b8ebed0f4c4d19ca3f037db03261d90cdb28b6cf

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

6 files changed, +75 -8Ignore whitespace
public/index.html+5 -4
@@ -2000,7 +2000,7 @@
20002000 </span>
20012001 </div>
20022002 </div>
20032003 <div class="range-block" data-source="deepseek,openrouter,custom,claude">
20042004 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
20052005 <input id="openai_show_thoughts" type="checkbox" />
20062006 <span>
@@ -2014,10 +2014,11 @@
20142014 </span>
20152015 </div>
20162016 </div>
20172017 <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude">
20182018 <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.">
20192019 <label for="openai_reasoning_effort" data-i18n="Reasoning Effort">
2020- Reasoning Effort
2020+ <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%)."></i>
20212022 </label>
20222023 <select id="openai_reasoning_effort">
20232024 <option data-i18n="openai_reasoning_effort_low" value="low">Low</option>
public/script.js+1 -1
@@ -5725,7 +5725,7 @@ function extractMessageFromData(data) {
57255725 case 'novel':
57265726 return data.output;
57275727 case 'openai':
57285728 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 ?? '';
57295729 default:
57305730 return '';
57315731 }
public/scripts/openai.js+3 -0
@@ -2149,6 +2149,9 @@ async function sendOpenAIRequest(type, messages, signal) {
21492149 */
21502150function getStreamingReply(data, state) {
21512151 if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) {
2152+ if (oai_settings.show_thoughts) {
2153+ state.reasoning += data?.delta?.thinking || '';
2154+ }
21522155 return data?.delta?.text || '';
21532156 } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) {
21542157 if (oai_settings.show_thoughts) {
public/scripts/reasoning.js+2 -0
@@ -76,6 +76,8 @@ export function extractReasoningFromData(data) {
7676 return data?.choices?.[0]?.message?.reasoning ?? '';
7777 case chat_completion_sources.MAKERSUITE:
7878 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 ?? '';
7981 case chat_completion_sources.CUSTOM: {
8082 return data?.choices?.[0]?.message?.reasoning_content
8183 ?? data?.choices?.[0]?.message?.reasoning
src/endpoints/backends/chat-completions.js+33 -3
@@ -28,6 +28,7 @@ import {
2828 cachingAtDepthForOpenRouterClaude,
2929 cachingAtDepthForClaude,
3030 getPromptNames,
31+ calculateBudgetTokens,
3132} from '../../prompt-converters.js';
3233
3334import { readSecret, SECRET_KEYS } from '../secrets.js';
@@ -129,6 +130,8 @@ async function sendClaudeRequest(request, response) {
129130 const useTools = request.body.model.startsWith('claude-3') && Array.isArray(request.body.tools) && request.body.tools.length > 0;
130131 const useSystemPrompt = (request.body.model.startsWith('claude-2') || request.body.model.startsWith('claude-3')) && request.body.claude_use_sysprompt;
131132 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;
132135 // Add custom stop sequences
133136 const stopSequences = [];
134137 if (Array.isArray(request.body.stop)) {
@@ -163,9 +166,9 @@ async function sendClaudeRequest(request, response) {
163166 .map(tool => tool.function)
164167 .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 messages
169+ 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;
169172 }
170173 if (enableSystemPromptCache && requestBody.tools.length) {
171174 requestBody.tools[requestBody.tools.length - 1]['cache_control'] = { type: 'ephemeral' };
@@ -180,6 +183,33 @@ async function sendClaudeRequest(request, response) {
180183 betaHeaders.push('prompt-caching-2024-07-31');
181184 }
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+
183213 if (betaHeaders.length) {
184214 additionalHeaders['anthropic-beta'] = betaHeaders.join(',');
185215 }
src/prompt-converters.js+31 -0
@@ -862,3 +862,34 @@ export function cachingAtDepthForOpenRouterClaude(messages, cachingAtDepth) {
862862 }
863863 }
864864}
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+ */
873+export 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+}