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 @@
19771977 </span>
19781978 </div>
19791979 </div>
19801980 <div class="range-block" data-source="makersuite,deepseek,openrouter">
19811981 <label for="openai_show_thoughts" class="checkbox_label widthFreeExpand">
19821982 <input id="openai_show_thoughts" type="checkbox" />
19831983 <span>
public/script.js+9 -12
@@ -170,7 +170,7 @@ import {
170170 isElementInViewport,
171171 copyText,
172172} from './scripts/utils.js';
173173import { debounce_timeout, THINK_BREAK } from './scripts/constants.js';
174174
175175import { doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from './scripts/extensions.js';
176176import { COMMENT_NAME_DEFAULT, executeSlashCommandsOnChatInput, getSlashCommandsHelp, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, processChatSlashCommands, stopScriptExecution } from './scripts/slash-commands.js';
@@ -5700,10 +5700,6 @@ function getTextContextFromData(data) {
57005700function extractMessageFromData(data){
57015701 const content = String(getTextContextFromData(data) ?? '');
57025702
5703- if (content.includes(THINK_BREAK)) {
5704- return content.split(THINK_BREAK)[1];
5705- }
5706-
57075703 return content;
57085704}
57095705
@@ -5713,14 +5709,15 @@ function extractMessageFromData(data){
57135709 * @returns {string} Extracted reasoning
57145710 */
57155711function extractReasoningFromData(data) {
5716- const content = String(getTextContextFromData(data) ?? '');
5712+ if (main_api === 'openai' && oai_settings.show_thoughts) {
5717-
5713+ 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) {
57235715 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+ }
57245721 }
57255722
57265723 return '';
public/scripts/constants.js+0 -5
@@ -14,8 +14,3 @@ export const debounce_timeout = {
1414 /** [5 sec] For delayed tasks, like auto-saving or completing batch operations that need a significant pause. */
1515 extended: 5000,
1616};
17-
18-/**
19- * Custom boundary for splitting the text between the model's reasoning and the actual response.
20- */
21-export const THINK_BREAK = '##�THINK_BREAK�##';
public/scripts/openai.js+5 -0
@@ -2161,6 +2161,11 @@ function getStreamingReply(data, state) {
21612161 state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || '');
21622162 }
21632163 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 ?? '';
21642169 } else {
21652170 return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? '';
21662171 }
public/scripts/sse-stream.js+15 -0
@@ -235,6 +235,21 @@ async function* parseStreamData(json) {
235235 }
236236 return;
237237 }
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+ }
238253 else if (typeof json.choices[0].delta.content === 'string' && json.choices[0].delta.content.length > 0) {
239254 for (let j = 0; j < json.choices[0].delta.content.length; j++) {
240255 const str = json.choices[0].delta.content[j];
src/constants.js+0 -5
@@ -413,8 +413,3 @@ export const VLLM_KEYS = [
413413 'guided_decoding_backend',
414414 'guided_whitespace_pattern',
415415];
416-
417-/**
418- * Custom boundary for splitting the text between the model's reasoning and the actual response.
419- */
420-export const THINK_BREAK = '##�THINK_BREAK�##';
src/endpoints/backends/chat-completions.js+6 -7
@@ -7,7 +7,6 @@ import {
77 CHAT_COMPLETION_SOURCES,
88 GEMINI_SAFETY,
99 OPENROUTER_HEADERS,
10- THINK_BREAK,
1110} from '../../constants.js';
1211import {
1312 forwardFetchResponse,
@@ -392,11 +391,7 @@ async function sendMakerSuiteRequest(request, response) {
392391 const responseContent = candidates[0].content ?? candidates[0].output;
393392 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);
400395 if (!responseText) {
401396 let message = 'Google AI Studio Candidate text empty';
402397 console.log(message, generateResponseJson);
@@ -404,7 +399,7 @@ async function sendMakerSuiteRequest(request, response) {
404399 }
405400
406401 // Wrap it back to OAI format
407402 const reply = { choices: [{ 'message': { 'content': responseText } }], responseContent };
408403 return response.send(reply);
409404 }
410405 } catch (error) {
@@ -993,6 +988,10 @@ router.post('/generate', jsonParser, function (request, response) {
993988 bodyParams['route'] = 'fallback';
994989 }
995990
991+ if (request.body.show_thoughts) {
992+ bodyParams['include_reasoning'] = true;
993+ }
994+
996995 let cachingAtDepth = getConfigValue('claude.cachingAtDepth', -1);
997996 if (Number.isInteger(cachingAtDepth) && cachingAtDepth >= 0 && request.body.model?.startsWith('anthropic/claude-3')) {
998997 cachingAtDepthForOpenRouterClaude(request.body.messages, cachingAtDepth);