-
fix(stream): normalize delta.reasoning alias and separate reasoning in client response (#771)
发布于
2026-03-29 19:12:22 +00:00 - fix(stream): normalize delta.reasoning to reasoning_content in SSE streaming
NVIDIA kimi-k2.5 (and potentially other providers) send reasoning
tokens asdelta.reasoningin SSE streaming chunks instead of the
standard OpenAIdelta.reasoning_contentfield. This caused reasoning
content to be silently dropped during stream passthrough — clients
received only the final answer with no reasoning separation.The non-streaming sanitizer (responseSanitizer.ts) already handled this
alias, but the streaming pipeline did not.Fix applied in 4 locations:
- stream.ts passthrough: normalize + force re-serialize sanitized chunk
- stream.ts translate: accumulate reasoning from delta.reasoning
- sseParser.ts: collect delta.reasoning in parseSSEToOpenAIResponse
- streamPayloadCollector.ts: collect delta.reasoning in buildOpenAISummary
- fix: eliminate injectedUsage reuse bug and add reasoning alias tests
- Detect delta.reasoning alias before sanitizeStreamingChunk() which
already normalizes it, removing dead post-sanitization normalization - Replace injectedUsage reuse with separate needsReserialization flag
so reasoning re-serialization cannot block finish_reason/usage
mutations on the same SSE chunk (fixes CRITICAL review finding) - Add unit test for parseSSEToOpenAIResponse reasoning alias
- Add unit test for buildStreamSummaryFromEvents reasoning alias
- fix(stream): separate reasoning from content in passthrough response body
The passthroughAccumulatedContent variable was mixing delta.content and
delta.reasoning_content into one string, causing the client_response
log and responseBody to lose reasoning separation.- Add passthroughAccumulatedReasoning accumulator for reasoning deltas
- Set message.reasoning_content in responseBody when reasoning exists
- Only accumulate delta.content into passthroughAccumulatedContent
- fix: trim leading whitespace from assembled content in log summaries
NVIDIA and other providers emit token deltas with leading spaces
(e.g. ' The', ' user'). When joined, these produce a leading space in
the provider_response and parsed non-streaming response logs. Trim
the joined content and reasoning_content in both buildOpenAISummary
and parseSSEToOpenAIResponse for consistent log output.- fix(stream): split combined reasoning+content deltas into separate SSE events
Some providers (e.g. NVIDIA NIM) send transition chunks with both
delta.reasoninganddelta.contentin the same SSE event.
After sanitization this becomesreasoning_content+content,
which violates the standard OpenAI streaming contract where these
fields are never mixed. Clients using if/else logic (LobeChat, etc.)
skip content when reasoning_content is present, losing the first
content token.Split such combined chunks into two separate SSE events:
- Reasoning-only event (finish_reason=null, no usage)
- Content-only event (carries finish_reason and usage)
下载附件