Skip to content

fix: report finish_reason "length" from non-streaming /v1/completions - #229

Merged
solderzzc merged 2 commits into
SharpAI:mainfrom
HamedShams:fix/completions-finish-reason-length
Oct 11, 2026
Merged

solderzzc merged 2 commits into
SharpAI:mainfrom
HamedShams:fix/completions-finish-reason-length

Conversation

@HamedShams

Copy link
Copy Markdown
Contributor

Closes #227. Stacked on #228, which rewrote the same loop; the first commit here is #228's and the diff collapses to the second once it lands.

What

/v1/completions without stream now reports finish_reason: "length" when generation hit max_tokens. The handler read info.generationTokenCount from the final .info event but ignored info.stopReason, so finish_reason was hardcoded to "stop". It now uses the same mapping as handleChatNonStreaming; the streaming text path already had it.

A stop sequence still wins: if the text was cut at a client stop string, the answer is "stop" regardless of why generation ended, which is the case you flagged in review. With #228's early exit the .info event never arrives on that path, so the reason stays at its .stop default, and the post-loop trim forces "stop" as well.

Why

finish_reason is how a client tells a truncated answer from a finished one. With this path always saying "stop", a completion that ran out of budget looked complete. Repro and line references in #227.

Verified

solderzzc and others added 2 commits October 11, 2026 12:18
The streaming handlers end generation when a stop sequence matches. The two
non-streaming handlers (chat and text) kept generating to EOS or max_tokens and
trimmed the text afterwards, so the same request spent more compute and reported
a larger completion_tokens when it was not streamed.

Contract tests: finish_reason is "length" on the chat and streaming-text paths
(Test 11), and completion_tokens after a stop sequence agree between streaming
and non-streaming for chat (Test 12) and drop for non-streaming completions
(Test 12b). Tests 12 and 12b fail on main (38 vs 14 and 60 vs 60 tokens).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
handleTextNonStreaming took generationTokenCount from the final .info
event but ignored info.stopReason and hardcoded finish_reason to "stop",
so a completion cut off by max_tokens looked like a natural end. It now
uses the same mapping as handleChatNonStreaming; the streaming text path
already had it. A client stop sequence still reports "stop" whatever
ended generation.

Contract Test 11 gains the non-streaming /v1/completions case it listed
as pending, and TextCompletionFinishReasonTests drives the handler with
a synthetic stream.

Closes SharpAI#227.
@solderzzc

Copy link
Copy Markdown
Member

Thanks @HamedShams — reviewed. The mapping matches handleChatNonStreaming, the stop-sequence override handles the case where .info says .length, and the early-exit from #228 correctly falls back to .stop. The unit tests cover both directions, and the contract test now has the fourth path. Merging once the integration matrix is green.

@solderzzc
solderzzc merged commit b1ae6cc into SharpAI:main Oct 11, 2026
15 checks passed
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.

Non-streaming /v1/completions reports finish_reason: "stop" when max_tokens was hit

2 participants