From 411a5897815353094e1965bf6145e7dfbd41e37d Mon Sep 17 00:00:00 2001 From: fjia Date: Wed, 17 Jun 2026 09:43:58 +0700 Subject: [PATCH] fix(claude-to-openai): handle OpenAI-format responses in non-streaming path Some providers (e.g. xiaomi-tokenplan -claude models) return OpenAI-format responses even when request was translated to Claude. Early-return now detects choices[]. Also strip reasoning_content only when content is non-empty so thinking models keep their only output. Closes #1836 Co-authored-by: Cursor --- .../handlers/chatCore/nonStreamingHandler.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/open-sse/handlers/chatCore/nonStreamingHandler.js b/open-sse/handlers/chatCore/nonStreamingHandler.js index 3c31e7a1..00040658 100644 --- a/open-sse/handlers/chatCore/nonStreamingHandler.js +++ b/open-sse/handlers/chatCore/nonStreamingHandler.js @@ -76,7 +76,12 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source // missing/null (e.g. M3 with max_tokens:1 spends the budget on thinking // and returns `content: null`). Returning the raw body would leave the // OpenAI client without a `choices` array and surface as a UI test error. - if (responseBody.content && !Array.isArray(responseBody.content)) return responseBody; + // Early return if the response is already in OpenAI format (has choices array) + // or if it has content as a non-array value (likely a different non-Claude format). + // Some providers (e.g. xiaomi-tokenplan) return OpenAI-format responses even when + // the request was translated to Claude format — the targetFormat is Claude but the + // actual response is OpenAI-native and needs no further translation. + if (responseBody.choices || (responseBody.content && !Array.isArray(responseBody.content))) return responseBody; let textContent = "", thinkingContent = ""; const toolCalls = []; @@ -193,11 +198,14 @@ export async function handleNonStreamingResponse({ providerResponse, provider, m translatedResponse.usage = filterUsageForFormat(addBufferToUsage(translatedResponse.usage), sourceFormat); } - // Strip reasoning_content — some clients (e.g. Firecrawl AI SDK) have JSON parsers that - // break on this non-standard field, even though OpenAI allows it in extensions. + // Strip reasoning_content only when content is non-empty. + // When content is empty (e.g. thinking models that used all tokens for reasoning), + // reasoning_content is the only useful output and must be preserved. if (translatedResponse?.choices) { for (const choice of translatedResponse.choices) { - if (choice?.message) delete choice.message.reasoning_content; + if (choice?.message?.reasoning_content && choice.message.content) { + delete choice.message.reasoning_content; + } } }