发布

  • fix(stream): normalize delta.reasoning alias and separate reasoning in client response (#771)

    frostbyte_neo 发布于 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 as delta.reasoning in SSE streaming chunks instead of the
    standard OpenAI delta.reasoning_content field. 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.reasoning and delta.content in the same SSE event.
    After sanitization this becomes reasoning_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:

    1. Reasoning-only event (finish_reason=null, no usage)
    2. Content-only event (carries finish_reason and usage)
    下载附件