fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623) - #6887
Conversation
…eaming OpenAI-to-Claude conversion (#6623)
There was a problem hiding this comment.
Code Review
This pull request introduces support for extracting OpenRouter reasoning and reasoning details during non-streaming OpenAI-to-Claude translation, along with corresponding unit tests. The review feedback correctly identifies a robustness issue in the reasoning extraction helper where non-string truthy values could prematurely halt the fallback chain, and suggests a cleaner implementation using safe string conversions.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| 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 ""; | ||
| } |
There was a problem hiding this comment.
The current implementation of resolveReasoningText has a potential robustness issue. If messageObj.reasoning_content is a truthy non-string value (such as a number or an object), if (messageObj.reasoning_content) will evaluate to true, but toString will return "". This causes the function to return "" immediately, preventing the fallback to reasoning or reasoning_details from being evaluated.
We can make this more robust and consistent by using the existing toString helper to safely extract and validate the string values before checking their truthiness. This also simplifies the type checks and null/undefined guards for reasoning and the individual details.
function resolveReasoningText(messageObj: JsonRecord): string {
const reasoningContent = toString(messageObj.reasoning_content);
if (reasoningContent) {
return reasoningContent;
}
const reasoning = toString(messageObj.reasoning);
if (reasoning) {
return reasoning;
}
if (Array.isArray(messageObj.reasoning_details)) {
const parts: string[] = [];
for (const detail of messageObj.reasoning_details) {
const detailObj = toRecord(detail);
const text = toString(detailObj.text ?? detailObj.content);
if (text) parts.push(text);
}
return parts.join("");
}
return "";
}…eaming OpenAI-to-Claude conversion (diegosouzapw#6623) (diegosouzapw#6887)
…eaming OpenAI-to-Claude conversion (diegosouzapw#6623) (diegosouzapw#6887)
Closes #6623
Root cause
oc/mimo-v2.5-free(opencode zen gateway, OpenRouter-backed) is a reasoning model whose thinking text arrives under OpenRouter-nativemessage.reasoning/message.reasoning_details[]fields, not DeepSeek'smessage.reasoning_content. The non-streaming OpenAI-to-Claude converter (convertOpenAINonStreamingToClaudeinopen-sse/handlers/responseTranslator.ts) only recognizedreasoning_content, so on a truncated (finish_reason: length,content: null) reasoning-only turn it dropped the reasoning entirely and emitted an internal(empty response)placeholder.detectMalformedNonStream(open-sse/utils/diagnostics.ts, added for #5108) correctly flags that placeholder as no real output, whichchatCore.tsturns into an HTTP 502./v1/chat/completionsnever hits this because source format equals target format there, so translation is skipped and the raw body passes through untouched — which is why it appeared to work on that route.The streaming per-chunk translator (
open-sse/translator/response/openai-to-claude.ts) already had thisreasoning/reasoning_details[]fallback (landed via PR #2951 for StepFun/OpenRouter compatibility) — it was never ported to the non-streaming converter. This fix adds aresolveReasoningText()helper mirroring that same fallback chain (reasoning_content→reasoning→reasoning_details[].text|content) toresponseTranslator.ts.Regression test
tests/unit/issue-6623-opencode-mimo-reasoning-details-nonstream.test.ts— reproduces the exact OpenRouter-style payload captured live fromopencode.ai/zen/v1/chat/completions(no auth required) and asserts the/v1/messages(openai→claude) translation is no longer flaggedempty_choicesbydetectMalformedNonStream.AssertionError: 'empty_choices' !== nullGates run (all green)
node scripts/check/check-file-size.mjs— OKnode scripts/check/check-complexity.mjs— OK (2050 ≤ baseline 2054)node scripts/check/check-cognitive-complexity.mjs— OK (885 = baseline 885)node scripts/check/check-changelog-integrity.mjs— OKnpm run typecheck:core— clean, exit 0npx eslint --suppressions-location config/quality/eslint-suppressions.json open-sse/handlers/responseTranslator.ts tests/unit/issue-6623-opencode-mimo-reasoning-details-nonstream.test.ts— exit 0plan3-p0.test.ts,t19-codex-responses-empty-content.test.ts,diagnostics.test.ts) — 71/71 pass, no regressionsNote: unrelated to #1635 (different failure — Codex Responses API missing input/previous_response_id), which the Kilo-bot duplicate flag incorrectly linked.