OpenRouter: Support reasoning blocks

03c98fb55ac95fb051ef98b48e6e787d54ff9eb0

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

7 files changed, +36 -30Showing whitespace changes
public/index.html+1 -1
@@ -1977,7 +1977,7 @@
1977 </span>1977 </span>
1978 </div>1978 </div>
1979 </div>1979 </div>
1980 <div class="range-block" data-source="makersuite,deepseek">1980 <div class="range-block" data-source="makersuite,deepseek,openrouter">
1981 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">1981 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
1982 <input id="openai_show_thoughts" type="checkbox" />1982 <input id="openai_show_thoughts" type="checkbox" />
1983 <span>1983 <span>
public/script.js+9 -12
@@ -170,7 +170,7 @@ import {
170 isElementInViewport,170 isElementInViewport,
171 copyText,171 copyText,
172} from './scripts/utils.js';172} from './scripts/utils.js';
173import { debounce_timeout, THINK_BREAK } from './scripts/constants.js';173import { debounce_timeout } from './scripts/constants.js';
174174
175import { doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js';175import { doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js';
176import { COMMENT_NAME_DEFAULT, executeSlashCommandsOnChatInput, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, stopScriptExecution } from './scripts/slash-commands.js';176import { COMMENT_NAME_DEFAULT, executeSlashCommandsOnChatInput, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, stopScriptExecution } from './scripts/slash-commands.js';
@@ -5700,10 +5700,6 @@ function getTextContextFromData(data) {
5700function extractMessageFromData(data){5700function extractMessageFromData(data){
5701 const content = String(getTextContextFromData(data) ?? '');5701 const content = String(getTextContextFromData(data) ?? '');
57025702
5703 if (content.includes(THINK_BREAK)) {
5704 return content.split(THINK_BREAK)[1];
5705 }
5706
5707 return content;5703 return content;
5708}5704}
57095705
@@ -5713,14 +5709,15 @@ function extractMessageFromData(data){
5713 * @returns {string} Extracted reasoning5709 * @returns {string} Extracted reasoning
5714 */5710 */
5715function extractReasoningFromData(data) {5711function extractReasoningFromData(data) {
5716 const content = String(getTextContextFromData(data) ?? '');5712 if (main_api === 'openai' && oai_settings.show_thoughts) {
57175713 switch (oai_settings.chat_completion_source) {
5718 if (content.includes(THINK_BREAK)) {5714 case chat_completion_sources.DEEPSEEK:
5719 return content.split(THINK_BREAK)[0];
5720 }
5721
5722 if (main_api === 'openai' && oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK && oai_settings.show_thoughts) {
5723 return data?.choices?.[0]?.message?.reasoning_content ?? '';5715 return data?.choices?.[0]?.message?.reasoning_content ?? '';
5716 case chat_completion_sources.OPENROUTER:
5717 return data?.choices?.[0]?.message?.reasoning ?? '';
5718 case chat_completion_sources.MAKERSUITE:
5719 return data?.responseContent?.parts?.filter(part => part.thought)?.map(part => part.text)?.join('\n\n') ?? '';
5720 }
5724 }5721 }
57255722
5726 return '';5723 return '';
public/scripts/constants.js+0 -5
@@ -14,8 +14,3 @@ export const debounce_timeout = {
14 /** [5 sec] For delayed tasks, like auto-saving or completing batch operations that need a significant pause. */14 /** [5 sec] For delayed tasks, like auto-saving or completing batch operations that need a significant pause. */
15 extended: 5000,15 extended: 5000,
16};16};
17
18/**
19 * Custom boundary for splitting the text between the model's reasoning and the actual response.
20 */
21export const THINK_BREAK = '##�THINK_BREAK�##';
public/scripts/openai.js+5 -0
@@ -2161,6 +2161,11 @@ function getStreamingReply(data, state) {
2161 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');2161 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
2162 }2162 }
2163 return data.choices?.[0]?.delta?.content || '';2163 return data.choices?.[0]?.delta?.content || '';
2164 } else if (oai_settings.chat_completion_source === chat_completion_sources.OPENROUTER) {
2165 if (oai_settings.show_thoughts) {
2166 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || '');
2167 }
2168 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
2164 } else {2169 } else {
2165 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';2170 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
2166 }2171 }
public/scripts/sse-stream.js+15 -0
@@ -235,6 +235,21 @@ async function* parseStreamData(json) {
235 }235 }
236 return;236 return;
237 }237 }
238 else if (typeof json.choices[0].delta.reasoning === 'string' && json.choices[0].delta.reasoning.length > 0) {
239 for (let j = 0; j < json.choices[0].delta.reasoning.length; j++) {
240 const str = json.choices[0].delta.reasoning[j];
241 const isLastSymbol = j === json.choices[0].delta.reasoning.length - 1;
242 const choiceClone = structuredClone(json.choices[0]);
243 choiceClone.delta.reasoning = str;
244 choiceClone.delta.content = isLastSymbol ? choiceClone.delta.content : '';
245 const choices = [choiceClone];
246 yield {
247 data: { ...json, choices },
248 chunk: str,
249 };
250 }
251 return;
252 }
238 else if (typeof json.choices[0].delta.content === 'string' && json.choices[0].delta.content.length > 0) {253 else if (typeof json.choices[0].delta.content === 'string' && json.choices[0].delta.content.length > 0) {
239 for (let j = 0; j < json.choices[0].delta.content.length; j++) {254 for (let j = 0; j < json.choices[0].delta.content.length; j++) {
240 const str = json.choices[0].delta.content[j];255 const str = json.choices[0].delta.content[j];
src/constants.js+0 -5
@@ -413,8 +413,3 @@ export const VLLM_KEYS = [
413 'guided_decoding_backend',413 'guided_decoding_backend',
414 'guided_whitespace_pattern',414 'guided_whitespace_pattern',
415];415];
416
417/**
418 * Custom boundary for splitting the text between the model's reasoning and the actual response.
419 */
420export const THINK_BREAK = '##�THINK_BREAK�##';
src/endpoints/backends/chat-completions.js+6 -7
@@ -7,7 +7,6 @@ import {
7 CHAT_COMPLETION_SOURCES,7 CHAT_COMPLETION_SOURCES,
8 GEMINI_SAFETY,8 GEMINI_SAFETY,
9 OPENROUTER_HEADERS,9 OPENROUTER_HEADERS,
10 THINK_BREAK,
11} from '../../constants.js';10} from '../../constants.js';
12import {11import {
13 forwardFetchResponse,12 forwardFetchResponse,
@@ -392,11 +391,7 @@ async function sendMakerSuiteRequest(request, response) {
392 const responseContent = candidates[0].content ?? candidates[0].output;391 const responseContent = candidates[0].content ?? candidates[0].output;
393 console.log('Google AI Studio response:', responseContent);392 console.log('Google AI Studio response:', responseContent);
394393
395 if (Array.isArray(responseContent?.parts) && isThinking && !showThoughts) {394 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.filter(part => !part.thought)?.map(part => part.text)?.join('\n\n');
396 responseContent.parts = responseContent.parts.filter(part => !part.thought);
397 }
398
399 const responseText = typeof responseContent === 'string' ? responseContent : responseContent?.parts?.map(part => part.text)?.join(THINK_BREAK);
400 if (!responseText) {395 if (!responseText) {
401 let message = 'Google AI Studio Candidate text empty';396 let message = 'Google AI Studio Candidate text empty';
402 console.log(message, generateResponseJson);397 console.log(message, generateResponseJson);
@@ -404,7 +399,7 @@ async function sendMakerSuiteRequest(request, response) {
404 }399 }
405400
406 // Wrap it back to OAI format401 // Wrap it back to OAI format
407 const reply = { choices: [{ 'message': { 'content': responseText } }] };402 const reply = { choices: [{ 'message': { 'content': responseText } }], responseContent };
408 return response.send(reply);403 return response.send(reply);
409 }404 }
410 } catch (error) {405 } catch (error) {
@@ -993,6 +988,10 @@ router.post('/generate', jsonParser, function (request, response) {
993 bodyParams['route'] = 'fallback';988 bodyParams['route'] = 'fallback';
994 }989 }
995990
991 if (request.body.show_thoughts) {
992 bodyParams['include_reasoning'] = true;
993 }
994
996 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);995 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);
997 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {996 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {
998 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);997 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);