Merge branch 'staging' into vision-cleanup
| @@ -2048,16 +2048,20 @@ | ||
| 2048 | 2048 | </span> |
| 2049 | 2049 | </div> |
| 2050 | 2050 | </div> |
| 2051 | 2051 | <div class="flex-container flexFlowColumn wide100p textAlignCenter marginTop10" data-source="openai,custom,claude,xai,makersuite,openrouter"> |
| 2052 | 2052 | <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."> |
| 2053 | 2053 | <label for="openai_reasoning_effort"> |
| 2054 | 2054 | <span data-i18n="Reasoning Effort">Reasoning Effort</span> |
| 2055 | 2055 | <i data-source="claudeopenai,custom,xai,openrouter" class="opacity50p fa-solid fa-circle-info" title="AllocatesOpenAI-style aoptions: portionlow, ofmedium, thehigh. responseMinimum lengthand formaximum thinkingare (low:aliased 10%,to medium:low 25%,and high:. 50%),Auto butdoes minimumnot 1024send tokensan effort level."></i> |
| 2056 | + <i data-source="claude,makersuite" class="opacity50p fa-solid fa-circle-info" title="Allocates a portion of the response length for thinking (low: 10%, medium: 25%, high: 50%). Other options are model-dependent."></i> | |
| 2056 | 2057 | </label> |
| 2057 | 2058 | <select id="openai_reasoning_effort"> |
| 2059 | + <option data-i18n="openai_reasoning_effort_auto" value="auto">Auto</option> | |
| 2060 | + <option data-i18n="openai_reasoning_effort_minimum" value="min">Mininum</option> | |
| 2058 | 2061 | <option data-i18n="openai_reasoning_effort_low" value="low">Low</option> |
| 2059 | 2062 | <option data-i18n="openai_reasoning_effort_medium" value="medium">Medium</option> |
| 2060 | 2063 | <option data-i18n="openai_reasoning_effort_high" value="high">High</option> |
| 2064 | + <option data-i18n="openai_reasoning_effort_maximum" value="max">Maximum</option> | |
| 2061 | 2065 | </select> |
| 2062 | 2066 | </div> |
| 2063 | 2067 | </div> |
| @@ -6955,15 +6955,23 @@ export async function saveChat({ chatName, withMetadata, mesId, force = false } | ||
| 6955 | 6955 | throw new Error(result.statusText); |
| 6956 | 6956 | } |
| 6957 | 6957 | |
| 6958 | 6958 | const forceSaveConfirmedpopupResult = await Popup.show.confirminput( |
| 6959 | 6959 | t`ERROR: Chat integrity check failed while saving the file.`, |
| 6960 | - t`Continuing the operation may result in data loss. Would you like to overwrite the chat file anyway? Pressing "NO" will cancel the save operation.`, | |
| 6960 | + t`<p>After you click OK, the page will be reloaded to prevent data corruption.</p> | |
| 6961 | - { okButton: t`Yes, overwrite`, cancelButton: t`No, cancel` }, | |
| 6961 | + <p>To confirm an overwrite (and potentially <b>LOSE YOUR DATA</b>), enter <code>OVERWRITE</code> (in all caps) in the box below before clicking OK.</p>`, | |
| 6962 | - ) === POPUP_RESULT.AFFIRMATIVE; | |
| 6962 | + '', | |
| 6963 | + { okButton: 'OK', cancelButton: false }, | |
| 6964 | + ); | |
| 6963 | 6965 | |
| 6964 | - if (forceSaveConfirmed) { | |
| 6966 | + const forceSaveConfirmed = popupResult === 'OVERWRITE'; | |
| 6965 | - await saveChat({ chatName, withMetadata, mesId, force: true }); | |
| 6967 | + | |
| 6968 | + if (!forceSaveConfirmed) { | |
| 6969 | + console.warn('Chat integrity check failed, and user did not confirm the overwrite. Reloading the page.'); | |
| 6970 | + window.location.reload(); | |
| 6971 | + return; | |
| 6966 | 6972 | } |
| 6973 | + | |
| 6974 | + await saveChat({ chatName, withMetadata, mesId, force: true }); | |
| 6967 | 6975 | } catch (error) { |
| 6968 | 6976 | console.error(error); |
| 6969 | 6977 | toastr.error(t`Check the server connection and reload the page to prevent data loss.`, t`Chat could not be saved`); |
| @@ -216,6 +216,15 @@ const openrouter_middleout_types = { | ||
| 216 | 216 | OFF: 'off', |
| 217 | 217 | }; |
| 218 | 218 | |
| 219 | +export const reasoning_effort_types = { | |
| 220 | + auto: 'auto', | |
| 221 | + low: 'low', | |
| 222 | + medium: 'medium', | |
| 223 | + high: 'high', | |
| 224 | + min: 'min', | |
| 225 | + max: 'max', | |
| 226 | +}; | |
| 227 | + | |
| 219 | 228 | const sensitiveFields = [ |
| 220 | 229 | 'reverse_proxy', |
| 221 | 230 | 'proxy_password', |
| @@ -382,7 +391,7 @@ const default_settings = { | ||
| 382 | 391 | continue_postfix: continue_postfix_types.SPACE, |
| 383 | 392 | custom_prompt_post_processing: custom_prompt_post_processing_types.NONE, |
| 384 | 393 | show_thoughts: true, |
| 385 | 394 | reasoning_effort: 'medium'reasoning_effort_types.auto, |
| 386 | 395 | enable_web_search: false, |
| 387 | 396 | request_images: false, |
| 388 | 397 | seed: -1, |
| @@ -463,7 +472,7 @@ const oai_settings = { | ||
| 463 | 472 | continue_postfix: continue_postfix_types.SPACE, |
| 464 | 473 | custom_prompt_post_processing: custom_prompt_post_processing_types.NONE, |
| 465 | 474 | show_thoughts: true, |
| 466 | 475 | reasoning_effort: 'medium'reasoning_effort_types.auto, |
| 467 | 476 | enable_web_search: false, |
| 468 | 477 | request_images: false, |
| 469 | 478 | seed: -1, |
| @@ -1937,6 +1946,31 @@ async function sendAltScaleRequest(messages, logit_bias, signal, type) { | ||
| 1937 | 1946 | return data.output; |
| 1938 | 1947 | } |
| 1939 | 1948 | |
| 1949 | +function getReasoningEffort() { | |
| 1950 | + // These sources expect the effort as string. | |
| 1951 | + const reasoningEffortSources = [ | |
| 1952 | + chat_completion_sources.OPENAI, | |
| 1953 | + chat_completion_sources.CUSTOM, | |
| 1954 | + chat_completion_sources.XAI, | |
| 1955 | + chat_completion_sources.OPENROUTER, | |
| 1956 | + ]; | |
| 1957 | + | |
| 1958 | + if (!reasoningEffortSources.includes(oai_settings.chat_completion_source)) { | |
| 1959 | + return oai_settings.reasoning_effort; | |
| 1960 | + } | |
| 1961 | + | |
| 1962 | + switch (oai_settings.reasoning_effort) { | |
| 1963 | + case reasoning_effort_types.auto: | |
| 1964 | + return undefined; | |
| 1965 | + case reasoning_effort_types.min: | |
| 1966 | + return reasoning_effort_types.low; | |
| 1967 | + case reasoning_effort_types.max: | |
| 1968 | + return reasoning_effort_types.high; | |
| 1969 | + default: | |
| 1970 | + return oai_settings.reasoning_effort; | |
| 1971 | + } | |
| 1972 | +} | |
| 1973 | + | |
| 1940 | 1974 | /** |
| 1941 | 1975 | * Send a chat completion request to backend |
| 1942 | 1976 | * @param {string} type (impersonate, quiet, continue, etc) |
| @@ -2023,7 +2057,7 @@ async function sendOpenAIRequest(type, messages, signal) { | ||
| 2023 | 2057 | 'char_name': name2, |
| 2024 | 2058 | 'group_names': getGroupNames(), |
| 2025 | 2059 | 'include_reasoning': Boolean(oai_settings.show_thoughts), |
| 2026 | 2060 | 'reasoning_effort': StringgetReasoningEffort(oai_settings.reasoning_effort), |
| 2027 | 2061 | 'enable_web_search': Boolean(oai_settings.enable_web_search), |
| 2028 | 2062 | 'request_images': Boolean(oai_settings.request_images), |
| 2029 | 2063 | 'custom_prompt_post_processing': oai_settings.custom_prompt_post_processing, |
| @@ -28,7 +28,8 @@ import { | ||
| 28 | 28 | cachingAtDepthForOpenRouterClaude, |
| 29 | 29 | cachingAtDepthForClaude, |
| 30 | 30 | getPromptNames, |
| 31 | 31 | calculateBudgetTokenscalculateClaudeBudgetTokens, |
| 32 | + calculateGoogleBudgetTokens, | |
| 32 | 33 | } from '../../prompt-converters.js'; |
| 33 | 34 | |
| 34 | 35 | import { readSecret, SECRET_KEYS } from '../secrets.js'; |
| @@ -202,7 +203,7 @@ async function sendClaudeRequest(request, response) { | ||
| 202 | 203 | // No prefill when thinking |
| 203 | 204 | voidPrefill = true; |
| 204 | 205 | const reasoningEffort = request.body.reasoning_effort; |
| 205 | 206 | const budgetTokens = calculateBudgetTokenscalculateClaudeBudgetTokens(requestBody.max_tokens, reasoningEffort, requestBody.stream); |
| 206 | 207 | const minThinkTokens = 1024; |
| 207 | 208 | if (requestBody.max_tokens <= minThinkTokens) { |
| 208 | 209 | const newValue = requestBody.max_tokens + minThinkTokens; |
| @@ -340,6 +341,7 @@ async function sendMakerSuiteRequest(request, response) { | ||
| 340 | 341 | const stream = Boolean(request.body.stream); |
| 341 | 342 | const enableWebSearch = Boolean(request.body.enable_web_search); |
| 342 | 343 | const requestImages = Boolean(request.body.request_images); |
| 344 | + const reasoningEffort = String(request.body.reasoning_effort); | |
| 343 | 345 | const isThinking = model.includes('thinking'); |
| 344 | 346 | const isGemma = model.includes('gemma'); |
| 345 | 347 | |
| @@ -412,13 +414,26 @@ async function sendMakerSuiteRequest(request, response) { | ||
| 412 | 414 | tools.push({ function_declarations: functionDeclarations }); |
| 413 | 415 | } |
| 414 | 416 | |
| 417 | + // One more models list to maintain, yay | |
| 418 | + const thinkingBudgetModels = [ | |
| 419 | + 'gemini-2.5-flash-preview-04-17', | |
| 420 | + ]; | |
| 421 | + | |
| 422 | + if (thinkingBudgetModels.includes(model)) { | |
| 423 | + const thinkingBudget = calculateGoogleBudgetTokens(generationConfig.maxOutputTokens, reasoningEffort); | |
| 424 | + | |
| 425 | + if (Number.isInteger(thinkingBudget)) { | |
| 426 | + generationConfig.thinkingConfig = { thinkingBudget: thinkingBudget }; | |
| 427 | + } | |
| 428 | + } | |
| 429 | + | |
| 415 | 430 | let body = { |
| 416 | 431 | contents: prompt.contents, |
| 417 | 432 | safetySettings: safetySettings, |
| 418 | 433 | generationConfig: generationConfig, |
| 419 | 434 | }; |
| 420 | 435 | |
| 421 | - if (useSystemPrompt) { | |
| 436 | + if (useSystemPrompt && Array.isArray(prompt.system_instruction.parts) && prompt.system_instruction.parts.length) { | |
| 422 | 437 | body.systemInstruction = prompt.system_instruction; |
| 423 | 438 | } |
| 424 | 439 | |
| @@ -868,7 +883,7 @@ async function sendXaiRequest(request, response) { | ||
| 868 | 883 | bodyParams['stop'] = request.body.stop; |
| 869 | 884 | } |
| 870 | 885 | |
| 871 | 886 | if (request.body.reasoning_effort && ['grok-3-mini-beta', 'grok-3-mini-fast-beta'].includes(request.body.model)) { |
| 872 | 887 | bodyParams['reasoning_effort'] = request.body.reasoning_effort === 'high' ? 'high' : 'low'; |
| 873 | 888 | } |
| 874 | 889 | |
| @@ -1210,6 +1225,10 @@ router.post('/generate', function (request, response) { | ||
| 1210 | 1225 | bodyParams['route'] = 'fallback'; |
| 1211 | 1226 | } |
| 1212 | 1227 | |
| 1228 | + if (request.body.reasoning_effort) { | |
| 1229 | + bodyParams['reasoning'] = { effort: request.body.reasoning_effort }; | |
| 1230 | + } | |
| 1231 | + | |
| 1213 | 1232 | let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1, 'number'); |
| 1214 | 1233 | if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) { |
| 1215 | 1234 | cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth); |
| @@ -1258,7 +1277,7 @@ router.post('/generate', function (request, response) { | ||
| 1258 | 1277 | } |
| 1259 | 1278 | |
| 1260 | 1279 | // A few of OpenAIs reasoning models support reasoning effort |
| 1261 | 1280 | if (request.body.reasoning_effort && [CHAT_COMPLETION_SOURCES.CUSTOM, CHAT_COMPLETION_SOURCES.OPENAI].includes(request.body.chat_completion_source)) { |
| 1262 | 1281 | if (['o1', 'o3-mini', 'o3-mini-2025-01-31', 'o4-mini', 'o4-mini-2025-04-16', 'o3', 'o3-2025-04-16'].includes(request.body.model)) { |
| 1263 | 1282 | bodyParams['reasoning_effort'] = request.body.reasoning_effort; |
| 1264 | 1283 | } |
| @@ -3,6 +3,15 @@ import { getConfigValue, tryParse } from './util.js'; | ||
| 3 | 3 | |
| 4 | 4 | const PROMPT_PLACEHOLDER = getConfigValue('promptPlaceholder', 'Let\'s get started.'); |
| 5 | 5 | |
| 6 | +const REASONING_EFFORT = { | |
| 7 | + auto: 'auto', | |
| 8 | + low: 'low', | |
| 9 | + medium: 'medium', | |
| 10 | + high: 'high', | |
| 11 | + min: 'min', | |
| 12 | + max: 'max', | |
| 13 | +}; | |
| 14 | + | |
| 6 | 15 | /** |
| 7 | 16 | * @typedef {object} PromptNames |
| 8 | 17 | * @property {string} charName Character name |
| @@ -356,7 +365,7 @@ export function convertCohereMessages(messages, names) { | ||
| 356 | 365 | * @param {string} model Model name |
| 357 | 366 | * @param {boolean} useSysPrompt Use system prompt |
| 358 | 367 | * @param {PromptNames} names Prompt names |
| 359 | 368 | * @returns {{contents: *[], system_instruction: {parts: {text: string}[]}}} Prompt for Google MakerSuite models |
| 360 | 369 | */ |
| 361 | 370 | export function convertGooglePrompt(messages, model, useSysPrompt, names) { |
| 362 | 371 | const visionSupportedModelPrefix = [ |
| @@ -369,8 +378,8 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) { | ||
| 369 | 378 | ]; |
| 370 | 379 | |
| 371 | 380 | const isMultimodal = visionSupportedModelPrefix.some(prefix => model.startsWith(prefix)); |
| 381 | + const sysPrompt = []; | |
| 372 | 382 | |
| 373 | - let sys_prompt = ''; | |
| 374 | 383 | if (useSysPrompt) { |
| 375 | 384 | while (messages.length > 1 && messages[0].role === 'system') { |
| 376 | 385 | // Append example names if not already done by the frontend (e.g. for group chats). |
| @@ -384,12 +393,12 @@ export function convertGooglePrompt(messages, model, useSysPrompt, names) { | ||
| 384 | 393 | messages[0].content = `${names.charName}: ${messages[0].content}`; |
| 385 | 394 | } |
| 386 | 395 | } |
| 387 | - sys_prompt += `${messages[0].content}\n\n`; | |
| 396 | + sysPrompt.push(messages[0].content); | |
| 388 | 397 | messages.shift(); |
| 389 | 398 | } |
| 390 | 399 | } |
| 391 | 400 | |
| 392 | 401 | const system_instruction = { parts: [{ sysPrompt.map(text: sys_prompt.trim=> (){ text }])) }; |
| 393 | 402 | const toolNameMap = {}; |
| 394 | 403 | |
| 395 | 404 | const contents = []; |
| @@ -919,25 +928,32 @@ export function cachingAtDepthForOpenRouterClaude(messages, cachingAtDepth) { | ||
| 919 | 928 | } |
| 920 | 929 | |
| 921 | 930 | /** |
| 922 | 931 | * Calculate the Claude budget tokens for a given reasoning effort. |
| 923 | 932 | * @param {number} maxTokens Maximum tokens |
| 924 | 933 | * @param {string} reasoningEffort Reasoning effort |
| 925 | 934 | * @param {boolean} stream If streaming is enabled |
| 926 | 935 | * @returns {number} Budget tokens |
| 927 | 936 | */ |
| 928 | 937 | export function calculateBudgetTokenscalculateClaudeBudgetTokens(maxTokens, reasoningEffort, stream) { |
| 929 | 938 | let budgetTokens = 0; |
| 930 | 939 | |
| 931 | 940 | switch (reasoningEffort) { |
| 932 | 941 | case 'low'REASONING_EFFORT.min: |
| 942 | + budgetTokens = 1024; | |
| 943 | + break; | |
| 944 | + case REASONING_EFFORT.low: | |
| 933 | 945 | budgetTokens = Math.floor(maxTokens * 0.1); |
| 934 | 946 | break; |
| 935 | 947 | case 'medium'REASONING_EFFORT.auto: |
| 948 | + case REASONING_EFFORT.medium: | |
| 936 | 949 | budgetTokens = Math.floor(maxTokens * 0.25); |
| 937 | 950 | break; |
| 938 | 951 | case 'REASONING_EFFORT.high': |
| 939 | 952 | budgetTokens = Math.floor(maxTokens * 0.5); |
| 940 | 953 | break; |
| 954 | + case REASONING_EFFORT.max: | |
| 955 | + budgetTokens = Math.floor(maxTokens * 0.95); | |
| 956 | + break; | |
| 941 | 957 | } |
| 942 | 958 | |
| 943 | 959 | budgetTokens = Math.max(budgetTokens, 1024); |
| @@ -948,3 +964,37 @@ export function calculateBudgetTokens(maxTokens, reasoningEffort, stream) { | ||
| 948 | 964 | |
| 949 | 965 | return budgetTokens; |
| 950 | 966 | } |
| 967 | + | |
| 968 | +/** | |
| 969 | + * Calculate the Google budget tokens for a given reasoning effort. | |
| 970 | + * @param {number} maxTokens Maximum tokens | |
| 971 | + * @param {string} reasoningEffort Reasoning effort | |
| 972 | + * @returns {number?} Budget tokens | |
| 973 | + */ | |
| 974 | +export function calculateGoogleBudgetTokens(maxTokens, reasoningEffort) { | |
| 975 | + let budgetTokens = 0; | |
| 976 | + | |
| 977 | + switch (reasoningEffort) { | |
| 978 | + case REASONING_EFFORT.auto: | |
| 979 | + return null; | |
| 980 | + case REASONING_EFFORT.min: | |
| 981 | + budgetTokens = 0; | |
| 982 | + break; | |
| 983 | + case REASONING_EFFORT.low: | |
| 984 | + budgetTokens = Math.floor(maxTokens * 0.1); | |
| 985 | + break; | |
| 986 | + case REASONING_EFFORT.medium: | |
| 987 | + budgetTokens = Math.floor(maxTokens * 0.25); | |
| 988 | + break; | |
| 989 | + case REASONING_EFFORT.high: | |
| 990 | + budgetTokens = Math.floor(maxTokens * 0.5); | |
| 991 | + break; | |
| 992 | + case REASONING_EFFORT.max: | |
| 993 | + budgetTokens = maxTokens; | |
| 994 | + break; | |
| 995 | + } | |
| 996 | + | |
| 997 | + budgetTokens = Math.min(budgetTokens, 24576); | |
| 998 | + | |
| 999 | + return budgetTokens; | |
| 1000 | +} | |