Skip to content

feat: stream + stop at first sentence boundary past NumPredict (SoftStop) - #157

Open
Saiska wants to merge 1 commit into
DustinHendrickson:mainfrom
Saiska:feat/ollama-soft-stop
Open

feat: stream + stop at first sentence boundary past NumPredict (SoftStop)#157
Saiska wants to merge 1 commit into
DustinHendrickson:mainfrom
Saiska:feat/ollama-soft-stop

Conversation

@Saiska

@Saiska Saiska commented Jun 3, 2026

Copy link
Copy Markdown

What

Makes the Ollama call stream and stop at the first complete-sentence boundary once NumPredict tokens have been generated, so bot replies end on a finished sentence instead of being hard-cut mid-word.

Why

With a finite NumPredict, replies get truncated mid-word (e.g. "Oh man, I'm"). This stops generation at a sentence boundary near the target length instead.

How

  • OllamaHttpClient::PostStreaming — streaming POST via the low-level send() + Request::content_receiver API (portable across bundled httplib versions), aborting cleanly when the receiver returns false (Error::Canceled is treated as success).
  • OllamaStreamAccumulator — reassembles the streamed NDJSON across raw byte chunks, counts response-bearing chunks (~tokens), and stops on tokens >= NumPredict && <sentence boundary>, or on done.
  • QueryOllamaAPI — a streaming branch gated by SoftStopEnable && NumPredict > 0 && !ThinkMode; it sends Ollama a hard ceiling of NumPredict + 32 so the in-progress sentence can finish, and preserves the legacy non-streaming path verbatim in the else.
  • New OllamaChat.SoftStopEnable config key (default 1); reuses NumPredict as the soft target — no new length key.
  • A debug-gated boot self-test (SoftStopSelfTest, 4 cases) validates the accumulator with no network.

Think-mode and NumPredict = 0 (unlimited) deliberately skip soft-stop and use the existing non-streaming path. Toggle off → byte-identical legacy behavior.

Testing

  • Boot self-test: SoftStop self-test: 4/4 passed (stop-at-period / wait-for-boundary / chunk-split / done-flag).
  • Live against a local Ollama: 100+ streaming generations; long replies stop at a sentence boundary ~100–112 tokens past an 80-token target; short replies finish naturally via done; no leaked connections.

AI assistance

This change was developed with AI assistance (Claude / Claude Code).

🤖 Generated with Claude Code

Bot replies were hard-cut mid-word when generation hit NumPredict. This
streams the Ollama response and stops at the first complete-sentence
boundary once NumPredict tokens have been generated, so replies end on a
finished sentence near the target length.

- OllamaHttpClient::PostStreaming: streaming POST via the low-level
  send() + Request::content_receiver API (portable across httplib
  versions), aborting cleanly when the receiver returns false
  (Error::Canceled treated as success).
- OllamaStreamAccumulator: reassembles the streamed NDJSON across raw byte
  chunks, counts response-bearing chunks (~tokens), and stops on
  tokens >= NumPredict && sentence-boundary, or on done.
- QueryOllamaAPI: streaming branch gated by SoftStopEnable && NumPredict>0
  && !ThinkMode; sends a hard ceiling of NumPredict+32 so the in-progress
  sentence can finish; the legacy non-streaming path is preserved in else.
- New OllamaChat.SoftStopEnable config key (default on); reuses NumPredict
  as the soft target (no new length key).
- Debug-gated boot self-test (SoftStopSelfTest, 4 cases) for the accumulator.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.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.

1 participant