From 579cae32b1407cd51ff41949883c54bcb8b9a581 Mon Sep 17 00:00:00 2001 From: SHANMUGAPRIYAN Date: Sun, 16 Aug 2026 08:46:36 +0530 Subject: [PATCH] fix(sse): buffer '' across SSE deltas leaked into content instead of being parsed as reasoning. Derive every proper prefix from THINK_OPEN itself so the lookahead list can never drift out of sync with the tag again. Covered by new unit tests for the partial-suffix lookahead and the split-delta buffering path. --- open-sse/utils/thinkTagParser.ts | 14 +++++++---- tests/unit/think-tag-parser.test.ts | 37 +++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/open-sse/utils/thinkTagParser.ts b/open-sse/utils/thinkTagParser.ts index bd75e28ee8..66ed5f7955 100644 --- a/open-sse/utils/thinkTagParser.ts +++ b/open-sse/utils/thinkTagParser.ts @@ -21,6 +21,15 @@ import { appendBoundedText, buildSyntheticChatChunk } from "./streamHelpers.ts"; const THINK_OPEN = ""; const THINK_CLOSE = ""; +/** + * Every proper prefix of `` ("<", " THINK_OPEN.slice(0, i + 1) +); + /** * Create the mutable streaming-parse context for one SSE stream. * `enabled` decides whether the caller should attempt think-tag parsing at @@ -52,10 +61,7 @@ export function initThinkState(isPassthroughMode: boolean, provider?: unknown, m * @returns {boolean} */ export function containsOrMayEndWithThinkOpenTag(value: string): boolean { - return ( - value.includes(THINK_OPEN) || - ["<", " value.endsWith(suffix)) - ); + return value.includes(THINK_OPEN) || THINK_OPEN_PARTIALS.some((suffix) => value.endsWith(suffix)); } /** diff --git a/tests/unit/think-tag-parser.test.ts b/tests/unit/think-tag-parser.test.ts index 2199e9356d..533fcb8d4a 100644 --- a/tests/unit/think-tag-parser.test.ts +++ b/tests/unit/think-tag-parser.test.ts @@ -1,8 +1,14 @@ import test from "node:test"; import assert from "node:assert/strict"; -const { hasThinkTags, extractThinkTags, processStreamingThinkDelta, flushThinkBuffer } = - await import("../../open-sse/utils/thinkTagParser.ts"); +const { + hasThinkTags, + extractThinkTags, + processStreamingThinkDelta, + flushThinkBuffer, + containsOrMayEndWithThinkOpenTag, + applyThinkTag, +} = await import("../../open-sse/utils/thinkTagParser.ts"); test("hasThinkTags detects opening tags and ignores empty input", () => { assert.equal(hasThinkTags("before plan after"), true); @@ -60,6 +66,33 @@ test("processStreamingThinkDelta extracts content and reasoning across split tag }); }); +test("containsOrMayEndWithThinkOpenTag flags a chunk ending on any partial open tag", () => { + for (const partial of ["<", "' across deltas", () => { + const ctx = { enabled: true, active: false, insideThink: false, buffer: "" }; + + const opening: { content: unknown; reasoning_content?: string } = { content: "plananswer", + }; + assert.equal(applyThinkTag(ctx, rest), true); + assert.equal(rest.reasoning_content, "plan"); + assert.equal(rest.content, ""); + + assert.deepEqual(flushThinkBuffer(ctx), { + reasoningDelta: null, + contentDelta: "answer", + }); +}); + test("processStreamingThinkDelta keeps partial closing tags buffered while inside think", () => { const ctx = { insideThink: true, buffer: "" };