Claude: map Reasoning Effort to adaptive thinking config (#5219) Supersedes #5105

63fa9c1d0778b171c0650331de1d9f73d3a9cb4e

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

Signed
3 files changed, +40 -7Showing whitespace changes
public/index.html+3 -0
@@ -2182,6 +2182,9 @@
2182 <div class="toggle-description justifyLeft marginBot5" data-i18n="Constrains the verbosity of the model's response.">2182 <div class="toggle-description justifyLeft marginBot5" data-i18n="Constrains the verbosity of the model's response.">
2183 Constrains the verbosity of the model's response.2183 Constrains the verbosity of the model's response.
2184 </div>2184 </div>
2185 <strong class="toggle-description justifyLeft marginBot5" data-source="claude">
2186 On Opus 4.6 / Sonnet 4.6, a non-automatic Reasoning Effort takes precedence over Verbosity.
2187 </strong>
2185 </div>2188 </div>
2186 </div>2189 </div>
2187 <div class="range-block" data-source="claude">2190 <div class="range-block" data-source="claude">
src/endpoints/backends/chat-completions.js+14 -5
@@ -228,6 +228,7 @@ async function sendClaudeRequest(request, response) {
228 const isLimitedSampling = /^claude-(opus-4-1|sonnet-4-5|haiku-4-5|opus-4-5|opus-4-6|sonnet-4-6)/.test(request.body.model);228 const isLimitedSampling = /^claude-(opus-4-1|sonnet-4-5|haiku-4-5|opus-4-5|opus-4-6|sonnet-4-6)/.test(request.body.model);
229 const useVerbosity = /^claude-(opus-4-5|opus-4-6|sonnet-4-6)/.test(request.body.model);229 const useVerbosity = /^claude-(opus-4-5|opus-4-6|sonnet-4-6)/.test(request.body.model);
230 const noPrefillModel = /^claude-(opus-4-6|sonnet-4-6)/.test(request.body.model);230 const noPrefillModel = /^claude-(opus-4-6|sonnet-4-6)/.test(request.body.model);
231 const isAdaptiveModel = /^claude-(opus-4-6|sonnet-4-6)/.test(request.body.model);
231 let fixThinkingPrefill = false;232 let fixThinkingPrefill = false;
232 // Add custom stop sequences233 // Add custom stop sequences
233 const stopSequences = [];234 const stopSequences = [];
@@ -305,10 +306,18 @@ async function sendClaudeRequest(request, response) {
305 }306 }
306307
307 const reasoningEffort = request.body.reasoning_effort;308 const reasoningEffort = request.body.reasoning_effort;
308 const budgetTokens = calculateClaudeBudgetTokens(requestBody.max_tokens, reasoningEffort, requestBody.stream);309 const budgetTokens = calculateClaudeBudgetTokens(requestBody.max_tokens, reasoningEffort, requestBody.stream, isAdaptiveModel);
309310
310 if (useThinking && Number.isInteger(budgetTokens)) {311 // Adaptive thinking: returns a string effort level (like Gemini 3)
311 // No prefill when thinking312 if (useThinking && typeof budgetTokens === 'string') {
313 fixThinkingPrefill = true;
314 requestBody.thinking = { type: 'adaptive' };
315 requestBody.output_config ??= {};
316 requestBody.output_config.effort = budgetTokens;
317 // top_k is not allowed in adaptive mode
318 delete requestBody.top_k;
319 } else if (useThinking && Number.isInteger(budgetTokens)) {
320 // Traditional thinking: returns a numeric budget
312 fixThinkingPrefill = true;321 fixThinkingPrefill = true;
313 const minThinkTokens = 1024;322 const minThinkTokens = 1024;
314 if (requestBody.max_tokens <= minThinkTokens) {323 if (requestBody.max_tokens <= minThinkTokens) {
@@ -332,8 +341,8 @@ async function sendClaudeRequest(request, response) {
332 convertedPrompt.messages[convertedPrompt.messages.length - 1].role = 'user';341 convertedPrompt.messages[convertedPrompt.messages.length - 1].role = 'user';
333 }342 }
334343
335 // Verbosity = 'effort' (same values as OpenAI)344 // Verbosity = 'effort' (same values as OpenAI) - only if not already set by adaptive thinking
336 if (useVerbosity && request.body.verbosity) {345 if (useVerbosity && request.body.verbosity && !requestBody.output_config?.effort) {
337 betaHeaders.push('effort-2025-11-24');346 betaHeaders.push('effort-2025-11-24');
338 requestBody.output_config ??= {};347 requestBody.output_config ??= {};
339 requestBody.output_config.effort = request.body.verbosity;348 requestBody.output_config.effort = request.body.verbosity;
src/prompt-converters.js+23 -2
@@ -1110,12 +1110,33 @@ export function cachingSystemPromptForOpenRouter(messages, ttl = undefined) {
11101110
1111/**1111/**
1112 * Calculate the Claude budget tokens for a given reasoning effort.1112 * Calculate the Claude budget tokens for a given reasoning effort.
1113 * Returns a string effort level for adaptive thinking (Opus 4.6+), a number for traditional thinking, or null for auto.
1113 * @param {number} maxTokens Maximum tokens1114 * @param {number} maxTokens Maximum tokens
1114 * @param {string} reasoningEffort Reasoning effort1115 * @param {string} reasoningEffort Reasoning effort
1115 * @param {boolean} stream If streaming is enabled1116 * @param {boolean} stream If streaming is enabled
1116 * @returns {number?} Budget tokens1117 * @param {boolean} isAdaptiveModel If the model supports adaptive thinking (Opus 4.6+)
1118 * @returns {number|string|null} Budget tokens, effort string, or null
1117 */1119 */
1118export function calculateClaudeBudgetTokens(maxTokens, reasoningEffort, stream) {1120export function calculateClaudeBudgetTokens(maxTokens, reasoningEffort, stream, isAdaptiveModel) {
1121 // Adaptive thinking for Opus 4.6+: return effort string (like Gemini 3)
1122 if (isAdaptiveModel) {
1123 switch (reasoningEffort) {
1124 case REASONING_EFFORT.auto:
1125 return null;
1126 case REASONING_EFFORT.min:
1127 return 'low';
1128 case REASONING_EFFORT.low:
1129 return 'low';
1130 case REASONING_EFFORT.medium:
1131 return 'medium';
1132 case REASONING_EFFORT.high:
1133 return 'high';
1134 case REASONING_EFFORT.max:
1135 return 'max';
1136 }
1137 return null;
1138 }
1139
1119 let budgetTokens = 0;1140 let budgetTokens = 0;
11201141
1121 switch (reasoningEffort) {1142 switch (reasoningEffort) {