Claude 3.7 think mode
| @@ -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. Currently supported values are low, medium, and high. 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. Currently supported values are low, medium, and high. 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 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> | ||
| 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> |
| @@ -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 | } |
| @@ -2149,6 +2149,9 @@ async function sendOpenAIRequest(type, messages, signal) { | |||
| 2149 | */ | 2149 | */ |
| 2150 | function getStreamingReply(data, state) { | 2150 | function 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) { |
| @@ -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_content | 82 | return data?.choices?.[0]?.message?.reasoning_content |
| 81 | ?? data?.choices?.[0]?.message?.reasoning | 83 | ?? data?.choices?.[0]?.message?.reasoning |
| @@ -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'; |
| 32 | 33 | ||
| 33 | import { readSecret, SECRET_KEYS } from '../secrets.js'; | 34 | import { 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 sequences | 135 | // 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 })); |
| 165 | 168 | ||
| 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; |
| 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 | } |
| 182 | 185 | ||
| 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 | } |
| @@ -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 | */ | ||
| 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 | } | ||