From 4d3868f4de8137e535ba14c90ae5ee70ce426ad0 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Sat, 11 Jul 2026 09:52:16 -0300 Subject: [PATCH] fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) --- .../fixes/6623-6623-mimo-502-messages.md | 1 + open-sse/handlers/responseTranslator.ts | 30 ++++++++++++- ...e-mimo-reasoning-details-nonstream.test.ts | 44 +++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/6623-6623-mimo-502-messages.md create mode 100644 tests/unit/issue-6623-opencode-mimo-reasoning-details-nonstream.test.ts diff --git a/changelog.d/fixes/6623-6623-mimo-502-messages.md b/changelog.d/fixes/6623-6623-mimo-502-messages.md new file mode 100644 index 00000000000..5d476c21efb --- /dev/null +++ b/changelog.d/fixes/6623-6623-mimo-502-messages.md @@ -0,0 +1 @@ +- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) diff --git a/open-sse/handlers/responseTranslator.ts b/open-sse/handlers/responseTranslator.ts index 93f6bc17842..faaf50d84be 100644 --- a/open-sse/handlers/responseTranslator.ts +++ b/open-sse/handlers/responseTranslator.ts @@ -560,6 +560,31 @@ export function translateNonStreamingResponse( return intermediateOpenAI; } +/** + * Resolve reasoning/thinking text off a non-streaming OpenAI-format message object. + * Checks DeepSeek-style `reasoning_content`, then the OpenRouter/StepFun aliases + * `reasoning` and `reasoning_details[]` (array of { text | content }), mirroring the + * streaming translator's fallback chain in open-sse/translator/response/openai-to-claude.ts. + */ +function resolveReasoningText(messageObj: JsonRecord): string { + if (messageObj.reasoning_content) { + return toString(messageObj.reasoning_content); + } + if (typeof messageObj.reasoning === "string" && messageObj.reasoning) { + return messageObj.reasoning; + } + if (Array.isArray(messageObj.reasoning_details)) { + const parts: string[] = []; + for (const detail of messageObj.reasoning_details) { + const detailObj = toRecord(detail); + const text = detailObj.text ?? detailObj.content; + if (typeof text === "string" && text) parts.push(text); + } + return parts.join(""); + } + return ""; +} + /** * Helper to convert an OpenAI chat.completion JSON object to Claude format for non-streaming. */ @@ -578,11 +603,12 @@ function convertOpenAINonStreamingToClaude(openaiResponse: JsonRecord): JsonReco let hasTextOrReasoning = false; - if (messageObj.reasoning_content) { + const reasoningText = resolveReasoningText(messageObj); + if (reasoningText) { hasTextOrReasoning = true; content.push({ type: "thinking", - thinking: toString(messageObj.reasoning_content), + thinking: reasoningText, }); } diff --git a/tests/unit/issue-6623-opencode-mimo-reasoning-details-nonstream.test.ts b/tests/unit/issue-6623-opencode-mimo-reasoning-details-nonstream.test.ts new file mode 100644 index 00000000000..4507c344150 --- /dev/null +++ b/tests/unit/issue-6623-opencode-mimo-reasoning-details-nonstream.test.ts @@ -0,0 +1,44 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { translateNonStreamingResponse } from "../../open-sse/handlers/responseTranslator.ts"; +import { detectMalformedNonStream } from "../../open-sse/utils/diagnostics.ts"; +import { isEmptyContentResponse } from "../../open-sse/services/errorClassifier.ts"; + +const mimoOpenRouterStyleResponse = { + id: "gen-1783636289-lJcRXwMde7qjgDJfHiBC", + object: "chat.completion", + created: 1783636289, + model: "mimo-v2.5-free", + choices: [ + { + index: 0, + finish_reason: "length", + logprobs: null, + message: { + role: "assistant", + content: null, + refusal: null, + reasoning: "Hmm, the user just said hi", + reasoning_details: [ + { type: "reasoning.text", text: "Hmm, the user just said hi", format: "unknown", index: 0 }, + ], + }, + }, + ], + usage: { prompt_tokens: 248, completion_tokens: 10, total_tokens: 258 }, +}; + +test("#6623 raw responseBody is not flagged empty by isEmptyContentResponse", () => { + assert.equal(isEmptyContentResponse(mimoOpenRouterStyleResponse), false); +}); + +test("#6623 /v1/messages non-stream translation of an OpenRouter reasoning-only turn is flagged malformed (502) - RED", () => { + const translated = translateNonStreamingResponse(mimoOpenRouterStyleResponse, "openai", "claude", null); + const malformedReason = detectMalformedNonStream(translated); + assert.equal(malformedReason, null); +}); + +test("#6623 /v1/chat/completions (openai->openai, no translation) is unaffected", () => { + const passthrough = translateNonStreamingResponse(mimoOpenRouterStyleResponse, "openai", "openai", null); + assert.equal(passthrough, mimoOpenRouterStyleResponse); +});