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: "" };