Skip to content

engine(streaming): stop-string suffix trim across token-boundary streaming chunks - #306

Draft
jamesburton wants to merge 2 commits into
kkokosa:mainfrom
jamesburton:issue/121-stop-string-streaming
Draft

engine(streaming): stop-string suffix trim across token-boundary streaming chunks#306
jamesburton wants to merge 2 commits into
kkokosa:mainfrom
jamesburton:issue/121-stop-string-streaming

Conversation

@jamesburton

Copy link
Copy Markdown

Summary

Addresses an item from #121. Stop-string suffix trimming now works correctly across token-boundary streaming chunks — a stop string that spans the boundary between two SSE chunks is detected and trimmed at the exact character boundary.

Test results

  • New: TextGeneratorStopStringStreamingTests (251 lines).
  • 6 files changed, 1010 insertions / 33 deletions.

Stack

This PR depends on:

  1. engine(text-generator): trim stop-string suffix at char boundary, not whole token #296 (issue/107-stop-string-suffix — char-boundary trim primitive)

Once #296 lands, this PR will rebase cleanly. The base will then be main.

jamesburton and others added 2 commits June 8, 2026 16:58
… whole token (#107)

When a `StopStringCondition` matched, `TextGenerator` removed the entire last
token from the generated-id list. That over-trimmed whenever the stop string
was a strict suffix of the last token's decoded text — the headline example
from the issue: last token decodes to `"ld<|im_end|>"`, stop string is
`"<|im_end|>"`, the user saw `"Hello, wor"` instead of `"Hello, world"`.

Introduces `StopSuffixTrimmer` — a static helper that finds the longest
stop-string suffix of the decoded text and trims it at the character (UTF-16)
boundary, defending against splitting a surrogate pair. `CheckStopConditions`
now also reports the matched condition index; when the match was a
`StopStringCondition`, the call sites keep the last token in `generatedIds`
and `BuildResponse` performs the suffix trim on the fully-decoded text.

EOS / max-tokens / other non-string stop conditions keep the original
token-removal semantics (their "token" is conceptually the terminator itself,
not text-bearing).

Regression tests cover the headline strict-suffix case, longest-of-multiple
matches, the no-match passthrough, ignoring non-`StopStringCondition` entries,
and the surrogate-pair safety trim.

Note: this PR focuses on the non-streaming `Generate` paths (prefill +
greedy/spec decode loops returning an `InferenceResponse`). The streaming
`GenerateStream` path keeps the existing token-removal behaviour; applying the
same character-level trim to the streaming SSE output is a follow-up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…aming chunks (#121)

Extends the non-streaming stop-string suffix trim from #107 to the SSE
streaming path. Where a stop string may straddle multiple already-decoded
tokens (e.g. "<|im_end|>" split across "<|", "im_", "end|>"), the prior
streaming code emitted the partial-match prefixes verbatim and only
suppressed the final token.

* StreamingStopBuffer (new) — rolling tail buffer holding the last K chars
  (K = max stop-string decoded char-length, computed from registered stop
  conditions). Each appended chunk emits the safe head and retains the
  trailing K chars; on stop-match detection, flushes the trimmed remainder
  up to the match start and emits the SSE terminal sentinel.
* TextGenerator.GenerateStreamingAsync — routes per-chunk text through
  StreamingStopBuffer when at least one StopStringCondition is registered;
  falls through unchanged otherwise (no overhead for stop-token-only stops).
* Discriminating tests — three-token split "<|" / "im_" / "end|>" emits
  zero chunks containing any part of the sequence; with the buffer disabled
  the same input emits two chunks containing partial matches.
* Surrogate-pair safety — buffer trims at char boundaries; multi-char
  codepoints in stop strings are not split.

Co-Authored-By: Claude Opus 4.7 <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