Skip to content

fix(server): send spec-compliant finish_reason on stream chunks - #1246

Open
MAN$I VERMA (mansiverma897993) wants to merge 1 commit into
qualcomm:mainfrom
mansiverma897993:fix/stream-finish-reason
Open

fix(server): send spec-compliant finish_reason on stream chunks#1246
MAN$I VERMA (mansiverma897993) wants to merge 1 commit into
qualcomm:mainfrom
mansiverma897993:fix/stream-finish-reason

Conversation

@mansiverma897993

Copy link
Copy Markdown
Contributor

Fixes #1243

Root cause

The streaming handlers built SSE chunks from the openai-go response structs (openai.ChatCompletionChunk / ChatCompletionChunkChoice). Their FinishReason field is a plain string, so every chunk serialized as "finish_reason": "" — and no terminal chunk was ever emitted before data: [DONE]. OpenAI-compatible clients wait for finish_reason: "stop" on a final empty-delta chunk, so they never detect completion (as reported with AnythingLLM).

Fix

Introduced small local chunk types in cli/server/handler/chat.go with spec-correct JSON semantics and closed every stream with a proper finishing chunk:

  • Intermediate chunks"finish_reason": null (pointer field, no omitempty)
  • Final chunk"delta": {} with finish_reason mapped from the SDK stop_reason via the existing mapFinishReason (so max-token truncation correctly yields "length", everything else "stop")
  • Tool-call streams → tool-call delta chunk with finish_reason: null, then a finishing chunk with "tool_calls"; the parse-fallback text path previously ended the stream with no finishing chunk and no [DONE] — it now terminates properly too
  • Usage chunk (stream_options.include_usage) keeps its "choices": [] shape
  • Chunks now also carry "object": "chat.completion.chunk"

Example final chunk now on the wire:

{"object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

Non-streaming responses are untouched (they already returned finish_reason: "stop").

Testing

  • Added unit tests in cli/server/handler/package_test.go asserting the serialized shapes: null finish_reason on content chunks, "stop" + empty delta on the finishing chunk, empty choices on the usage chunk, and the mapFinishReason table.
  • Verified the exact JSON output against openai-go v3.17.0 (the version pinned in go.mod):
    • intermediate: {"object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":"hello"},"finish_reason":null}]}
    • final: {"object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
  • I don't have a Snapdragon device to run geniex serve end-to-end — Novatoris could you verify with AnythingLLM once a build with this patch is available?

Streaming chunks reused openai-go response structs, whose plain-string
FinishReason marshals as "" on every chunk, and no terminal chunk was
emitted at all. OpenAI-compatible clients (e.g. AnythingLLM) wait for a
final chunk with finish_reason "stop"/"length"/"tool_calls" and never
detect completion. Use local chunk types that serialize finish_reason as
null on intermediate chunks and close every stream with an empty-delta
finishing chunk mapped from the SDK stop_reason; the tool-call fallback
path now also terminates with a finishing chunk and [DONE].

Fixes qualcomm#1243

Signed-off-by: mansiverma897993 <vmansi756@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

serve — Streaming response always returns empty finish_reason instead of null/"stop", breaking OpenAI-compatible clients

1 participant