diff --git a/open-sse/handlers/sseParser.ts b/open-sse/handlers/sseParser.ts index e1e6136127..4a9626ac7b 100644 --- a/open-sse/handlers/sseParser.ts +++ b/open-sse/handlers/sseParser.ts @@ -52,6 +52,10 @@ export function parseSSEToOpenAIResponse(rawSSE, fallbackModel) { if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) { reasoningParts.push(delta.reasoning_content); } + // Normalize `reasoning` alias (NVIDIA kimi-k2.5 etc.) + if (typeof delta.reasoning === "string" && delta.reasoning.length > 0 && !delta.reasoning_content) { + reasoningParts.push(delta.reasoning); + } // T18: Accumulate tool calls correctly across streamed chunks if (delta.tool_calls) { diff --git a/open-sse/utils/stream.ts b/open-sse/utils/stream.ts index 4c30fe3782..6c274663d7 100644 --- a/open-sse/utils/stream.ts +++ b/open-sse/utils/stream.ts @@ -314,6 +314,12 @@ export function createSSEStream(options: StreamOptions = {}) { const delta = parsed.choices?.[0]?.delta; + // Normalize `reasoning` alias → `reasoning_content` (NVIDIA kimi-k2.5 etc.) + if (delta?.reasoning && typeof delta.reasoning === "string" && !delta.reasoning_content) { + delta.reasoning_content = delta.reasoning; + delete delta.reasoning; + } + // Extract tags from streaming content if (delta?.content && typeof delta.content === "string") { const { content, thinking } = extractThinkingFromContent(delta.content); @@ -323,6 +329,14 @@ export function createSSEStream(options: StreamOptions = {}) { } } + // If reasoning was normalized (reasoning → reasoning_content) or + // tags were extracted, force re-serialization so the client sees + // the standard `reasoning_content` field instead of the raw provider line. + if (delta?.reasoning_content && !injectedUsage) { + output = `data: ${JSON.stringify(parsed)}\n`; + injectedUsage = true; + } + // T18: Track if we saw tool calls & accumulate for call log if (delta?.tool_calls && delta.tool_calls.length > 0) { passthroughHasToolCalls = true; @@ -483,6 +497,16 @@ export function createSSEStream(options: StreamOptions = {}) { if (state?.accumulatedContent !== undefined) state.accumulatedContent += r; } } + // Normalize `reasoning` alias → `reasoning_content` (NVIDIA kimi-k2.5 etc.) + if (parsed.choices?.[0]?.delta?.reasoning && !parsed.choices?.[0]?.delta?.reasoning_content) { + const r = parsed.choices[0].delta.reasoning; + if (typeof r === "string") { + parsed.choices[0].delta.reasoning_content = r; + delete parsed.choices[0].delta.reasoning; + totalContentLength += r.length; + if (state?.accumulatedContent !== undefined) state.accumulatedContent += r; + } + } // Gemini format - may have multiple parts if (parsed.candidates?.[0]?.content?.parts) { diff --git a/open-sse/utils/streamPayloadCollector.ts b/open-sse/utils/streamPayloadCollector.ts index 961dfc46ba..427770fa08 100644 --- a/open-sse/utils/streamPayloadCollector.ts +++ b/open-sse/utils/streamPayloadCollector.ts @@ -157,6 +157,10 @@ function buildOpenAISummary(events: StructuredSSEEvent[], fallbackModel?: string if (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) { reasoningParts.push(delta.reasoning_content); } + // Normalize `reasoning` alias (NVIDIA kimi-k2.5 etc.) + if (typeof delta.reasoning === "string" && delta.reasoning.length > 0 && !delta.reasoning_content) { + reasoningParts.push(delta.reasoning); + } if (Array.isArray(delta.tool_calls)) { for (const item of delta.tool_calls) {