diff --git a/open-sse/handlers/responseSanitizer.ts b/open-sse/handlers/responseSanitizer.ts
index 146e105c757..68f6307d675 100644
--- a/open-sse/handlers/responseSanitizer.ts
+++ b/open-sse/handlers/responseSanitizer.ts
@@ -72,6 +72,55 @@ function stripZeroWidthText(value: string): string {
return value.replace(/[\u200B-\u200D\uFEFF]/g, "");
}
+/**
+ * Detect whether a string contains the internal reasoning placeholder that
+ * should only appear inside reasoning fields, not as visible content.
+ *
+ * The placeholder is "(prior reasoning summary unavailable)" but upstream
+ * providers sometimes emit it with wrappers/prefixes such as:
+ * - "Thinking: (prior reasoning summary unavailable)"
+ * - "(prior reasoning summary unavailable)"
+ * - "(Prior reasoning summary unavailable.)"
+ *
+ * Match case-insensitively on the core phrase.
+ */
+const REASONING_PLACEHOLDER_CORE = "prior reasoning summary unavailable";
+const REASONING_PLACEHOLDER_RE = new RegExp(REASONING_PLACEHOLDER_CORE.replace(/ /g, "\\s+"), "i");
+
+function isReasoningPlaceholder(value: unknown): boolean {
+ return typeof value === "string" && REASONING_PLACEHOLDER_RE.test(value);
+}
+
+/**
+ * Strip the internal reasoning placeholder from visible content.
+ *
+ * Returns the cleaned string. If the content is ONLY the placeholder (optionally
+ * wrapped in thinking tags / prefix), returns an empty string so the caller
+ * can drop it entirely. If the placeholder appears alongside real content,
+ * only the placeholder fragment is removed.
+ *
+ * Non-streaming path: applied in sanitizeMessageContent.
+ * Streaming path: applied in sanitizeStreamingChunk.
+ */
+function stripReasoningPlaceholderFromContent(content: string): string {
+ if (!REASONING_PLACEHOLDER_RE.test(content)) return content;
+
+ // Try to remove …placeholder… wrapper first.
+ const thinkTagRe =
+ /\s*\(?\s*prior\s+reasoning\s+summary\s+unavailable\.?\s*\)?\s*<\/think>\s*/gi;
+ let cleaned = content.replace(thinkTagRe, "");
+
+ // Remove "Thinking: (prior reasoning summary unavailable)" prefix.
+ const prefixRe = /thinking\s*:\s*\(?\s*prior\s+reasoning\s+summary\s+unavailable\.?\s*\)?\s*/gi;
+ cleaned = cleaned.replace(prefixRe, "");
+
+ // Remove bare placeholder occurrences.
+ const bareRe = /\(?\s*prior\s+reasoning\s+summary\s+unavailable\.?\s*\)?\s*/gi;
+ cleaned = cleaned.replace(bareRe, "");
+
+ return cleaned.trim();
+}
+
function stripZeroWidthToolArgumentJson(value: unknown): string {
return stripZeroWidthText(typeof value === "string" ? value : JSON.stringify(value || {}));
}
@@ -395,7 +444,9 @@ function sanitizeChoice(
function sanitizeMessageContent(msgRecord: JsonRecord, options: ParseOptions = {}): JsonRecord {
if (typeof msgRecord.content === "string") {
- const strippedContent = stripInternalToolEnvelopeText(msgRecord.content);
+ const strippedContent = stripReasoningPlaceholderFromContent(
+ stripInternalToolEnvelopeText(msgRecord.content)
+ );
const nativeReasoning = getReadableReasoningValue(msgRecord);
const { content, thinking } =
options.parseTextualReasoningTags === true && !nativeReasoning
@@ -1035,7 +1086,9 @@ export function sanitizeStreamingChunk(parsed: unknown): unknown {
if (deltaRecord.content !== undefined) {
delta.content =
typeof deltaRecord.content === "string"
- ? collapseExcessiveNewlines(stripZeroWidthText(deltaRecord.content))
+ ? collapseExcessiveNewlines(
+ stripReasoningPlaceholderFromContent(stripZeroWidthText(deltaRecord.content))
+ )
: deltaRecord.content;
}
copyOpenAICompatibleReasoningFields(deltaRecord, delta);
diff --git a/tests/unit/reasoning-placeholder-leak.test.ts b/tests/unit/reasoning-placeholder-leak.test.ts
new file mode 100644
index 00000000000..107e0059e6a
--- /dev/null
+++ b/tests/unit/reasoning-placeholder-leak.test.ts
@@ -0,0 +1,218 @@
+/**
+ * Unit tests for reasoning placeholder leak fix (Issue #8081).
+ *
+ * The internal sentinel "(prior reasoning summary unavailable)" must never
+ * leak into visible message.content — it should only appear inside
+ * reasoning_content / reasoning fields.
+ */
+import { describe, test } from "node:test";
+import assert from "node:assert/strict";
+
+import {
+ sanitizeOpenAIResponse,
+ sanitizeStreamingChunk,
+} from "../../open-sse/handlers/responseSanitizer.ts";
+
+describe("Reasoning placeholder leak (#8081)", () => {
+ // ── Non-streaming ──────────────────────────────────────────────
+
+ test("strips bare placeholder from content (non-streaming)", () => {
+ const body = {
+ id: "chatcmpl-1",
+ object: "chat.completion",
+ created: 1,
+ model: "test",
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: "assistant",
+ content: "(prior reasoning summary unavailable)",
+ },
+ finish_reason: "stop",
+ },
+ ],
+ };
+ const result = sanitizeOpenAIResponse(body) as Record;
+ assert.equal(
+ result.choices[0].message.content,
+ "",
+ "bare placeholder should be stripped to empty string"
+ );
+ });
+
+ test("strips placeholder with trailing period and case variation", () => {
+ const body = {
+ id: "chatcmpl-2",
+ object: "chat.completion",
+ created: 1,
+ model: "test",
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: "assistant",
+ content: "(Prior reasoning summary unavailable.)",
+ },
+ finish_reason: "stop",
+ },
+ ],
+ };
+ const result = sanitizeOpenAIResponse(body) as Record;
+ assert.equal(result.choices[0].message.content, "");
+ });
+
+ test("strips -wrapped placeholder", () => {
+ const body = {
+ id: "chatcmpl-3",
+ object: "chat.completion",
+ created: 1,
+ model: "test",
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: "assistant",
+ content: "(prior reasoning summary unavailable)",
+ },
+ finish_reason: "stop",
+ },
+ ],
+ };
+ const result = sanitizeOpenAIResponse(body) as Record;
+ assert.equal(result.choices[0].message.content, "");
+ });
+
+ test("strips 'Thinking:' prefixed placeholder", () => {
+ const body = {
+ id: "chatcmpl-4",
+ object: "chat.completion",
+ created: 1,
+ model: "test",
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: "assistant",
+ content: "Thinking: (prior reasoning summary unavailable)",
+ },
+ finish_reason: "stop",
+ },
+ ],
+ };
+ const result = sanitizeOpenAIResponse(body) as Record;
+ assert.equal(result.choices[0].message.content, "");
+ });
+
+ test("preserves real content when placeholder is mixed in", () => {
+ const body = {
+ id: "chatcmpl-5",
+ object: "chat.completion",
+ created: 1,
+ model: "test",
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: "assistant",
+ content: "Hello! (prior reasoning summary unavailable) How are you?",
+ },
+ finish_reason: "stop",
+ },
+ ],
+ };
+ const result = sanitizeOpenAIResponse(body) as Record;
+ assert.equal(result.choices[0].message.content, "Hello! How are you?");
+ });
+
+ test("does NOT strip placeholder from reasoning_content", () => {
+ const body = {
+ id: "chatcmpl-6",
+ object: "chat.completion",
+ created: 1,
+ model: "test",
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: "assistant",
+ content: "The answer is 42.",
+ reasoning_content: "(prior reasoning summary unavailable)",
+ },
+ finish_reason: "stop",
+ },
+ ],
+ };
+ const result = sanitizeOpenAIResponse(body) as Record;
+ assert.equal(result.choices[0].message.content, "The answer is 42.");
+ // reasoning_content is preserved (it's the correct field for the placeholder)
+ assert.equal(
+ result.choices[0].message.reasoning_content,
+ "(prior reasoning summary unavailable)"
+ );
+ });
+
+ test("leaves unrelated content untouched", () => {
+ const body = {
+ id: "chatcmpl-7",
+ object: "chat.completion",
+ created: 1,
+ model: "test",
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: "assistant",
+ content: "Just a normal response without any placeholders.",
+ },
+ finish_reason: "stop",
+ },
+ ],
+ };
+ const result = sanitizeOpenAIResponse(body) as Record;
+ assert.equal(
+ result.choices[0].message.content,
+ "Just a normal response without any placeholders."
+ );
+ });
+
+ // ── Streaming ──────────────────────────────────────────────────
+
+ test("strips placeholder from streaming delta content", () => {
+ const chunk = {
+ id: "chatcmpl-s1",
+ object: "chat.completion.chunk",
+ created: 1,
+ model: "test",
+ choices: [
+ {
+ index: 0,
+ delta: {
+ content: "(prior reasoning summary unavailable)",
+ },
+ },
+ ],
+ };
+ const result = sanitizeStreamingChunk(chunk) as Record;
+ assert.equal(result.choices[0].delta.content, "");
+ });
+
+ test("preserves real content in streaming delta alongside placeholder", () => {
+ const chunk = {
+ id: "chatcmpl-s2",
+ object: "chat.completion.chunk",
+ created: 1,
+ model: "test",
+ choices: [
+ {
+ index: 0,
+ delta: {
+ content: "Sure! (prior reasoning summary unavailable) Here is the answer.",
+ },
+ },
+ ],
+ };
+ const result = sanitizeStreamingChunk(chunk) as Record;
+ assert.equal(result.choices[0].delta.content, "Sure! Here is the answer.");
+ });
+});